12

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

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

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

4

5 回答 5

8

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

可能的变体是:

如果添加另一个框架不是一个选项:您为 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);
} 

摘自“ 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 容器等等。当然,您可以忽略这个定义,但它非常有用)如果您同意将单元测试与其他测试区分开来是有用的,并且这个定义有助于实现这一点,那么您将寻求另一个框架或至少另一种方法不是单元的测试,您将为不同类型的测试创建单独的套件,依此类推。

希望,这会有所帮助)

于 2012-09-13T12:25:14.227 回答
2

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

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

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

于 2012-09-13T10:56:58.513 回答
1

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

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

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

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

于 2012-09-13T21:44:10.973 回答
1

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

Example Page 1,输入 2 输入,点击回车,验证 page 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) {
  ...
}

...
于 2012-09-13T11:15:26.947 回答
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;

    }

}
于 2015-10-05T10:32:02.840 回答