Selenium + JUnit:测试顺序/流程?

Posted

技术标签:

【中文标题】Selenium + JUnit:测试顺序/流程?【英文标题】:Selenium + JUnit: test order/flow? 【发布时间】:2012-09-06 10:23:21 【问题描述】:

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

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

在过去几天阅读了大量有关测试的内容后,我在这个特定主题上找不到任何有趣的东西。因此,我现在征求意见。

我已经确定的可能解决方案:

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

    将所有测试(用于页面 A、页面 B 等)分组到一种测试方法下。但我读过它很糟糕,请参阅SO question: junit-one-test-case-per-method-or-multiple-test-cases-per-method。做硒测试时有那么糟糕吗?我已经看到一些代码这样做了,所以我认为它可能不是。

    将所有测试(对于页面 A、页面 B 等)分组到一个测试方法下,但使用 JUnit 的 ErrorCollector 类:ErrorCollector 允许您在同一方法中执行有序检查并产生特定的如果一个失败但让方法(因此检查)运行到最后,则会出现错误消息。这个解决方案对我来说似乎太“残酷”了。

    使用 JUnit 的 TestSuite 类:它按照套件中定义的测试类的顺序运行套件中列出的测试。所以这将涉及在测试类中使用独立的测试方法来测试页面 A(比如 TestA),然后是在测试类中测试页面 B 的所有测试方法(比如 TestB),等等。然后将它们插入到测试套件中,例如 @SuiteClasses( TestA.class, TestB.class, TestC.class, ... )

    将JUnit's TestSuite class 与 JUnit 的 ErrorCollector 类结合使用。哦,好吧,既然我们可以,您可能希望在不同的类中对每个页面进行分组测试,并在该组页面之上使用 ErrorCollector 测试“区域”。如果您的网页非常密集或其他原因,此解决方案可能非常有用。

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

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

【问题讨论】:

这可能是我在 SO 上看到的最合格且结构良好的问题。 谢谢。这很讨人喜欢。在向 *** 上的其他人学习之后,我正在尽力而为。我得说,这个网站确实教的比 IT 多得多,我用得越多,我就越能把复杂的问题用书面和清晰的文字表达出来 ..而且通常情况下,这实际上在我问完问题之前就解决了问题:)。您是否开始使用建议的行为测试工具之一?我很想听。 是的,没错。最后我没有太多时间使用这些工具,因为我很快就转到了前端工作。我确实尝试过 TestNG,发现它很好用,在我的偏见中比 JUnit 更方便/直观 ;-) 【参考方案1】:

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

使用该概念,您可以让您的 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;

    


【讨论】:

【参考方案2】:

JUnit 并不是真正为流/集成级别测试而设计的。

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

(是的,这对于单元测试来说是一种不好的做法——但我们并不是在谈论单元测试,即使它们使用 jUnit 运行)。

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

【讨论】:

【参考方案3】:

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

可能的变种是:

BDD 框架:已经提到Cucumber、JDave、JBehave、Spock。例如,Spock 是基于 JUnit 和 Groovy =) TestNG

如果添加另一个框架不是一个选项:您为 JUnit 枚举了更多选项,那么我可以想象 =) 我会将流的整个测试脚本放在一个测试方法中,并将测试代码组织到“驱动程序”中。这意味着您的端到端测试不会直接调用您的应用程序或 Selenium API 的方法,而是将它们包装到驱动程序组件的方法中,这些方法隐藏了 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);
 

sn-p 取自“Growing Object-Oriented Software Guided by Tests”。这本书真的很棒,我强烈推荐阅读它。

这是真正的端到端测试,使用真正的 XMPP 连接、Openfire jabber 服务器和 WindowLicker Swing GUI 测试框架。但是如果将所有这些东西卸载到驱动程序组件。在您的测试中,您只会看到不同的参与者是如何交流的。它是有序的:在应用程序开始投标后,我们检查拍卖服务器是否收到加入请求,然后我们指示拍卖服务器报告新价格并检查它是否反映在 UI 中等等。完整代码可在github 获得。

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

关于制作所有测试单元。除了诸如用于数学、字符串处理等的实用程序库之类的东西之外,任何东西都是不可能的。如果您有完全单元测试的应用程序,这意味着您不是测试所有应用程序,或者您不了解哪些测试是单元测试,哪些不是。第一种情况可能没问题,但所有未涵盖的内容都必须由开发人员、测试人员、用户或任何人手动测试和重新测试。这很常见,但最好是有意识的决定而不是随意的决定。为什么不能对所有内容进行单元测试?

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

文件系统 网络、套接字、RMI 等 图形界面 多线程 外部库需要测试框架并支持 Hamcrest 等简单库

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

因此,您知道,无论您如何重新编码,您都无法对 Web 应用程序进行完整的单元测试。因为它有流、JSP、servlet 容器等等。当然,您可以忽略这个定义,但它非常有用)如果您同意将单元测试与其他测试区分开来是有用的,并且这个定义有助于实现这一点,那么您将寻求另一个框架或至少另一种方法不是单元的测试,您将为不同类型的测试创建单独的套件,依此类推。

希望,这会有所帮助)

【讨论】:

我明白你的意思:我想做的不是单元测试,因此 JUnit 很可能不是最好的工具(因此得名)。我将研究您建议的其他框架。但我也在考虑另一种解决方案:重新编码我的网络应用程序,以便可以访问网页而无需彼此之间的“逻辑”依赖(也可能是更简洁的设计)。完成此操作后,我可以单独测试页面,从而执行单元测试,对吗? 我没有得到“将测试代码组织成驱动程序”的东西。试图通过查看 github repo 来尝试,但这个例子可能太难开始了。感谢推荐书,已添加到我的 ToRead 列表中:) 我会扩展答案,因为它对于 cmets 来说太长了,好吗? =) 呸!那确实很长。感谢您的努力,考虑到您做了多少解释,您确实应该得到正确的答案。 Ps:我会去读那本书,但它必须等待..我的书架上已经有几本了!【参考方案4】:

我们使用框架调用 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) 
  ...


...

【讨论】:

我觉得这和我还没用过的testNG很像。 不,TestNG 没有提供任何类似于 BDD 的东西。它使用测试方法和/或组之间的直接依赖关系。从字面上看,您声明:“方法 A 取决于方法 B”;这保证了方法 A 将在方法 B 之后被调用。仅此而已。【参考方案5】:

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

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

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

【讨论】:

对于单元测试来说这是非常糟糕的做法,但对于问题所假设的验收测试却不是这样。事实上,使用前面的步骤作为设置可能是一种不好的接受习惯。例如,它可能导致运行时间非常长的测试,这对单元和验收总是不利的。

以上是关于Selenium + JUnit:测试顺序/流程?的主要内容,如果未能解决你的问题,请参考以下文章

Selenium 中的 JUnit 注解

使用 JUnit (Java) 的 Selenium 单元测试

Selenium 可以截取 JUnit 测试失败的屏幕截图吗?

Web自动化测试Selenium+Eclipse+Junit+TestNG+Python

Selenium - 以编程方式将 html 转换为 junit

基于webdriver的jmeter性能测试-Eclipse+Selenium+JUnit生成jar包