Selenium是一套支持浏览器自动化的工具和库,它主要用于网络应用测试。Selenium的组件之一是Selenium WebDriver,它提供客户端库、JSON线协议(与浏览器驱动程序通信的协议)和浏览器驱动程序。Selenium WebDriver的主要优势之一是它被所有主要的编程语言所支持,并且可以在所有主要的操作系统上运行。
在JUnit 5与Selenium WebDriver教程的这一部分,你将了解到JUnit 5的额外功能,这些功能将帮助你通过并行运行测试、配置测试顺序和创建参数化测试来减少测试的执行时间。
你还将学习如何利用Selenium Jupiter的功能,如通过系统属性进行测试执行配置,单一浏览器会话测试以加快测试执行或在测试中进行截图。最后,你将学习如何将AssertJ库添加到你的项目中。
用JUnit 5执行并行测试
JUnit 5带有内置的并行测试执行支持。
下面的命令将并行地运行TodoMvcTests的测试方法:
./gradlew clean test --tests *TodoMvcTests -Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=concurrent
构建成功,在执行过程中,你应该注意到有两个Chrome浏览器的实例正在运行。在这次运行中,所有测试的执行时间减少到10秒:
> Task :test
pl.codeleak.demos.selenium.todomvc.TodoMvcTests > createsTodo() PASSED
pl.codeleak.demos.selenium.todomvc.TodoMvcTests > createsTodosWithSameName() PASSED
pl.codeleak.demos.selenium.todomvc.TodoMvcTests > togglesAllTodosCompleted() PASSED
pl.codeleak.demos.selenium.todomvc.TodoMvcTests > togglesTodoCompleted() PASSED
pl.codeleak.demos.selenium.todomvc.TodoMvcTests > clearsCompletedTodos() PASSED
pl.codeleak.demos.selenium.todomvc.TodoMvcTests > editsTodo() PASSED
pl.codeleak.demos.selenium.todomvc.TodoMvcTests > removesTodo() PASSED
BUILD SUCCESSFUL in 10s
4 actionable tasks: 4 executed
JUnit 5的测试执行顺序
自动测试应该能够独立运行,没有特定的顺序,以及测试的结果不应该依赖于以前的测试结果。但在有些情况下,测试执行的特定顺序是合理的。
默认情况下,在JUnit 5中,测试方法的执行在构建之间是可重复的,因此是确定的,但该算法是故意不明显的(正如库的作者所说)。幸运的是,执行顺序可以根据我们的需要使用内置的方法排序器或创建自定义的方法排序器来调整。我们将使用@Order 注解来提供测试方法的顺序,我们将用@TestMethodOrder 注解类来指示JUnit 5方法是有序的:
@ExtendWith(SeleniumExtension.class)
@SingleSession
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@DisplayName("Managing Todos")
class TodoMvcTests {
@Test
@Order(1)
@DisplayName("Creates Todo with given name")
void createsTodo() {
}
@Test
@Order(2)
@DisplayName("Creates Todos all with the same name")
void createsTodosWithSameName() {
}
// rest of the methods omitted for readability
}
使用Selenium Jupiter的单一浏览器会话
你可能注意到,对于TodoMvcTests 类中的每个测试,都会启动一个新的Chrome浏览器实例,并在每个测试后关闭它。这种行为导致整个套件的执行需要相当长的时间(在之前的执行中是27秒)。Selenium Jupiter提供了一个方便的类注释,可以改变这种行为。@SingleSession 注释改变了行为,使浏览器的实例在所有测试前被初始化一次,在所有测试后被关闭。
为了应用@SingleSession ,我们需要稍微修改测试类,将驱动对象注入构造函数,而不是注入@BeforeEach 方法。我们还需要照顾到每个测试的适当状态。这可以通过清除@AfterEach 方法中存储todos的本地存储来完成。我还创建了一个字段driver ,以保持所有测试中使用的驱动对象实例。
我测试了@SingleSession ,将驱动程序注入到@BeforeEach 和@AfterEach 方法中,但似乎这并不像预期的那样工作,每次执行新的测试都会创建一个新的驱动程序实例。我相信这是该库的另一个设计缺陷:
private final ChromeDriver driver;
public TodoMvcTests(ChromeDriver driver) {
this.driver = driver;
this.todoMvc = PageFactory.initElements(driver, TodoMvcPage.class);
this.todoMvc.navigateTo();
}
@AfterEach
void storageCleanup() {
driver.getLocalStorage().clear();
}
当我们执行测试时,我们可以观察到执行所有测试的时间明显减少:
./gradlew clean test
> Task :test
pl.codeleak.demos.selenium.todomvc.TodoMvcTests > editsTodo() PASSED
pl.codeleak.demos.selenium.todomvc.TodoMvcTests > togglesTodoCompleted() PASSED
pl.codeleak.demos.selenium.todomvc.TodoMvcTests > createsTodo() PASSED
pl.codeleak.demos.selenium.todomvc.TodoMvcTests > removesTodo() PASSED
pl.codeleak.demos.selenium.todomvc.TodoMvcTests > togglesAllTodosCompleted() PASSED
pl.codeleak.demos.selenium.todomvc.TodoMvcTests > createsTodosWithSameName() PASSED
pl.codeleak.demos.selenium.todomvc.TodoMvcTests > clearsCompletedTodos() PASSED
pl.codeleak.demos.selenium.todomvc.SeleniumTest > projectIsConfigured(ChromeDriver) PASSED
BUILD SUCCESSFUL in 9s
3 actionable tasks: 3 executed
提示:如果你希望从选定的类中运行测试,你可以使用Gradle测试任务附带的测试过滤。例如,这个命令将只运行TodoMvcTests类的测试。
./gradlew clean test --tests *.todomvc.TodoMvcTests
单个浏览器会话测试的并行执行
请注意,如果你现在尝试使用JUnit 5的并行性来执行测试,测试会失败。在并行执行中,每个方法都需要单独的驱动实例,而在启用@SingleSession ,我们有一个单一的实例共享给所有测试。为了解决这个问题,我们需要运行测试配置并行执行,使顶级类并行运行,但方法在同一线程。
只要复制TodoMvcTests类并尝试以下命令:
./gradlew clean test --tests *TodoMvcTests -Djunit.jupiter.execution.parallel.enabled=true -Djunit.jupiter.execution.parallel.mode.default=same_thread -Djunit.jupiter.execution.parallel.mode.classes.default=concurrent
当执行过程中,你应该看到有3个浏览器在运行,终端的输出与下面类似:
<===========--> 87% EXECUTING [3s]
> :test > 0 tests completed
> :test > Executing test pl.codeleak.demos.selenium.todomvc.MoreTodoMvcTests
> :test > Executing test pl.codeleak.demos.selenium.todomvc.EvenMoreTodoMvcTests
> :test > Executing test pl.codeleak.demos.selenium.todomvc.TodoMvcTests
使用Selenium Jupiter的通用驱动配置
在当前的测试中,我们直接将ChromeDriver注入到测试类中。但在有些情况下,我们希望对注入的驱动有更多的控制,我们宁愿注入WebDriver(接口),然后再决定应该注入哪个驱动实例。我们还需要改变storageCleanup() 方法,因为通用WebDriver不提供直接的localStorage访问:
public TodoMvcTests(WebDriver driver) {
this.driver = driver;
this.todoMvc = PageFactory.initElements(driver, TodoMvcPage.class);
this.todoMvc.navigateTo();
}
@AfterEach
void storageCleanup() {
((JavascriptExecutor) driver).executeScript("window.localStorage.clear()");
}
而现在为了在运行时改变浏览器类型,我们需要调整sel.jup.default.browser 配置属性。
配置JUnit 5和Selenium Jupiter的常用方法之一是通过Java系统属性。这可以通过程序完成,使用属性文件,也可以通过使用-D 开关直接将属性传递给JVM。为了确保在执行Gradle时传递给JVM的属性在测试中可用,我们需要修改build.gradle ,如下所示:
test {
// Make system properties available in tests
systemProperties System.getProperties()
useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed"
}
}
就目前而言,当你运行像./gradlew clean test -Dprop=value 这样的命令时,属性prop 将在测试中可用。
有了上述改变,我们就可以选择浏览器类型来运行测试了:
./gradlew clean test --tests *TodoMvcTests -Dsel.jup.default.browser=firefox
用Selenium Jupiter保存屏幕截图
Selenium Jupiter允许在测试结束时保存屏幕截图--总是或只在失败时保存。你还可以自定义输出目录和格式:
./gradlew clean test --tests *TodoMvcTests -Dsel.jup.default.browser=firefox -Dsel.jup.screenshot.at.the.end.of.tests=true -Dsel.jup.screenshot.format=png -Dsel.jup.output.folder=/tmp
提示。请查阅文档以了解更多选项:https://bonigarcia.github.io/selenium-jupiter/#screenshots
JUnit 5的参数化测试
参数化单元测试的一般想法是对不同的测试数据运行相同的测试方法。为了在JUnit 5中创建一个参数化测试,你用@ParameterizedTest 注释一个测试方法,并为测试方法提供参数源。有几个可用的参数源,包括:
@ValueSource- 提供了对字面值数组的访问,即shorts, ints, strings等。@MethodSource- 提供对工厂方法返回值的访问@CsvSource- 从一个或多个提供的CSV行中读取逗号分隔的值(CSV)。@CsvFileSource- 用于加载逗号分隔的值(CSV)文件。
在接下来的例子中,我们将使用存储在src/test/resources 目录中的以下CSV:
todo;done
Buy the milk;false
Clean up the room;true
Read the book;false
为了在我们的测试中使用上述CSV文件,我们需要在测试中使用@ParameterizedTest 注释(而不是@Test ),然后是指向该文件的@CsvFileSource 注释:
@ParameterizedTest
@CsvFileSource(resources = "/todos.csv", numLinesToSkip = 1, delimiter = ';')
@DisplayName("Creates Todo with given name")
void createsTodo(String todo) {
todoMvc.createTodo(todo);
assertSingleTodoShown(todo);
}
CSV文件中的每条记录都有两个字段:name 和done 。在上述测试中,只使用了todo的名字。但我们当然可以将测试复杂化一些,使用两个属性:
@ParameterizedTest(name = "{index} - {0}, done = {1}" )
@CsvFileSource(resources = "/todos.csv", numLinesToSkip = 1, delimiter = ';')
@DisplayName("Creates and optionally removes Todo with given name")
void createsAndRemovesTodo(String todo, boolean done) {
todoMvc.createTodo(todo);
assertSingleTodoShown(todo);
todoMvc.showActive();
assertSingleTodoShown(todo);
if (done) {
todoMvc.completeTodo(todo);
assertNoTodoShown(todo);
todoMvc.showCompleted();
assertSingleTodoShown(todo);
}
todoMvc.removeTodo(todo);
assertNoTodoShown(todo);
}
请注意,在同一个测试类中,允许有多个参数化测试。
使用AssertJ更好的断言
JUnit 5有很多内置的断言,但当真正的工作开始时,你可能需要比JUnit 5所提供的更多。在这种情况下,我推荐AssertJ库。AssertJ AssertJ是一个Java库,提供了丰富的断言集,真正有用的错误信息,提高了测试代码的可读性,并被设计成在你最喜欢的IDE中超级容易使用。
AssertJ的一些特点:
- 对许多Java类型进行流畅的断言,包括日期、集合、文件等。
- SoftAssertions(类似于JUnit 5的assertAll)
- 复杂的字段比较
- 可以轻松扩展--自定义条件和自定义断言
要在一个项目中使用AssertJ,我们需要在build.gradle 中添加一个依赖关系:
testCompile('org.assertj:assertj-core:3.13.2')
为了开始使用,我们需要静态地导入org.assertj.core.api.Assertions.* ,并使用assertThat 方法的代码完成。assertThat(objectUnderTest).
例如,在普通的JUnit 5中,你会用AssertJ写assertThat(todoMvc.getTodosLeft()).isEqualTo(3); ,而不是assertEquals(3, todoMvc.getTodosLeft()); ,或者用assertThat(todoMvc.todoExists(readTheBook)).isTrue() ,而不是assertTrue(todoMvc.todoExists(readTheBook)) 。
与复杂类型的工作甚至更好:
todoMvc.createTodos(buyTheMilk, cleanupTheRoom, readTheBook);
assertThat(todoMvc.getTodos())
.hasSize(3)
.containsSequence(buyTheMilk, cleanupTheRoom, readTheBook);
总结
在这篇文章中,我介绍了如何利用JUnit 5的内置功能来提高项目配置的执行速度,但不仅仅如此。你还了解了如何利用Selenium Jupiter的某些特性来改进项目。