Selenium + JUnit:测试订单/流量?

时间:2012-09-13 10:41:29

标签: java java-ee testing selenium junit

我正在使用Selenium来测试我的java web应用程序的html页面(实际上是JSP)。我的网络应用程序需要一个流程来访问每个页面(它是一个小型的在线游戏网络应用程序),如:要进入页面B,您需要转到页面A,输入一些文本并按一个按钮进入页面B.显然我已经有一些测试来验证页面A是否正常工作。

我希望能够编写更多测试,以便检查在页面A运行测试之后,我将获得运行页面B的测试(对于应用程序的其余部分依此类推)。简而言之:在我的测试中以某种方式定义一些顺序。

在过去几天做了很多关于测试的阅读之后,我找不到关于这个特定主题的任何有趣内容。因此,我现在正在征求意见。

我发现可能的解决方案:

  1. 为页面A定义(在同一测试类中)测试方法,然后测试测试B的测试方法。然后命令执行测试方法。但我们知道JUnit(但TestNG确实)不允许测试方法执行顺序,请参阅SO question selenium-junit-tests-how-do-i-run-tests-within-a-test-in-sequential-order

  2. 在一个测试方法下对所有测试(对于第A页,第B页,等等)进行分组。但我读到它很糟糕,请参阅SO question: junit-one-test-case-per-method-or-multiple-test-cases-per-method。做硒测试时是不是很糟糕?我见过一些代码,所以我认为它可能不是。

  3. 在一个测试方法下对所有测试(对于第A页,第B页,依此类推)进行分组,但使用JUnit的ErrorCollector类:ErrorCollector允许您使用相同的方法执行有序检查,如果一个失败,则产生一个特定的错误消息但是让方法(因此检查)一直运行到结束。这个解决方案对我来说太“野蛮”了。

  4. 使用JUnit的TestSuite类:它在套件中列出了测试类中定义的测试类。因此,这将涉及在测试类中测试页面A的独立测试方法(比如说TestA),然后测试测试类中的页面B的所有测试方法(比如说TestB),依此类推。然后将它们插入测试套件中,例如@SuiteClasses({TestA.class,TestB.class,TestC.class,...})

  5. JUnit's TestSuite class与JUnit的ErrorCollector类结合使用。哦,好吧,既然我们可以,你可能想要在不同的课程中对每页进行分组测试,&在该组页面顶部使用ErrorCollector测试“区域”。如果您有非常密集的网页或其他原因,此解决方案可能非常有用。

  6. 相当激进:使用其他工具(例如TestNG)可以访问测试方法排序等功能。

  7. 注意:我想有些人会推荐最后一个解决方案(迁移到TestNG),但我也希望听到与JUnit相关的其他想法/意见。如果我在一个不能(由于某种原因)迁移到另一个测试框架的团队中工作,那么他们将如何处理这个测试订购问题呢?

5 个答案:

答案 0 :(得分:8)

为什么要迁移?您可以使用JUnit进行单元测试,使用另一个框架进行更高级别的测试。在您的情况下,它是一种接受或功能或端到端,如何命名它并不重要。但重要的是要了解这些测试单位。他们坚持不同的规则:它们更复杂,运行时间更长,更少,它们需要复杂的设置,外部依赖性,并且可能偶尔会失败。为什么不为它们使用另一个框架(甚至是另一种编程语言)?

可能的变体是:

如果添加另一个框架不是一个选项:你枚举了JUnit的更多选项,那么我可以想象=)我会将整个测试脚本放在一个测试方法中,并将测试代码组织到" Drivers&# 34 ;.这意味着您的端到端测试不会直接调用您的应用程序或Selenium API的方法,而是将它们包装到Driver组件的方法中,这些方法隐藏了API的复杂性,看起来像是发生了什么或预期的内容。看一下这个例子:

@Test 
public void sniperWinsAnAuctionByBiddingHigher() throws Exception {
    auction.startSellingItem();

    application.startBiddingIn(auction);
    auction.hasReceivedJoinRequestFrom(ApplicationRunner.SNIPER_XMPP_ID);

    auction.reportPrice(1000, 98, "other bidder");
    application.hasShownSniperIsBidding(auction, 1000, 1098);

    auction.hasReceivedBid(1098, ApplicationRunner.SNIPER_XMPP_ID);

    auction.reportPrice(1098, 97, ApplicationRunner.SNIPER_XMPP_ID);
    application.hasShownSniperIsWinning(auction, 1098);

    auction.announceClosed();
    application.hasShownSniperHasWonAuction(auction, 1098);
} 

