接上次博客:测试开发(6)软件测试教程——自动化测试selenium(自动化测试介绍、如何实施、Selenium介绍 、Selenium相关的API)-CSDN博客
目录
Junit框架解析
什么是Junit框架
JUnit框架是一个用于Java编程语言的单元测试框架。它允许开发人员编写和运行自动化的单元测试,以验证代码的各个单元(通常是方法)是否按预期工作。JUnit提供了一组注解和断言方法,使得编写和组织测试用例变得简单而灵活。通过JUnit,开发人员可以快速有效地测试他们的代码,并确保其行为符合预期。JUnit框架也被广泛应用于持续集成和测试驱动开发(TDD)等软件开发实践中。
JUnit框架的作用
JUnit框架主要有以下作用:
-
单元测试: JUnit框架用于编写和执行单元测试,以验证代码的各个单元(通常是方法)是否按照预期工作。单元测试是软件开发过程中的重要组成部分,通过编写各种测试用例,开发人员可以确保代码在各种情况下都能正确地执行。
-
自动化测试: JUnit框架支持自动化测试,开发人员可以编写一次测试用例,然后重复执行多次,以确保代码的行为符合预期。这种自动化测试机制提高了测试的效率和可靠性,可以在短时间内进行大量测试,并及时发现潜在的问题。
-
断言和验证: JUnit框架提供了一组断言方法,用于验证代码的输出是否符合预期。开发人员可以使用这些断言方法检查方法的返回值、异常情况和其他预期结果,从而确保代码的正确性和健壮性。
-
测试报告生成: JUnit框架生成详细的测试报告,用于描述测试执行的结果。这些报告通常包括测试用例的执行情况、通过率、失败的测试用例以及失败的原因等信息,帮助开发人员及时发现和解决问题。
-
持续集成: JUnit框架被广泛用于持续集成(CI)环境中,与CI工具(如Jenkins、Travis CI等)结合使用,自动执行测试套件,并及时反馈测试结果。这种集成能够保证代码的稳定性和可靠性,提高团队的开发效率。
总之,JUnit框架在软件开发过程中发挥着重要的作用,帮助开发人员确保代码的质量和可靠性,同时提高了开发效率和工作效率。
JUnit框架的特点
JUnit框架的主要特点包括:
-
简单易用: JUnit提供了简洁清晰的API和注解,使得编写和运行测试用例变得非常简单和直观。开发人员无需编写复杂的测试框架代码,只需按照约定俗成的方式编写测试方法即可。
-
灵活性: JUnit框架支持各种类型的测试,包括单元测试、集成测试、功能测试等。开发人员可以根据需要选择合适的测试类型,并在测试用例中使用适当的断言和验证方法。
-
自动化测试: JUnit框架支持自动化测试,可以重复运行测试用例,以确保代码的稳定性和可靠性。自动化测试可以大大提高测试效率,减少人工测试的工作量。
-
丰富的断言和验证: JUnit提供了丰富的断言方法,用于验证方法的返回值、异常情况和其他预期结果。开发人员可以根据需要选择合适的断言方法,并编写自定义的验证逻辑。
-
集成开发环境(IDE)支持: JUnit框架得到了各种集成开发环境(如Eclipse、IntelliJ IDEA等)的支持,使得在开发过程中编写、运行和调试测试用例变得更加方便和高效。
-
持续集成支持: JUnit框架被广泛应用于持续集成(CI)环境中,与CI工具(如Jenkins、Travis CI等)结合使用,自动执行测试套件,并及时反馈测试结果。这种集成能够保证代码的稳定性和可靠性,提高团队的开发效率。
总之,JUnit框架具有简单易用、灵活性强、自动化测试、丰富的断言和验证、集成开发环境支持以及持续集成支持等特点,是Java开发中最常用的单元测试框架之一。
重要概念
JUnit框架中的重要概念包括:
-
测试用例(Test Case): 测试用例是针对代码中的一个或多个单元进行测试的描述性规范。每个测试用例通常对应一个方法或函数,用于验证代码在特定输入条件下的行为是否符合预期。
-
断言(Assertion): 断言是测试用例中的一种检查机制,用于验证代码的实际输出是否符合预期输出。JUnit提供了一组断言方法(如assertEquals、assertTrue、assertFalse等),用于比较实际值和预期值,如果断言失败,则测试用例失败。
-
测试套件(Test Suite): 测试套件是一组相关的测试用例的集合,用于对代码进行全面的测试。JUnit框架支持将多个测试用例组合成一个测试套件,并一次性执行所有测试用例。
-
测试运行器(Test Runner): 测试运行器是JUnit框架的核心组件之一,用于执行测试用例和生成测试报告。JUnit提供了多种测试运行器(如JUnitCore、JUnit4、JUnit5等),用于不同类型的测试场景。
-
注解(Annotation): 注解是一种元数据标记,用于在测试代码中提供额外的信息或指令。JUnit框架提供了一系列注解(如@Test、@Before、@After、@BeforeClass、@AfterClass等),用于标记测试方法、初始化方法、清理方法等。
-
测试生命周期(Test Lifecycle): 测试生命周期描述了测试用例的执行过程,包括测试初始化、测试执行、测试清理等阶段。JUnit框架通过注解来定义和控制测试生命周期,确保测试用例的执行顺序和状态管理。
-
异常测试(Exception Testing): 异常测试是一种测试方法,用于验证代码在特定条件下是否会抛出预期的异常。JUnit框架提供了专门的断言方法(如assertThrows),用于验证方法是否抛出指定类型的异常。
-
参数化测试(Parameterized Testing): 参数化测试是一种测试方法,允许在多组输入参数下执行相同的测试用例。JUnit框架通过注解(如@ParameterizedTest、@ValueSource、@CsvSource等)支持参数化测试,提高了测试用例的复用性和覆盖范围。
以上是JUnit框架中的一些重要概念,了解并熟练运用这些概念可以帮助我们更有效地编写和执行单元测试,提高代码的质量和稳定性。
引入依赖
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.9.1</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.9.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-params -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>5.9.1</version>
</dependency>
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<version>1.9.1</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.platform/junit-platform-suite -->
<dependency>
<groupId>org.junit.platform</groupId>
<artifactId>junit-platform-suite</artifactId>
<version>1.9.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-engine -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.1</version>
<scope>test</scope>
</dependency>
-
org.junit.jupiter:junit-jupiter-api:5.9.1:
这是JUnit 5 Jupiter API 模块的依赖。它包含了编写JUnit Jupiter 测试所需的核心API和注解,例如@Test、@BeforeEach、@AfterEach等。如果我们要编写基于JUnit 5的测试用例,这个依赖是必需的。 -
org.junit.jupiter:junit-jupiter-params:5.9.1:
这是JUnit 5 Jupiter 参数化测试模块的依赖。它提供了支持参数化测试的注解和工具类,例如@ParameterizedTest、@ValueSource、@CsvSource等。如果我们需要编写参数化测试,就需要引入这个依赖。 -
org.junit.platform:junit-platform-suite:1.9.1:
这是JUnit Platform Suite Engine 模块的依赖。JUnit Platform 提供了一个测试平台,用于执行不同测试框架的测试,并支持在各种环境中运行测试。Suite Engine 允许我们创建测试套件,并在测试运行时执行一组测试。这个依赖通常在构建自定义测试运行器或扩展时使用。 -
org.junit.jupiter:junit-jupiter-engine:5.9.1:
这是JUnit 5 Jupiter Engine 模块的依赖。JUnit Jupiter Engine 是JUnit 5的核心引擎,负责执行JUnit Jupiter测试。如果我们使用JUnit 5作为测试框架,并且需要在构建和运行测试时使用JUnit Jupiter Engine,则需要引入这个依赖。
总的来说,这些依赖提供了JUnit 5 测试框架所需的核心组件、参数化测试支持、测试平台和执行引擎。根据具体的测试需求和环境配置,你可以选择性地引入这些依赖来构建和运行你的测试套件。
当我们在Java方法上添加类似@Test的注解时,IntelliJ IDEA会自动识别并提示我们是否需要导入JUnit框架。如果我们的项目中已经包含了JUnit依赖,并且我们已经在IDEA中正确配置了JUnit,那么IDEA会自动导入所需的JUnit相关类和包。如果我们的项目中尚未包含JUnit依赖,IDEA也会提示我们添加相应的依赖。
常用的注解
在JUnit框架中,常用的注解包括:
-
@Test: 用于标记测试方法,表示该方法是一个测试用例。JUnit会执行所有标记了@Test注解的方法,并报告测试结果。
-
@Before: 用于标记初始化方法,在每个测试方法执行之前执行。常用于初始化测试环境,准备测试数据等操作。
-
@After: 用于标记清理方法,在每个测试方法执行之后执行。常用于清理测试环境,释放资源等操作。
-
@BeforeClass: 用于标记静态初始化方法,只会执行一次,在所有测试方法执行之前执行。常用于初始化共享的资源,准备测试环境等操作。
-
@AfterClass: 用于标记静态清理方法,只会执行一次,在所有测试方法执行之后执行。常用于释放共享的资源,清理测试环境等操作。
-
@Ignore: 用于标记测试方法或测试类,表示该方法或类暂时不执行测试。通常用于临时禁用测试用例或测试类。
-
@RunWith: 用于指定测试运行器,可以替换JUnit默认的测试运行器。常用于集成其他测试框架或自定义测试逻辑。
-
@Parameters: 用于标记参数化测试方法,指定测试方法的输入参数。常与@ParameterizedTest注解一起使用。
-
@DisplayName: 用于指定测试方法或测试类的显示名称,用于在测试报告中提供更友好的测试名称。
-
@Timeout: 用于指定测试方法的超时时间,如果测试方法执行时间超过指定时间,则测试失败。
这些注解提供了丰富的功能和灵活性,可以帮助我们更有效地编写和管理测试用例。通过合理使用这些注解,可以提高测试代码的可读性、可维护性和可靠性。
除了上述提到的常用注解外,JUnit 5 还引入了一些新的注解,用于支持更灵活的测试生命周期管理和参数化测试。其中包括:
-
@BeforeAll: 用于标记静态方法,在所有测试方法执行之前执行,仅执行一次。常用于初始化共享资源或准备测试环境的操作。
-
@AfterAll: 用于标记静态方法,在所有测试方法执行之后执行,仅执行一次。常用于释放共享资源或清理测试环境的操作。
-
@BeforeEach: 用于标记方法,在每个测试方法执行之前执行,执行次数与测试方法个数相同。常用于初始化测试数据或准备测试环境的操作。
-
@AfterEach: 用于标记方法,在每个测试方法执行之后执行,执行次数与测试方法个数相同。常用于清理测试数据或重置测试环境的操作。
-
@Order:在测试类中,如果有多个测试方法,可以使用 @Order 注解为它们指定执行的顺序。这样可以确保测试方法按照指定的顺序执行,而不是按照默认的顺序执行。例如,@Order(1) 用于标记测试方法的顺序为第一个执行。这样就能够确保在执行测试时,先执行这个测试方法。
我们用代码具体来看:
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import static org.junit.jupiter.api.Assertions.*;
// 指定测试方法的执行顺序
@TestMethodOrder(OrderAnnotation.class)
public class Junit {
// 在所有测试方法执行之前执行,仅执行一次,用于初始化共享资源
@BeforeAll
static void 在所有测试前执行() {
System.out.println("执行所有测试之前的初始化工作");
}
// 在每个测试方法执行之前执行,执行次数与测试方法个数相同,用于准备测试环境
@BeforeEach
void 在每个测试前执行() {
System.out.println("执行每个测试之前的准备工作");
}
// 测试用例:加法运算
@Test
@Order(1)
@DisplayName("测试1:加法运算")
void 加法测试() {
int result = Calculator.add(3, 5);
assertEquals(8, result);
System.out.println("+++++++++++++++++这是测试1+++++++++++++++++");
}
// 测试用例:减法运算
@Test
@Order(2)
@DisplayName("测试2:减法运算")
void 减法测试() {
int result = Calculator.subtract(10, 4);
assertEquals(6, result);
System.out.println("+++++++++++++++++这是测试2+++++++++++++++++");
}
// 在每个测试方法执行之后执行,执行次数与测试方法个数相同,用于清理测试环境
@AfterEach
void 在每个测试后执行() {
System.out.println("执行每个测试之后的清理工作");
}
// 在所有测试方法执行之后执行,仅执行一次,用于释放共享资源
@AfterAll
static void 在所有测试后执行() {
System.out.println("执行所有测试之后的清理工作");
}
// 测试方法:暂时禁用的测试用例
@Test
@Disabled("这个测试用例暂时被禁用")
void 被禁用的测试() {
// 此测试用例将不会被执行
System.out.println("这行文字不会被打印");
}
}
// 加法和减法运算类
class Calculator {
static int add(int a, int b) {
return a + b;
}
static int subtract(int a, int b) {
return a - b;
}
}
现在我们调换一下测试1和测试2的位置:
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.MethodOrderer.OrderAnnotation;
import static org.junit.jupiter.api.Assertions.*;
// 指定测试方法的执行顺序
@TestMethodOrder(OrderAnnotation.class)
public class Junit {
// 在所有测试方法执行之前执行,仅执行一次,用于初始化共享资源
@BeforeAll
static void 在所有测试前执行() {
System.out.println("执行所有测试之前的初始化工作");
}
// 在每个测试方法执行之前执行,执行次数与测试方法个数相同,用于准备测试环境
@BeforeEach
void 在每个测试前执行() {
System.out.println("执行每个测试之前的准备工作");
}
// 测试用例:减法运算
@Test
@Order(2)
@DisplayName("测试2:减法运算")
void 减法测试() {
int result = Calculator.subtract(10, 4);
assertEquals(6, result);
System.out.println("+++++++++++++++++这是测试2+++++++++++++++++");
}
// 测试用例:加法运算
@Test
@Order(1)
@DisplayName("测试1:加法运算")
void 加法测试() {
int result = Calculator.add(3, 5);
assertEquals(8, result);
System.out.println("+++++++++++++++++这是测试1+++++++++++++++++");
}
// 在每个测试方法执行之后执行,执行次数与测试方法个数相同,用于清理测试环境
@AfterEach
void 在每个测试后执行() {
System.out.println("执行每个测试之后的清理工作");
}
// 在所有测试方法执行之后执行,仅执行一次,用于释放共享资源
@AfterAll
static void 在所有测试后执行() {
System.out.println("执行所有测试之后的清理工作");
}
// 测试方法:暂时禁用的测试用例
@Test
@Disabled("这个测试用例暂时被禁用")
void 被禁用的测试() {
// 此测试用例将不会被执行
System.out.println("这行文字不会被打印");
}
}
// 加法和减法运算类
class Calculator {
static int add(int a, int b) {
return a + b;
}
static int subtract(int a, int b) {
return a - b;
}
}
没有改变。 JUnit 里面的代码执行顺序遵循怎样的规则?
测试用例执行顺序
@Order 注解
@Order 注解是 JUnit 5 提供的用于指定测试方法执行顺序的注解。当测试类使用 @TestMethodOrder(OrderAnnotation.class) 注解时,JUnit 5 将按照 @Order 注解指定的顺序执行测试方法。
重点知识点:
-
用法:
- @Order 注解可以用在测试方法上,用于指定测试方法的执行顺序。
- 通过为每个测试方法添加 @Order 注解并指定顺序值,可以控制测试方法的执行顺序。
-
顺序值:
- 顺序值越小的测试方法越优先执行。顺序值可以是任意整数,但必须是唯一的。
- 如果多个测试方法具有相同的顺序值,则它们的执行顺序可能不确定。
-
注意事项:
- 如果测试类没有使用 @TestMethodOrder 注解指定测试方法的执行顺序,则 @Order 注解将不起作用。
- 若要确保@Order 注解生效,需要在测试类上添加 @TestMethodOrder(OrderAnnotation.class) 注解,指定使用 OrderAnnotation 来排序测试方法。
- 没有指定执行顺序的测试方法将按照默认的规则执行,可能不是按照代码顺序执行。
在 JUnit 中,测试方法的执行顺序不是由它们在代码中的顺序决定的,而是由 JUnit 框架自身的规则来确定的。JUnit 会根据一定的规则对测试方法进行排序和执行。
@TestMethodOrder 注解
- 用于指定测试方法的执行顺序策略。
- 必须与 MethodOrderer 接口的实现类一起使用,常用的实现类有 OrderAnnotation 和 DisplayName。
测试方法执行顺序
- JUnit 5 默认不保证测试方法的执行顺序,因此应避免依赖于测试方法的执行顺序。
- 默认情况下,JUnit 5 按照以下顺序执行测试方法:
- 执行所有被 @BeforeAll 注解修饰的方法,这些方法会在所有测试方法执行之前执行,且仅执行一次。
- 对于每个测试类:
- 执行所有被 @BeforeEach 注解修饰的方法,这些方法会在每个测试方法执行之前执行。
- 执行所有标记了 @Test 注解的测试方法。
- 执行所有被 @AfterEach 注解修饰的方法,这些方法会在每个测试方法执行之后执行。
- 执行所有被 @AfterAll 注解修饰的方法,这些方法会在所有测试方法执行之后执行,且仅执行一次。
注意事项
- 如果需要确保 @Order 注解生效,需要在测试类上添加 @TestMethodOrder 注解并指定 OrderAnnotation.class。
- 使用 @Order 注解时,若测试类没有使用 @TestMethodOrder 注解指定执行顺序,则 @Order 注解将不起作用。
- 在编写测试用例时,应避免依赖于测试方法的执行顺序,因为默认情况下执行顺序可能不确定。
// 测试用例:减法运算
@Test
@DisplayName("测试2:减法运算")
void 减法测试() {
int result = Calculator.subtract(10, 4);
assertEquals(6, result);
System.out.println("+++++++++++++++++这是测试2+++++++++++++++++");
}
// 测试用例:加法运算
@Test
@DisplayName("测试1:加法运算")
void 加法测试() {
int result = Calculator.add(3, 5);
assertEquals(8, result);
System.out.println("+++++++++++++++++这是测试1+++++++++++++++++");
}
去掉@Order 注解之后,按照代码顺序运行测试用例:
参数化
当涉及到参数化测试时,JUnit 提供了丰富的功能来允许在相同测试方法中使用不同的输入参数执行多次测试。
参数化测试概述
参数化测试是一种软件测试方法,允许开发人员通过在测试方法中使用不同的输入参数多次运行相同的测试逻辑,以验证在不同输入条件下程序的行为是否符合预期。参数化测试的主要目的是增强测试用例的复用性和可维护性,同时提高测试覆盖率,确保程序在各种可能的输入情况下都能正确运行。
优点
-
提高测试覆盖率: 参数化测试允许测试相同的逻辑在多组输入条件下执行,从而更全面地覆盖各种情况,包括边界情况和特殊情况。
-
简化测试代码: 通过参数化测试,可以将相似的测试用例合并为一个测试方法,并通过不同的参数值来执行不同的测试路径,从而减少了重复的代码编写。
-
便于维护: 使用参数化测试可以使测试代码更加模块化和清晰,便于后续的维护和修改。当需要修改测试逻辑时,只需修改一个地方即可影响所有相关的测试用例。
-
提高测试效率: 参数化测试可以大大减少编写和维护测试用例的工作量,从而提高测试的效率和生产力。
参数源
参数源是指提供参数值的来源,用于参数化测试中。它可以是不同类型的数据集合或数据源,用于为测试方法提供参数值。
常见的参数源包括:
- @ValueSource: 提供单一类型的参数值集合,如基本数据类型、字符串等。
- @EnumSource: 提供枚举类型的参数值集合。
- @MethodSource: 提供一个方法来生成参数值集合。
- @CsvSource: 提供通过逗号分隔的参数值列表。
- @CsvFileSource: 提供一个 CSV 文件作为参数值来源。
- @ArgumentsSource: 提供自定义的参数值集合来源,可以通过实现 ArgumentsProvider 接口来自定义参数源。
实现方式
在JUnit中,实现参数化测试通常需要以下步骤:
-
使用 @ParameterizedTest 注解标记测试方法: 在需要进行参数化测试的方法上添加 @ParameterizedTest 注解,以指示该方法是一个参数化测试方法。
-
选择合适的参数源: 根据测试方法的需要,选择合适的参数源来提供参数值。常见的参数源包括 @ValueSource、@CsvSource、@EnumSource 等,它们分别用于提供基本数据类型、CSV 格式数据、枚举类型数据等。
-
为测试方法添加参数: 根据测试方法的参数列表,确保参数化测试方法接受正确数量和类型的参数。参数的顺序应与参数源提供的参数顺序相匹配。
-
执行测试方法: 运行测试类时,JUnit 将自动识别带有 @ParameterizedTest 注解的方法,并根据参数源提供的参数值多次运行测试方法。每次运行测试方法时,都会使用不同的参数值。
注意事项
-
参数顺序: 确保参数的顺序与测试方法的参数顺序一致,以便正确地将参数值传递给测试方法。
-
参数类型: 参数化测试支持多种数据类型的参数,包括基本数据类型、字符串、枚举等,根据需要选择合适的参数源。
-
参数化测试命名: 参数化测试方法应该具有描述性的命名,清晰地表达该测试方法的目的和所用参数的含义。
-
避免过度参数化: 避免使用过多的参数化测试,以免测试用例变得过于复杂和难以维护。
参数化测试是编写高效、健壮的测试用例的重要手段之一,通过合理地使用参数化测试,可以提高测试质量和效率,帮助发现和解决程序中的问题。
单参数化测试
单参数化测试是指在测试方法中只接受一个参数,并使用不同的参数值多次运行该测试方法。在JUnit中,通常使用 @ParameterizedTest 注解来标记单参数化测试方法。
使用单参数化测试可以有效地验证在不同输入条件下的方法行为是否正确。通过多次运行相同的测试方法,每次使用不同的参数值,可以覆盖更多的测试场景,提高测试覆盖率和代码质量。
//源代码
@Retention(RetentionPolicy.RUNTIME)
@Documented
@API(
status = Status.STABLE,
since = "5.7"
)
@ArgumentsSource(ValueArgumentsProvider.class)
public @interface ValueSource {
short[] shorts() default {};
byte[] bytes() default {};
int[] ints() default {};
long[] longs() default {};
float[] floats() default {};
double[] doubles() default {};
char[] chars() default {};
boolean[] booleans() default {};
String[] strings() default {};
Class<?>[] classes() default {};
}
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;
public class SingleParameterizedTest {
@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
void testIsPositive(int number) {
assertTrue(number > 0);
}
}
在上面的示例中,我们使用 @ParameterizedTest 注解标记了一个单参数化测试方法 testIsPositive,并使用 @ValueSource 注解提供了一个整数数组作为参数源。JUnit 将使用数组中的每个整数值依次运行 testIsPositive 方法,确保每个值都能通过断言验证为正数。
优点:
-
代码简洁清晰: 单参数化测试使得测试代码更加简洁和易读,因为只需要一个参数即可覆盖多种情况。
-
易于理解: 单参数化测试使得测试用例的意图更加清晰,因为每个测试方法只涉及一个参数,测试的目的和预期结果更容易理解。
-
便于维护: 单参数化测试使得测试代码更易于维护,因为每个测试方法都专注于一个参数的测试,修改或调试时更加方便。
-
适用性广泛: 单参数化测试适用于多种情况,可以覆盖不同的输入范围和边界条件,从而提高测试覆盖率。
缺点:
-
覆盖范围有限: 单参数化测试只能覆盖一个参数的不同取值情况,对于多个参数之间的组合可能无法完全覆盖,导致测试覆盖不全。
-
可扩展性受限: 当需要测试多个参数之间的交叉影响时,单参数化测试的扩展性受到限制,可能需要编写大量重复的测试方法。
-
重复代码较多: 单参数化测试可能会导致测试代码中存在大量重复的逻辑,例如重复的测试方法命名、参数定义等,增加了代码的维护成本。
-
测试粒度较粗: 单参数化测试往往以整体的方式验证方法的行为,无法针对方法内部的细节进行精细化的验证,可能会导致问题难以定位。
综上所述,单参数化测试适用于简单场景下的测试,可以提高测试代码的可读性和维护性,但在复杂场景下可能会存在覆盖范围有限、可扩展性受限等问题,需要根据具体情况进行权衡和选择。
多参数化测试
多参数化测试是指测试方法接受多个参数,并使用不同的参数组合多次运行该测试方法。在JUnit中,可以使用不同的方式来实现多参数化测试,其中包括自定义的参数源和方法生成参数。
-
自定义参数源:
- 自定义参数源允许测试方法接受多个参数,并使用不同的参数组合来运行测试方法。
- 开发人员可以根据需要创建自定义的参数源,以提供测试方法所需的参数。
- 自定义参数源需要实现 ArgumentsProvider 接口,并重写其 provideArguments 方法来提供测试参数。
-
方法生成参数:
- 方法生成参数是通过测试类中的方法来生成参数值,然后将这些参数传递给测试方法进行测试。
- 开发人员可以编写方法来生成参数,并使用这些参数来运行测试方法。
- 在方法生成参数的情况下,需要使用 @MethodSource 注解来指定提供参数的方法。
其实除了上述两种方法,还有其他的。
当涉及到多参数化测试时,JUnit还提供了几种其他的注解和方式来实现:
@CsvSource: 从CSV格式的数据源中提取参数。适合简单的多参数化测试情况,参数以逗号分隔。
@ParameterizedTest
@CsvSource({"apple, 1", "banana, 2", "orange, 3"})
void testFruit(String fruit, int quantity) {
// Test logic using fruit and quantity
}
@CsvSource用于从CSV格式的数据源中提取参数,适合于简单的多参数化测试情况。而自定义参数源和方法生成参数是更灵活的方式,允许开发人员根据具体需求生成各种类型的参数,不局限于CSV格式的数据。
@CsvFileSource: 从CSV文件中读取数据源。与@CsvSource类似,但参数存储在外部文件中,可提高数据管理和维护性。数据源最好放在 下面。
为了保证文件没有出错,建议安装插件:
// 多参数,读取文件中的数据,进行操作
@ParameterizedTest
@CsvFileSource(resources = "test01.csv")
void Test05(String name, int age) {
System.out.println("name:" + name + ", age:" + age);
}
@EnumSource: 从枚举类型中提取参数。适用于测试针对枚举类型的方法。
enum Fruit { APPLE, BANANA, ORANGE }
@ParameterizedTest
@EnumSource(Fruit.class)
void testWithEnumSource(Fruit fruit) {
// Test logic using fruit
}
自定义参数源示例:
假设我们有一个需求是测试一个计算器类中的加法方法,该方法接受两个整数参数,并返回它们的和。我们可以使用自定义参数源来提供不同的参数组合。
import org.junit.jupiter.api.extension.ExtensionContext; // 导入扩展上下文类
import org.junit.jupiter.params.ParameterizedTest; // 导入参数化测试的注解
import org.junit.jupiter.params.provider.Arguments; // 导入参数类
import org.junit.jupiter.params.provider.ArgumentsProvider; // 导入参数提供者接口
import org.junit.jupiter.params.provider.ArgumentsSource; // 导入参数来源注解
import java.util.stream.Stream; // 导入流类
public class CalculatorTest { // 定义测试类 CalculatorTest
@ParameterizedTest // 标记为参数化测试方法
@ArgumentsSource(CustomArgumentsProvider.class) // 指定参数来源为 CustomArgumentsProvider 类
void testAddition(int a, int b, int expected) { // 测试方法,接受三个整数参数
Calculator calculator = new Calculator(); // 创建 Calculator 实例
int result = calculator.add(a, b); // 调用 Calculator 类的 add 方法进行加法计算
assertEquals(expected, result); // 使用断言判断实际结果与预期结果是否相等
}
static class CustomArgumentsProvider implements ArgumentsProvider { // 自定义参数提供者类
@Override
public Stream<? extends Arguments> provideArguments(ExtensionContext context) { // 重写 provideArguments 方法
return Stream.of( // 返回包含多组测试参数的流
Arguments.of(2, 3, 5), // 测试参数1:输入为2和3,预期结果为5
Arguments.of(-1, 1, 0), // 测试参数2:输入为-1和1,预期结果为0
Arguments.of(0, 0, 0) // 测试参数3:输入为0和0,预期结果为0
);
}
}
}
在上面的示例中,我们首先定义了一个 CalculatorTest 测试类,其中的 testAddition 方法使用了 @ParameterizedTest 注解标记。参数源使用了自定义的 CustomArgumentsProvider 类,该类实现了 ArgumentsProvider 接口,并重写了 provideArguments 方法来提供测试参数。在这个方法中,我们返回了多组参数,每组参数包含两个整数和它们的期望和。
方法生成参数示例:
下面我们通过两个简单的示例来说明方法生成参数的用法。
import org.junit.jupiter.api.*;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.*;
import java.util.stream.Stream;
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class JunitTest {
public static Stream<Arguments> Generate() {
return Stream.of(
Arguments.arguments("张三", 13),
Arguments.arguments("李四",14)
);
}
// 通过方法生成测试数据
@ParameterizedTest
@MethodSource("Generate")
void Test06(String name, int age) {
System.out.println("姓名:" + name + "年龄:" + age);
}
假设我们需要测试一个简单的字符串操作工具类,其中一个方法是将字符串转换为大写。我们可以使用方法生成参数来提供不同的测试输入。
import org.junit.jupiter.params.ParameterizedTest; // 导入参数化测试的注解
import org.junit.jupiter.params.provider.MethodSource; // 导入参数来源为方法的注解
import static org.junit.jupiter.api.Assertions.assertEquals; // 导入断言类中的assertEquals方法
import java.util.stream.Stream; // 导入流类
public class StringUtilsTest { // 定义测试类 StringUtilsTest
@ParameterizedTest // 标记为参数化测试方法
@MethodSource("stringProvider") // 指定参数来源为 stringProvider 方法
void testToUpperCase(String input, String expected) { // 测试方法,接受两个参数
StringUtils stringUtils = new StringUtils(); // 创建 StringUtils 实例
String result = stringUtils.toUpperCase(input); // 调用 toUpperCase 方法,将输入字符串转换为大写形式
assertEquals(expected, result); // 使用断言判断实际结果与预期结果是否相等
}
static Stream<String> stringProvider() { // 定义静态方法,提供测试参数流
return Stream.of( // 返回包含多组测试参数的流
Arguments.of("hello", "HELLO"), // 测试参数1:输入为"hello",预期结果为"HELLO"
Arguments.of("world", "WORLD"), // 测试参数2:输入为"world",预期结果为"WORLD"
Arguments.of("", "") // 测试参数3:输入为空字符串,预期结果也为空字符串
);
}
}
在上面的示例中,我们首先定义了一个 StringUtilsTest 测试类,其中的 testToUpperCase 方法使用了 @ParameterizedTest 注解标记。方法生成参数使用了 @MethodSource("stringProvider") 注解,指定了提供参数的方法名为 stringProvider。在 stringProvider 方法中,我们返回了一个包含不同测试输入的流,每个输入都是一个字符串,以及它们的期望输出。这样,在测试方法中就可以使用不同的参数来运行相同的测试逻辑,以验证方法的正确性。
注意事项
@Test
@ParameterizedTest
@CsvSource({"'张三', 22"})
void Test04(String name, int age) {
System.out.println(name + "今年" + age + "岁");
}
多参数化测试的优点:
-
更全面的覆盖性: 多参数化测试允许在多个参数组合下运行相同的测试方法,从而可以更全面地覆盖不同的测试场景和边界条件。
-
有效的错误检测: 通过测试多个参数的组合,可以更容易地检测到方法在不同参数组合下的行为,帮助发现潜在的错误或异常情况。
-
提高测试代码的可维护性: 多参数化测试将测试数据与测试逻辑分离,使得测试代码更具可读性和可维护性。测试数据的修改不会影响测试逻辑,使得测试代码更易于维护和更新。
-
减少重复工作: 通过使用多参数化测试,可以避免编写大量重复的测试用例,只需编写一组测试逻辑,然后通过不同的参数组合运行测试,从而减少了重复工作。
多参数化测试的缺点:
-
测试用例设计复杂: 编写多参数化测试需要设计多个参数的组合,可能会增加测试用例设计的复杂度和难度,特别是在处理多个参数之间的交叉影响时。
-
测试结果解读困难: 当测试方法接受多个参数时,生成的测试结果可能会变得更加复杂,对测试结果的解读和分析可能会更加困难,需要花费更多的时间和精力。
-
执行时间增加: 多参数化测试可能会增加测试的执行时间,特别是在测试大量参数组合时,测试执行的时间可能会大大增加,导致测试效率降低。
-
维护成本提高: 随着参数组合的增加,多参数化测试的维护成本也会相应增加,需要花费更多的时间和精力来维护和更新测试代码,特别是在参数变更时需要更新大量的测试用例。
测试套件
Unit 中的 @Suite 注解用于组织多个测试类,将它们作为一个测试套件来运行。通过 @Suite 注解,可以将多个测试类汇总在一起,按照指定的顺序依次执行。
-
组织多个测试类:
- 使用 @Suite 注解可以将多个测试类组织在一起,形成一个测试套件。
- 通过 @Suite.SuiteClasses 注解参数来指定包含的测试类。
-
运行顺序:
- 默认情况下,@Suite 中指定的测试类将按照它们在数组中的顺序依次执行。
- 可以使用 @FixMethodOrder(MethodSorters.NAME_ASCENDING) 注解来指定测试类的执行顺序。
-
示例:
import org.junit.runner.RunWith; import org.junit.runners.Suite; @RunWith(Suite.class) @Suite.SuiteClasses({TestClass1.class, TestClass2.class, TestClass3.class}) public class TestSuite { // 这个类不包含任何测试,只用来组织其他测试类 }
上面的示例中,TestSuite 类用 @Suite 注解组织了 TestClass1、TestClass2 和 TestClass3 这三个测试类。当运行 TestSuite 类时,JUnit 将依次执行这三个测试类中的测试方法。
-
自定义套件:
- 除了直接在代码中使用 @Suite 注解组织测试类外,还可以编写自定义的测试运行器来创建更灵活的测试套件。
- 自定义测试运行器可以根据特定的条件动态地选择要执行的测试类。
通过使用 @Suite 注解,我们可以更好地组织和管理大型测试代码库,使测试执行更加清晰、有序。
当使用JUnit的测试套件(Suite)时,有两种常见的方式来组织和运行测试用例:通过类(Class)和通过包(Package)。
1. 通过类运行测试用例:
-
优点:
- 可以选择性地将特定的测试类包含在测试套件中,而不必包含整个包中的所有测试类。
- 提供了更精确的控制,可以针对性地运行特定的测试类。
- 适用于当需要对特定的功能或模块进行测试时。
-
步骤:
- 在测试套件类中使用 @Suite.SuiteClasses 注解,指定需要包含的测试类。
- 在注解中列出需要包含的测试类。
-
示例:
import org.junit.runner.RunWith; import org.junit.runners.Suite; @RunWith(Suite.class) @Suite.SuiteClasses({TestClass1.class, TestClass2.class}) public class TestSuiteByClass { // 测试套件中不需要编写其他测试方法,只需要指定包含的测试类即可 }
2. 通过包运行测试用例:
-
优点:
- 可以自动扫描指定包下的所有测试类,并将它们包含在测试套件中,减少了手动维护测试类列表的工作量。
- 更适用于对整个应用程序或模块进行全面测试时。
-
步骤:
在测试套件类中编写方法,通过编程方式动态扫描指定包下的所有测试类,并将它们添加到测试套件中。 -
示例:
import org.junit.runner.RunWith; import org.junit.runners.Suite; import org.junit.runners.Suite.SuiteClasses; @RunWith(Suite.class) @SuitePackages(value = {"example"}) public class TestSuiteByPackage { // 在测试套件类中编写方法,动态扫描指定包下的所有测试类,并将它们添加到测试套件中 // 添加测试类的逻辑可以通过编程方式实现 }
注意:在JUnit 4中,如果你的测试类名以“Test”结尾,而且位于与被测类相同的包中,那么它们将被自动识别并执行,不需要显式地使用@RunWith(Suite.class)注解。JUnit会自动检测并执行这些测试类。
JUnit的断言
JUnit 提供了丰富的断言方法,用于在单元测试中验证预期结果与实际结果是否相符。以下是JUnit的断言概述:
1. 常用断言方法:
- assertEquals(expected, actual): 验证两个值是否相等。
- assertNotEquals(unexpected, actual): 验证两个值是否不相等。
- assertTrue(condition): 验证条件是否为 true。
- assertFalse(condition): 验证条件是否为 false。
- assertNull(object): 验证对象是否为 null。
- assertNotNull(object): 验证对象是否不为 null。
- assertSame(expected, actual): 验证两个对象是否引用同一个对象。
- assertNotSame(unexpected, actual): 验证两个对象是否引用不同的对象。
- assertArrayEquals(expectedArray, actualArray): 验证两个数组是否相等。
2. 自定义断言方法:
除了上述常用的断言方法之外,JUnit还允许开发人员创建自定义的断言方法,以满足特定的测试需求。
3. 重要概念:
- 预期值(Expected): 在测试中期望得到的结果。
- 实际值(Actual): 测试执行后得到的实际结果。
- 断言失败(Assertion Failure): 当预期值与实际值不相符时,断言失败,测试也会失败。
- 断言错误(Assertion Error): 当测试代码本身存在问题时,会抛出断言错误,这并不代表测试失败。
4. 作用:
- 验证测试结果: 确保测试代码按照预期执行,并且得到正确的结果。
- 提供反馈: 当测试失败时,断言会提供详细的错误信息,帮助开发人员定位问题。
- 提高测试覆盖率: 通过断言可以验证各种条件和情况,从而提高测试用例的覆盖率。
5. 注意事项:
- 使用合适的断言: 根据需要选择合适的断言方法,确保测试代码的准确性和可读性。
- 避免过多断言: 每个测试方法应该只包含一个断言,以确保清晰明了的测试结果。
- 自定义错误消息: 在断言中可以添加自定义的错误消息,提供更清晰的失败信息。
- 谨慎使用 assertNull 和 assertNotNull: 在某些情况下,使用 assertNull 和 assertNotNull 可能会导致测试变得脆弱,因为它们不验证预期的值。
JUnit的断言是编写单元测试时的关键组成部分,正确使用断言可以有效地验证代码的正确性,并帮助我们快速发现和修复问题。
博客系统完善——自动化测试代码
还记得我们之前用Java写好的博客系统吗?我们现在可以对它进行进一步完善了。
确定环境
环境搭建成功!
确定测试用例
写自动化测试代码
package BlogAutoTests;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import javax.annotation.CheckReturnValue;
public class InitAndEndBrowser {
// 设置 Chrome 驱动器路径
static {
// 设置 Chrome 驱动器路径
System.setProperty("webdriver.chrome.driver", "C:\\Program Files\\Google\\Chrome\\Application\\chromedriver.exe");
}
static WebDriver webDriver;
@BeforeAll
static void OpenBlogSystem() {
webDriver = new ChromeDriver();
}
@AfterAll
static void CloseBlogSystem() {
webDriver.quit();
}
}
登录自动化测试用例
package BlogAutoTests;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;
import org.junit.jupiter.params.provider.CsvSource;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import static java.lang.Thread.sleep;
public class Tests extends InitAndEndBrowser{
// 登录测试用例
@ParameterizedTest
@CsvSource({"'http://101.43.192.49:8080/java_blog_system/login.html', '神圣的管理员SAMA', 'http://101.43.192.49:8080/java_blog_system/blog_list.html', '000'"})
void Login(String url, String username, String expected_jump_url, String password) throws InterruptedException {
webDriver.get(url);
sleep(3000);
// 输入用户名
webDriver.findElement(By.cssSelector("#username")).sendKeys(username);
// 输入密码
webDriver.findElement(By.cssSelector("#password")).sendKeys(password);
// 点击提交
webDriver.findElement(By.cssSelector("#submit")).click();
sleep(3000);
// 登录成功后,校验是否登陆成功
String uname = webDriver.findElement(By.cssSelector("body > div.container > div.container-left > div > h3")).getText();
if(username.equals("神圣的管理员SAMA")) {
System.out.println("测试通过");
} else {
System.out.println("测试不通过");
}
String cur_url = webDriver.getCurrentUrl();
if(cur_url.equals(expected_jump_url)) {
System.out.println("测试通过");
} else {
System.out.println("测试不通过");
}
}
@Test
void LoginError() throws InterruptedException {
String username = "神圣的管理员SAMA";
String password = "010101";
webDriver.get("http://101.43.192.49:8080/java_blog_system/blog_list.html");
sleep(3000);
webDriver.findElement(By.cssSelector("#username")).sendKeys(username);
webDriver.findElement(By.cssSelector("#password")).sendKeys(password);
webDriver.findElement(By.cssSelector("#submit")).click();
sleep(3000);
WebElement webElement = webDriver.findElement(By.cssSelector("body"));
Assertions.assertEquals( "您输入的用户名或密码不正确!",webElement.getText());
}
}
Assertions.assertEquals("您输入的用户名或密码不正确!",webElement.getText())这行代码,使用 JUnit Jupiter 的断言方法 assertEquals 来比较实际获取的页面文本内容(通过 webElement.getText() 获取)是否与期望的文本内容 "您输入的用户名或密码不正确!" 相同。如果两者相同,则测试通过;否则,测试失败。
博客发布测试用例
@Order(2)
@Test
void PublishBlog() throws InterruptedException {
// 打开博客列表页
webDriver.get("http://101.43.192.49:8080/java_blog_system/blog_list.html");
// 点击写博客按钮
webDriver.findElement(By.cssSelector("body > div.nav > a:nth-child(5)")).click();
sleep(6000);
//document.querySelector("#title-input")
//document.querySelector("#title-input").value="自动测试化用例"
// 输入标题(通过Selenium执行JS脚本完成)
((JavascriptExecutor)webDriver).executeScript("document.querySelector(\"#title-input\").value = \"自动测试化用例3\"");
sleep(2000);
// 点击发布博客按钮
webDriver.findElement(By.cssSelector("#submit")).click();
sleep(3000);
// 校验
// 1、找到发布成功的博客标题
WebElement title = webDriver.findElement(By.cssSelector("body > div.container > div.container-right > h3"));
sleep(2000);
// 2、校验标题对应的元素是否为空,如果不为空,测试通过,如果为空,测试不通过
Assertions.assertNotNull(title);
}
注意,这里应该从登录测试用例开始运行,如果只运行写博客测试用例,进入网页后会直接退出到登录页面,导致测试失败。
删除博客测试用例
@Order(3)
@Test
void DeleteBlog() throws InterruptedException {
webDriver.get("http://101.43.192.49:8080/java_blog_system/blog_list.html");
sleep(3000);
webDriver.findElement(By.cssSelector("body > div.container > div.container-right > div:nth-child(1) > a")).click();
sleep(2000);
//选中删除按钮
webDriver.findElement(By.cssSelector("body > div.nav > a:nth-child(7)")).click();
sleep(2000);
String url = webDriver.getCurrentUrl();
Assertions.assertEquals("http://101.43.192.49:8080/java_blog_system/blog_list.html", url);
}
退出博客页面测试用例
// @Order(4)
@Test
void LogOut() throws InterruptedException {
webDriver.get("http://101.43.192.49:8080/java_blog_system/blog_list.html");
webDriver.findElement(By.cssSelector("body > div.nav > a:nth-child(6)")).click();
sleep(6000);
//校验,页面是否跳转?
String url = webDriver.getCurrentUrl(); ;
sleep(3000);
String username = webDriver.findElement(By.cssSelector("body > div.login-container > div > form > div:nth-child(1) > span")).getText();
Assertions.assertEquals("http://101.43.192.49:8080/java_blog_system/login.html", url);
Assertions.assertEquals("用户名", username);
}
报告
略