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

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

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

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

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

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

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

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

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

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

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

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

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

可能的变种是:

  • BDD框架:已经提到过Cucumber , JDave , JBehave , Spock 。 例如,Spock基于JUnit而Groovy =)
  • TestNG的

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

摘录摘自“ 以测试为导向的面向对象的软件 ”。 这本书真的很棒,我强烈建议阅读它。

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

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

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

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

  • 文件系统
  • 网络,套接字,RMI等
  • GUI
  • multithreading
  • 外部库需要测试框架并支持像Hamcrest这样的简单库

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

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

希望,这会有所帮助)

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

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

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

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

示例第1页,输入2输入,单击enter,validation第2页加载了什么。

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

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

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

 ... @Given("^Page 1$") public void iLoadPage1() { WebDriver driver = new .... driver.go('URL'); } @Given("I enter (.*)$") public void iEnterTest(String txt) { ... } ... 

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

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

(是的,对于unit testing来说这是一个不好的做法 – 但即使它们使用jUnit运行,我们也不会谈论unit testing)。

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

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

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

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

在单元级测试中,我肯定会说这个解决方案forms不好,但是当你集成像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 oneIsTrue = new Predicate() { public boolean apply(SystemState input) { return input.one; } }; Predicate twoIsTrue = new Predicate() { public boolean apply(SystemState input) { return input.two; } }; Predicate threeIsTrue = new Predicate() { public boolean apply(SystemState input) { return input.three; } }; Predicate fourIsTrue = new Predicate() { public boolean apply(SystemState input) { return input.four; } }; Predicate oneIsFalse = new Predicate() { public boolean apply(SystemState input) { return !input.one; } }; Predicate twoIsFalse = new Predicate() { public boolean apply(SystemState input) { return !input.two; } }; Predicate threeIsFalse = new Predicate() { public boolean apply(SystemState input) { return !input.three; } }; Predicate fourIsFalse = new Predicate() { public boolean apply(SystemState input) { return !input.four; } }; List 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 verifyStartState; Runnable changeState; Predicate verifyEndState; public SequentialParams(Predicate pre, Runnable task, Predicate 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; } }