摘录自" Growing Object-Oriented Software Guided by Tests"。这本书真的很棒,我强烈建议阅读它。

这是真正的端到端测试,它使用真正的XMPP连接,Openfire jabber服务器和WindowLicker Swing GUI测试框架。但所有这些东西如果卸载到驱动程序组件。在你的测试中,你只看到不同的演员如何沟通。并且它被命令:在申请开始竞标之后我们检查拍卖服务器收到加入请求,然后我们指示拍卖服务器报告新价格并检查它是否反映在UI等。整个代码可在github上找到。

github上的示例很复杂,因为应用程序并不像书籍示例中常见的那样简单。但是这本书逐渐给出了它,我能够按照书籍指南从头开始构建整个应用程序。事实上,这是我读过的关于TDD和自动化开发人员测试的唯一一本书,它提供了如此全面而完整的示例。而且我已经阅读了很多这些内容。但请注意,驱动程序方法不会使您的测试单元。它只是让你隐藏复杂性。它也可以(也应该)与其他框架一起使用。如果需要,它们只是为您提供了将测试分成连续步骤的其他可能性;编写用户可读的测试用例;将测试数据外部化为CSV,Excel表格,XML文件或数据库,以使测试超时;与外部系统,servlet和DI容器集成;定义和运行单独的测试组;提供更多用户友好的报告等。

关于制作所有测试单元。对于数学,字符串处理等实用程序库之类的东西是不可能的。如果您的应用程序完全经过单元测试,则表示您不是测试所有应用程序,也不知道哪些测试是单元测试,哪些测试不是。第一种情况可能没问题,但是未覆盖的所有内容必须由开发人员,测试人员,用户或任何人手动测试和重新测试。这是很常见的,但最好是有意识的决定而不是随意的决定。为什么你不能单元测试一切?

有很多单元测试的定义,它导致圣战)我更喜欢以下内容:"单元测试是单独测试程序单元"。有人说:"嘿,单位是我的申请!我测试登录,它是简单的单元功能"。但也有隐藏在隔离中的语用学。为什么我们需要将单元测试与其他测试区别开来?因为这是我们的第一个安全网。他们必须快。您经常提交(例如git),并且在每次提交之前至少运行 。但想象一下," unit"测试需要5分钟才能运行。您可以较少地运行它们,也可以减少运行次数,或者您当时只运行一个测试用例甚至一种测试方法,或者等待每2分钟一次,以便在5分钟内完成测试。在5分钟内你将去Coding Horror,你将在接下来的2小时内度过=)并且单元测试绝不能偶尔失败。如果他们这样做 - 你就不会相信他们。因此,隔离:您必须从单元测试中隔离缓慢和偶发故障的来源。因此,隔离意味着单元测试不应使用:

  • 文件系统
  • 网络,套接字,RMI等
  • GUI
  • 多线程
  • 外部库需要测试框架和支持简单库,如Hamcrest

单元测试必须是本地的。当您在编码后2分钟内出现缺陷时,您希望只有一个左右的测试失败,而不是整个套件的一半。这意味着您在单元测试中测试有状态行为非常有限。您不应该进行测试设置,使5个状态转换达到前提条件。因为第一次转换中的失败将至少打破4次后续转换测试,以及当前为第6次转换编写的另一次测试。任何非平凡的应用程序都有相当多的流和状态转换。所以这不能进行单元测试。出于同样的原因,单元测试不得在数据库,静态字段,Spring上下文或其他任何内容中使用可更改的共享状态。这正是JUnit为每个测试方法创建测试类的新实例的原因。

所以,你知道,无论你如何重新编写它,你都无法完全对web应用进行单元测试。因为它有流,JSP,servlet容器,可能更多。当然,你可以忽略这个定义,但它很有用)如果你同意区分单元测试与其他测试是有用的,并且这个定义有助于实现这一点,那么你将去另一个框架或至少另一个框架对于非单元测试的方法,您将为不同类型的测试创建单独的套件,依此类推。

希望,这会有所帮助)

答案 1 :(得分:2)

您可以实现自己的Runner,它可能会根据yo9u定义的标准包装其他Runner和排序测试。这是你真的需要这个。

但我不认为这是必要的。我知道如果页面A不起作用,传递给B也不会有效。因此,您希望首先运行测试A,然后运行测试A-> B。但它真的有意义吗?例如,如果测试A-> B首先运行并且因为它无法到达页面A而失败,则其他测试验证测试A也将失败。因此,两个测试都将失败,而id不依赖于测试顺序。

但是,如果您的意思是要将测试A用作测试B的设置操作,则这是非常糟糕的做法。您可以使用测试A的逻辑作为测试B的开始,但是您不应该在两个测试之间进行耦合。一个明显的原因是这使得调试非常困难。要调试测试A-> B,您必须同时运行A和A-> B测试,这意味着您可能必须运行所有测试(至少在一个测试用例中)。

答案 2 :(得分:1)

我们使用框架调用Cucumber。它的行为驱动发展。基本上,您可以创建与流程和使用功能无关的测试功能来控制测试流程。

示例页面1,输入2输入,单击输入,验证第2页已加载某些内容。

然后你会在一个功能文件中有这个:

Given Page 1
   Then I enter text1
   Then I enter text2
   Then I click button
   Then I see page 2

在您的代码中,您可以拥有一个实现这些步骤的类。使用黄瓜框架,您可以使用注释来表示测试代码和功能文件之间的映射。

...
@Given("^Page 1$")
public void iLoadPage1() {
  WebDriver driver = new ....
  driver.go('URL');
}

@Given("I enter (.*)$")
public void iEnterTest(String txt) {
  ...
}

...

答案 3 :(得分:1)

JUnit并非真正用于流/集成级别测试。

对于这种情况,我设计了自己的Runner来保证测试运行的顺序,并且还为所有测试重用了测试类的相同实例,因此您可以将步骤的值传递给另一个。

(是的,对于单元测试来说这是一个不好的做法 - 但即使它们使用jUnit运行,我们也不会讨论那里的单元测试。)

使用其他工具(黄瓜,fitnesse,TestNG等)也是一个很好的解决方案 - 但项目中有太多的测试工具。

答案 4 :(得分:1)

另一个可行的选择是将JUnit Parameterization应用于您的测试。我目前的理解是,实现的参数总是按照提供的顺序执行。

使用该概念,您可以让您的JUnit实现接受URL作为构造函数参数,并根据提供的参数在内部分叉测试。

为了确保您使用相同的WebDriver引用,可能需要静态@ BeforeClass / @ AfterClass声明。有了这个,你可以让参数链相互关联,有效地测试“从上一次测试我在第X页。在这里我将执行任务Y.在这个测试结束时,我将在第Z页,或在州A“。

在单元级测试中,我肯定会说这个解决方案形式不好,但是当你集成像Selenium这样的工具时,你就开始在集成测试级别上进行操作。我自己对这个概念比较陌生,但是在集成测试级别中,模块化规则看起来有点模糊,因为你会有条件依赖。

我很好奇,所以我试了一下。如果我们假设我们可以将应用程序视为与测试相关的静态资源,那就像我想的那样。

package demo.testing;

import java.util.List;

import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
import org.junit.runners.Parameterized.Parameters;

import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Lists;

@RunWith(Parameterized.class)
public class SequentialParams {

    private static SystemState state;

    @BeforeClass
    public static void validateBeforeState() {
        state = new SystemState();

        Assert.assertFalse(state.one);
        Assert.assertFalse(state.two);
        Assert.assertFalse(state.three);
        Assert.assertFalse(state.four);
    }

    @Parameters
    public static Object buildParameters() {
        Runnable reset = new Runnable() {

            public void run() {
                state.one = false;
                state.two = false;
                state.three = false;
                state.four = false;
            }
        };
        Runnable oneToTrue = new Runnable() {

            public void run() {
                state.one = true;
            }
        };
        Runnable twoToTrue = new Runnable() {

            public void run() {
                state.two = true;
            }
        };
        Runnable threeToTrue = new Runnable() {

            public void run() {
                state.three = true;
            }
        };
        Runnable fourToTrue = new Runnable() {

            public void run() {
                state.four = true;

            }
        };


        Predicate<SystemState> oneIsTrue = new Predicate<SequentialParams.SystemState>() {
            public boolean apply(SystemState input) {
                return input.one;
            }
        };
        Predicate<SystemState> twoIsTrue = new Predicate<SequentialParams.SystemState>() {
            public boolean apply(SystemState input) {
                return input.two;
            }
        };
        Predicate<SystemState> threeIsTrue = new Predicate<SequentialParams.SystemState>() {
            public boolean apply(SystemState input) {
                return input.three;
            }
        };
        Predicate<SystemState> fourIsTrue = new Predicate<SequentialParams.SystemState>() {
            public boolean apply(SystemState input) {
                return input.four;
            }
        };

        Predicate<SystemState> oneIsFalse = new Predicate<SequentialParams.SystemState>() {
            public boolean apply(SystemState input) {
                return !input.one;
            }
        };
        Predicate<SystemState> twoIsFalse = new Predicate<SequentialParams.SystemState>() {
            public boolean apply(SystemState input) {
                return !input.two;
            }
        };
        Predicate<SystemState> threeIsFalse = new Predicate<SequentialParams.SystemState>() {
            public boolean apply(SystemState input) {
                return !input.three;
            }
        };
        Predicate<SystemState> fourIsFalse = new Predicate<SequentialParams.SystemState>() {
            public boolean apply(SystemState input) {
                return !input.four;
            }
        };
        List<Object[]> params = Lists.newArrayList();

        params.add(new Object[]{Predicates.and(oneIsFalse, twoIsFalse, threeIsFalse, fourIsFalse), oneToTrue, Predicates.and(oneIsTrue, twoIsFalse, threeIsFalse, fourIsFalse)});
        params.add(new Object[]{Predicates.and(oneIsTrue, twoIsFalse, threeIsFalse, fourIsFalse), twoToTrue, Predicates.and(oneIsTrue, twoIsTrue, threeIsFalse, fourIsFalse)});
        params.add(new Object[]{Predicates.and(oneIsTrue, twoIsTrue, threeIsFalse, fourIsFalse), threeToTrue, Predicates.and(oneIsTrue, twoIsTrue, threeIsTrue, fourIsFalse)});
        params.add(new Object[]{Predicates.and(oneIsTrue, twoIsTrue, threeIsTrue, fourIsFalse), fourToTrue, Predicates.and(oneIsTrue, twoIsTrue, threeIsTrue, fourIsTrue)});

        params.add(new Object[]{ Predicates.and(oneIsTrue, twoIsTrue, threeIsTrue, fourIsTrue), reset, Predicates.and(oneIsFalse, twoIsFalse, threeIsFalse, fourIsFalse)});

        params.add(new Object[]{Predicates.and(oneIsFalse, twoIsFalse, threeIsFalse, fourIsFalse), threeToTrue, Predicates.and(oneIsFalse, twoIsFalse, threeIsTrue, fourIsFalse)});
        params.add(new Object[]{Predicates.and(oneIsFalse, twoIsFalse, threeIsTrue, fourIsFalse), oneToTrue, Predicates.and(oneIsTrue, twoIsFalse, threeIsTrue, fourIsFalse)});
        params.add(new Object[]{Predicates.and(oneIsTrue, twoIsFalse, threeIsTrue, fourIsFalse), fourToTrue, Predicates.and(oneIsTrue, twoIsFalse, threeIsTrue, fourIsTrue)});
        params.add(new Object[]{Predicates.and(oneIsTrue, twoIsFalse, threeIsTrue, fourIsTrue), twoToTrue, Predicates.and(oneIsTrue, twoIsTrue, threeIsTrue, fourIsTrue)});


        return params;
    }

    Predicate<SystemState> verifyStartState;
    Runnable changeState;
    Predicate<SystemState> verifyEndState;

    public SequentialParams(Predicate<SystemState> pre, Runnable task, Predicate<SystemState> post) {
      verifyStartState = pre;
      changeState = task;
      verifyEndState = post;
    }

    @Test
    public void perform() {
        Assert.assertTrue(verifyStartState.apply(state));
       changeState.run();
       Assert.assertTrue(verifyEndState.apply(state));
    }


    private static class SystemState {
        public boolean one = false;
        public boolean two = false;
        public boolean three = false;
        public boolean four = false;

    }

}