测试
junit
junit4 和 junit5
JUnit4和JUnit5是Java编程语言中广泛使用的单元测试框架的两个主要版本。它们各自具有独特的功能和特性,以下是对这两个版本的详细比较:
一、架构与组成
- JUnit4
- JUnit4是一个完整的单元测试框架,它提供了用于编写和运行测试的注解、断言和测试运行器。
- 它将所有的功能打包在一个单一的jar包中,便于集成和使用。
- JUnit5
- JUnit5采用了模块化架构,由三个子项目组成:JUnit Platform、JUnit Jupiter和JUnit Vintage。
- JUnit Platform提供了在JVM上启动测试框架的基础服务,支持命令行、IDE和构建工具等执行测试。
- JUnit Jupiter包含了新的编程模型和扩展模型,用于编写测试代码和扩展代码。
- JUnit Vintage用于支持在JUnit5平台上运行JUnit3和JUnit4编写的测试用例。
二、注解与扩展
- 注解
- JUnit4:使用
@Test注解来标记测试方法,@Ignore注解来忽略测试,@Before和@After注解来设置每个测试方法前后的前置和后置条件,@BeforeClass和@AfterClass注解来设置测试类前后的全局前置和后置条件。 - JUnit5:引入了新的注解,如
@ExtendWith用于启用扩展,@DisplayName用于设置测试方法在测试报告中的显示名称,@Nested用于定义内嵌测试类,@Tag用于为测试方法添加标签以便分类执行。此外,JUnit5的注解更加语义化,如@BeforeEach和@AfterEach分别替代了JUnit4中的@Before和@After,更清晰地表达了它们的作用范围。
- JUnit4:使用
- 扩展
- JUnit4:使用
@RunWith注解来指定运行器,如SpringRunner来集成Spring的功能。运行器是JUnit4中用于扩展测试框架的机制。 - JUnit5:不再使用
@RunWith注解,而是使用@ExtendWith注解来启用扩展。扩展是JUnit5中用于扩展测试框架的新机制,它提供了更灵活和强大的方式来定制测试的执行过程。
- JUnit4:使用
三、断言与异常测试
- 断言
- JUnit4:使用
org.junit.Assert类中的断言方法来验证测试结果。这些断言方法包括assertEquals、assertTrue、assertFalse等。 - JUnit5:引入了新的断言API,
org.junit.jupiter.Assertions类提供了更多断言方法,如assertAll、assertThrows、assertTimeout等。这些新的断言方法使得断言更加清晰和强大,同时提供了更好的错误消息和异常处理。
- JUnit4:使用
- 异常测试
- JUnit4:可以使用
@Test(expected = XxxException.class)注解来测试异常,或者使用try-catch块来捕获异常并进行断言。 - JUnit5:提供了
assertThrows方法来测试异常,该方法允许你指定期望的异常类型和异常消息,并返回捕获到的异常对象以便进行进一步的断言。
- JUnit4:可以使用
四、测试执行与报告
- 测试执行
- JUnit4:测试方法必须是
public void类型,测试类和测试方法没有严格的访问修饰符要求(尽管通常建议将它们设为public)。 - JUnit5:测试方法和测试类可以是包作用域(没有
public修饰符),甚至可以是protected或private的。JUnit5使用反射来查找测试类和测试方法,因此不再强制要求它们必须是public的。
- JUnit4:测试方法必须是
- 测试报告
- JUnit4:生成的测试报告通常包含测试方法的名称、执行状态和持续时间等信息。
- JUnit5:提供了更丰富的测试报告功能,包括测试方法的显示名称(通过
@DisplayName注解设置)、标签(通过@Tag注解设置)和嵌套测试结构(通过@Nested注解定义)。这些功能使得测试报告更加易于理解和导航。
五、兼容性与迁移
- 兼容性
- JUnit4:适用于Java 5及以上版本。
- JUnit5:需要Java 8及以上版本。同时,JUnit5提供了JUnit Vintage模块来兼容JUnit3和JUnit4编写的测试用例。
- 迁移
- 对于现有的JUnit4测试用例,可以通过逐步迁移到JUnit5来利用新的功能和特性。迁移过程可能涉及更新注解、断言和测试运行器等方面的修改。
- JUnit团队提供了详细的迁移指南和工具来帮助开发者进行迁移。
综上所述,JUnit4和JUnit5在架构、注解与扩展、断言与异常测试、测试执行与报告以及兼容性与迁移等方面存在显著差异。开发者在选择使用哪个版本时,应根据项目的具体需求和目标来决定。
junit5
详细用法请参考示例
https://gitee.com/dexterleslie/demonstration/tree/master/demo-java/demo-library/demo-testing/demo-junit5
maven 配置
<!-- junit5 依赖 -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.3</version>
<scope>test</scope>
</dependency>public class DemoUtil {
public int add(int a, int b) {
return a + b;
}
}// 入门示例
public class GettingStartedTests {
@Test
public void test() {
DemoUtil demoUtil = new DemoUtil();
// 断言
Assertions.assertEquals(10, demoUtil.add(5, 5), "5 + 5 = 10");
Assertions.assertNotEquals(9, demoUtil.add(2, 8), "2 + 8 != 9");
}
}生命周期
详细用法请参考示例的 LifecycleTests 测试用例。
JUnit 5 提供了一套强大的测试生命周期回调机制,使开发者可以在测试执行的不同阶段执行特定逻辑。这些回调通过注解实现,能够简化资源初始化、清理等任务,从而提高测试的效率和可维护性。以下是对JUnit 5 测试生命周期的详细解释:
一、测试生命周期回调的定义
测试生命周期回调定义了在测试执行过程中,不同阶段触发的方法。这些回调方法包括类级别的回调和方法级别的回调。
- 类级别的回调:在测试类的所有方法运行之前和之后执行。
- 方法级别的回调:在每个测试方法的执行前后执行。
二、常用的回调注解
JUnit 5 提供了多个注解来控制测试生命周期的不同阶段,以下是一些常用的注解:
- @BeforeAll:用于在整个测试类的生命周期中执行一次初始化工作。它必须定义在 static 方法中。
- @AfterAll:用于在整个测试类的生命周期中执行一次清理工作。它也必须定义在 static 方法中。
- @BeforeEach:在每个测试方法运行之前执行,用于初始化与单个测试相关的资源。
- @AfterEach:在每个测试方法运行之后执行,用于清理与单个测试相关的资源。
三、生命周期注解的应用场景
- @BeforeAll:在测试类的所有测试方法之前执行,通常用于初始化共享资源,如数据库连接池。
- @AfterAll:在测试类的所有测试方法之后执行,通常用于释放共享资源,防止资源泄露。
- @BeforeEach:在每个测试方法之前执行,用于为每个测试方法设置干净的测试状态。
- @AfterEach:在每个测试方法之后执行,用于重置状态,避免测试间相互影响。
四、动态测试中的生命周期回调
在动态测试中,JUnit 5 的生命周期回调机制有所不同。具体来说:
- @BeforeEach 和 @AfterEach 生命周期方法会针对每个 @TestFactory 方法执行,但不会针对每个动态测试执行。
- 对于静态测试用例(即使用 @ParameterizedTest 和 @MethodSource 注解生成的测试用例),每个参数用例都会执行一次 BeforeEach 和 AfterEach。
- 对于动态测试用例(即使用 DynamicTest 生成的测试用例),默认情况下,BeforeEach 和 AfterEach 的生命周期会被提升为 BeforeAll 和 AfterAll。这意味着在整个动态测试集合中,BeforeEach 和 AfterEach 方法只会执行一次。
五、示例代码
以下是一个简单的示例代码,展示了如何在JUnit 5中使用这些生命周期注解:
import org.junit.jupiter.api.*;
public class LifecycleExample {
@BeforeAll
static void beforeAll() {
System.out.println("BeforeAll - 执行所有测试前的初始化工作");
}
@BeforeEach
void beforeEach() {
System.out.println("BeforeEach - 每个测试前的初始化工作");
}
@Test
void test1() {
System.out.println("Executing Test 1");
}
@Test
void test2() {
System.out.println("Executing Test 2");
}
@AfterEach
void afterEach() {
System.out.println("AfterEach - 每个测试后的清理工作");
}
@AfterAll
static void afterAll() {
System.out.println("AfterAll - 执行所有测试后的清理工作");
}
}运行上述代码时,控制台将输出以下结果:
BeforeAll - 执行所有测试前的初始化工作
BeforeEach - 每个测试前的初始化工作
Executing Test 1
AfterEach - 每个测试后的清理工作
BeforeEach - 每个测试前的初始化工作
Executing Test 2
AfterEach - 每个测试后的清理工作
AfterAll - 执行所有测试后的清理工作这展示了JUnit 5测试生命周期回调的执行顺序和机制。
自定义显示名称
详细用法请参考示例的 DisplayNameTests 测试用例。
在JUnit 5中,自定义测试或测试类的显示名称通常通过@DisplayName注解来实现。这个注解允许你为测试方法或测试类指定一个更具描述性和可读性的名称,这个名称将在测试报告、IDE的测试运行器或其他测试工具中显示。
使用@DisplayName注解自定义显示名称
- 为测试方法指定显示名称:
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
public class MyTests {
@Test
@DisplayName("测试用户登录功能")
void testUserLogin() {
// 测试代码
}
}在这个例子中,testUserLogin方法的显示名称被设置为“测试用户登录功能”。
- 为测试类指定显示名称:
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
@DisplayName("用户功能测试")
public class UserTests {
@Test
void testUserCreation() {
// 测试用户创建的代码
}
@Test
void testUserDeletion() {
// 测试用户删除的代码
}
}在这个例子中,整个UserTests类的显示名称被设置为“用户功能测试”。这意味着当这个类的测试被运行时,它们将作为一个组在测试报告或IDE的测试运行器中显示,并且这个组的名称是“用户功能测试”。
使用@DisplayNameGeneration和自定义DisplayNameGenerator
除了直接使用@DisplayName注解外,JUnit 5还提供了@DisplayNameGeneration注解和DisplayNameGenerator接口来生成自定义的显示名称。这通常用于需要为多个测试方法或类生成类似显示名称的场景。
- 使用预定义的
DisplayNameGenerator:
JUnit 5提供了一些预定义的DisplayNameGenerator实现,如ReplaceUnderscores(将下划线替换为空格)和IndicativeSentences(将方法名转换为指示性句子)。你可以通过@DisplayNameGeneration注解来指定使用哪个预定义的生成器。
import org.junit.jupiter.api.DisplayNameGeneration;
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.Test;
@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
public class PredefinedGeneratorTests {
@Test
void test_user_login() {
// 测试代码
}
}在这个例子中,test_user_login方法的显示名称将被自动替换为“test user login”。
- 实现自定义的
DisplayNameGenerator:
如果你需要更复杂的显示名称生成逻辑,你可以实现自己的DisplayNameGenerator。
import org.junit.jupiter.api.DisplayNameGenerator;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Method;
@ExtendWith(CustomDisplayNameGeneratorExtension.class)
public class CustomGeneratorTests {
static class CustomDisplayNameGeneratorExtension implements DisplayNameGenerator {
@Override
public String generateDisplayNameForClass(Class<?> testClass) {
// 自定义类显示名称生成逻辑
return "自定义类: " + testClass.getSimpleName();
}
@Override
public String generateDisplayNameForMethod(Class<?> testClass, Method testMethod) {
// 自定义方法显示名称生成逻辑
return "自定义方法: " + testMethod.getName().replace("_", " ");
}
}
@Test
void test_custom_method() {
// 测试代码
}
}在这个例子中,我们创建了一个自定义的DisplayNameGenerator实现,并通过@ExtendWith注解将其应用于测试类。然后,我们为类和方法提供了自定义的显示名称生成逻辑。
请注意,上面的自定义扩展示例中,CustomDisplayNameGeneratorExtension是一个静态内部类,它实现了DisplayNameGenerator接口。在实际应用中,你可能希望将这个扩展类放在一个单独的源文件中,以便更好地组织代码。此外,@ExtendWith注解用于将自定义扩展应用于测试类。
断言
详细用法请参考示例的 AssertionTests 测试用例。
知识点
- assertSame 和 assertNotSame 判断是否同一个对象
- assertArrayEquals 判断数组是否相等
- assertIterableEquals 判断 ArrayList 是否相等
- assertLinesMatch 判断字符串类型的 ArrayList 是否相等
- assertThrows 和 assertDoesNotThrow 判断是否抛出指定异常
- assertTimeout 判断方法执行是否超时
执行顺序
注意:未遇到此需求,所以暂时不做实验。
代码覆盖率和测试报告
通过代码覆盖率报告能够查看到哪些方法或者代码没有被覆盖测试。通过测试报告能够查看哪些测试用例运行失败。
使用 IntelliJ 生成报告
生成测试报告
在测试结果面板中点击 Export Test Results... 以导出测试报告,设置信息如下:
- Export format 为默认值
- Output File name 为默认值
- Output Folder 为默认值
- 勾选 Open exported file in browser
点击 OK 按钮后浏览器会自动打开测试报告。
生成覆盖率报告
点击Run 'xxx' with Coverage按钮使用 Code Coverage 功能运行所有测试用例。
在 Coverage 测试结果导航中,选中并双击 Method, % 列没有 100% 的类,对应的类文件被打开,查看类文件绿色标注的区域表示测试已经覆盖,红色标注的区域表示测试未覆盖
点击 Coverage 测试结果导航面板中的 Generate Coverage Report... 功能以导出覆盖率报告,设置信息如下:
- Output Directory 为默认值
- 勾选 Open generated HTML in browser
点击 Save 按钮后浏览器会自动打开覆盖率报告,点击 Method, % 列没有 100% 的包或者类,查看类文件绿色标注的区域表示测试已经覆盖,红色标注的区域表示测试未覆盖
mvn 命令行生成报告
生成测试报告
pom 配置如下:
<build>
<plugins>
<!--
默认情况下,执行 ./mvnw clean test 命令时,maven不会自动扫描测试用例并运行,需要手动添加surefire插件
-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
<configuration>
<!-- 测试成功和失败都输出测试报告 -->
<testFailureIgnore>true</testFailureIgnore>
<!-- 处理测试方法中的@DisplayName注解 -->
<statelessTestsetReporter
implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5Xml30StatelessReporter">
<usePhrasedTestCaseMethodName>true</usePhrasedTestCaseMethodName>
</statelessTestsetReporter>
</configuration>
</plugin>
<!-- 生成测试报告配置 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-report-plugin</artifactId>
<version>3.5.2</version>
<executions>
<execution>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>运行测试
./mvnw clean test使用浏览器打开 target/reports/surefire.html 查看测试报告。
生成覆盖率报告
pom 配置如下:
<build>
<plugins>
<!--
默认情况下,执行 ./mvnw clean test 命令时,maven不会自动扫描测试用例并运行,需要手动添加surefire插件
-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.2</version>
<configuration>
<!-- 测试成功和失败都输出测试报告 -->
<testFailureIgnore>true</testFailureIgnore>
<!-- 处理测试方法中的@DisplayName注解 -->
<statelessTestsetReporter
implementation="org.apache.maven.plugin.surefire.extensions.junit5.JUnit5Xml30StatelessReporter">
<usePhrasedTestCaseMethodName>true</usePhrasedTestCaseMethodName>
</statelessTestsetReporter>
</configuration>
</plugin>
<!-- 使用jacoco插件生成测试覆盖率 -->
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.7</version>
<executions>
<execution>
<id>jacoco-prepare</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>jacoco-report</id>
<phase>test</phase>
<goals>
<goal>report</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>运行测试
./mvnw clean test使用浏览器打开 target/site/jacoco/index.html 查看覆盖率报告。
条件测试
未遇到此需求,所以暂时不做实验。
在 JUnit 5 中,条件测试允许你根据特定的条件来启用或禁用测试。JUnit 5 提供了一些注解来实现这一点,比如 @EnabledIf、@DisabledIf、@EnabledOnOs、@DisabledOnOs、@EnabledForJre、@DisabledForJre 等。这些注解使你能够在测试方法或测试类上指定条件,从而更灵活地控制测试的执行。
以下是一些常用的条件测试注解及其用法示例:
- 使用
@EnabledIf和@DisabledIf
@EnabledIf 和 @DisabledIf 注解允许你根据布尔表达式来启用或禁用测试。表达式可以是方法引用,也可以是直接写在注解中的字符串表达式(使用 SpEL,即 Spring Expression Language)。
示例
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIf;
import org.junit.jupiter.api.condition.DisabledIf;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class ConditionalTests {
private static final boolean IS_WINDOWS = System.getProperty("os.name").toLowerCase().contains("win");
@EnabledIf("IS_WINDOWS")
@Test
void testOnlyOnWindows() {
assertTrue(IS_WINDOWS, "This test should only run on Windows");
}
@DisabledIf("${system.property:'user.home'}.contains('temp')")
@Test
void testDisabledIfHomeContainsTemp() {
// This test will be disabled if the user's home directory contains "temp"
}
@EnabledIf(value = "isEnvironmentConfiguredCorrectly()", enabledIfExpression = "true")
@Test
void testIfEnvironmentConfigured() {
// This test will be enabled if the method `isEnvironmentConfiguredCorrectly()` returns true
}
private boolean isEnvironmentConfiguredCorrectly() {
// Your logic to check if the environment is configured correctly
return true;
}
}- 使用
@EnabledOnOs和@DisabledOnOs
这些注解允许你根据操作系统来启用或禁用测试。
示例
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledOnOs;
import org.junit.jupiter.api.condition.OS;
import org.junit.jupiter.api.condition.DisabledOnOs;
@Test
@EnabledOnOs(OS.WINDOWS)
void testOnlyOnWindows() {
// This test will only run on Windows
}
@Test
@DisabledOnOs({OS.WINDOWS, OS.MAC})
void testNotOnWindowsOrMac() {
// This test will not run on Windows or macOS
}- 使用
@EnabledForJre和@DisabledForJre
这些注解允许你根据 Java 运行时环境(JRE)版本来启用或禁用测试。
示例
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledForJre;
import org.junit.jupiter.api.condition.JRE;
import org.junit.jupiter.api.condition.DisabledForJre;
@Test
@EnabledForJre(JRE.JAVA_8)
void testOnlyOnJava8() {
// This test will only run on Java 8
}
@Test
@DisabledForJre(JRE.JAVA_11)
void testNotOnJava11() {
// This test will not run on Java 11
}- 使用
@EnabledIfEnvironmentVariable和@DisabledIfEnvironmentVariable
这些注解允许你根据环境变量的值来启用或禁用测试。
示例
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.EnabledIfEnvironmentVariable;
import org.junit.jupiter.api.condition.DisabledIfEnvironmentVariable;
@Test
@EnabledIfEnvironmentVariable(named = "MY_ENV_VAR", matches = "production")
void testOnlyInProduction() {
// This test will only run if the environment variable MY_ENV_VAR is set to "production"
}
@Test
@DisabledIfEnvironmentVariable(named = "MY_ENV_VAR", matches = "test")
void testNotInTestEnvironment() {
// This test will not run if the environment variable MY_ENV_VAR is set to "test"
}总结
通过使用这些条件测试注解,JUnit 5 提供了强大的工具来根据各种条件启用或禁用测试,从而提高了测试的灵活性和可维护性。你可以根据具体的测试需求选择合适的注解和条件来实现条件测试。
参数化测试
详细用法请参考示例 https://gitee.com/dexterleslie/demonstration/blob/master/demo-java/demo-library/demo-testing/demo-junit5/src/test/java/com/future/demo/ParameterizedTests.java
@ParameterizedTest 参数数据源
在 JUnit Jupiter 中,@ParameterizedTest 注解允许你使用不同的参数集运行同一个测试方法多次。 参数数据源有多种选择,以下列出几种常见的实现方式以及它们的优缺点:
1. @CsvSource: 用于从 CSV 字符串中读取参数。
- 优点: 简洁明了,适合少量参数的情况。
- 缺点: 对于大量参数,CSV 字符串会变得难以维护和阅读。
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.junit.jupiter.api.Assertions.*;
public class MyParameterizedTest {
@ParameterizedTest
@CsvSource({"1, 1", "2, 4", "3, 9"})
void testSquare(int input, int expected) {
assertEquals(expected, input * input);
}
}2. @CsvFileSource: 从 CSV 文件中读取参数。
- 优点: 适合大量参数,易于维护和管理。参数数据可以独立于测试代码进行管理。
- 缺点: 需要额外的 CSV 文件。
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvFileSource;
import static org.junit.jupiter.api.Assertions.*;
public class MyParameterizedTest {
@ParameterizedTest
@CsvFileSource(resources = "/data.csv", numLinesToSkip = 1) // 跳过第一行标题行
void testFromFile(int input, int expected) {
assertEquals(expected, input * input);
}
}data.csv 文件内容示例:
input,expected
1,1
2,4
3,93. @MethodSource: 从另一个方法中获取参数。
- 优点: 灵活,可以根据需要动态生成参数。
- 缺点: 需要编写额外的参数提供方法。
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.stream.Stream;
import static org.junit.jupiter.api.Assertions.*;
public class MyParameterizedTest {
@ParameterizedTest
@MethodSource("provideData")
void testFromMethod(int input, int expected) {
assertEquals(expected, input * input);
}
static Stream<Arguments> provideData() {
return Stream.of(
Arguments.of(1, 1),
Arguments.of(2, 4),
Arguments.of(3, 9)
);
}
}4. @ValueSource: 从一组值中读取参数。
- 优点: 简单直接,适合少量参数。
- 缺点: 不适合大量参数。
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
import static org.junit.jupiter.api.Assertions.*;
public class MyParameterizedTest {
@ParameterizedTest
@ValueSource(ints = {1, 2, 3})
void testFromValues(int input) {
assertTrue(input > 0);
}
}5. 自定义参数提供器:
- 优点: 最灵活,可以实现各种复杂的逻辑。
- 缺点: 需要编写额外的类来实现
ArgumentsProvider接口。
选择哪个数据源取决于你的具体需求。 对于少量参数,@CsvSource 或 @ValueSource 就足够了。 对于大量参数,@CsvFileSource 或 @MethodSource 更合适。 如果需要复杂的参数生成逻辑,则需要自定义参数提供器。 记住要根据你的项目结构调整文件路径等信息。
@CsvSource 提供参数
// 测试@CsvSource
@ParameterizedTest(name = "加法:{0}+{1}={2}")
@CsvSource({
"1,2,3",
"4,5,9",
"7,8,15"
})
public void testCsvSource(int a, int b, int c) {
Assertions.assertEquals(c, this.demoUtil.add(a, b), a + "+" + b + "=" + c);
}@CsvFileSource 提供参数
// 测试@CsvFileSource
@ParameterizedTest(name = "加法:{0}+{1}={2}")
@CsvFileSource(resources = "/test.csv")
public void testCsvFileSource(int a, int b, int c) {
Assertions.assertEquals(c, this.demoUtil.add(a, b), a + "+" + b + "=" + c);
}@MethodSource 提供参数
// 测试 @MethodSource 从另外一个方法中获取参数
@ParameterizedTest(name = "加法:{0}+{1}={2}")
@MethodSource("methodSourceSupplier")
public void testMethodSource(int a, int b, int c) {
Assertions.assertEquals(c, this.demoUtil.add(a, b), a + "+" + b + "=" + c);
}
static Stream<Arguments> methodSourceSupplier() {
return Stream.of(
Arguments.of(1, 2, 3),
Arguments.of(4, 5, 9),
Arguments.of(7, 8, 15)
);
}@ValueSource 提供参数
// 测试 @ValueSource 从一个数组中获取参数
@ParameterizedTest(name = "测试 @ValueSource 从一个数组中获取参数 {0}")
@ValueSource(ints = {1, 2, 3})
public void testValueSource(int a) {
Assertions.assertTrue(a > 0);
}命令行运行测试
https://stackoverflow.com/questions/1873995/run-a-single-test-method-with-maven
./mvnw test -Dtest=OrderPerfAssistantTests -Dspring.profiles.active=1wmockito
详细用法请参考示例
https://gitee.com/dexterleslie/demonstration/tree/master/demo-java/demo-library/demo-testing/demo-mockito-java
mockito-inline 和 mockito-all
Mockito是一个流行的Java单元测试框架,它提供了多种工具和模块来帮助开发者进行单元测试。其中,mockito-inline和mockito-all是Mockito框架中的两个不同模块或组件,它们各自具有不同的特点和用途。
mockito-inline
- 功能:
mockito-inline是Mockito框架的一个扩展模块,它提供了对final类、final方法和静态方法的模拟功能。这是通过字节码操作来实现的,使得Mockito能够模拟那些通常无法被模拟的类和方法。 - 使用场景:当测试代码需要模拟final类、final方法或静态方法时,可以使用
mockito-inline。这在某些特定的测试场景下非常有用,比如当被测试的代码依赖于这些无法轻易更改的类和方法时。 - 依赖:要使用
mockito-inline,需要在项目的依赖管理文件中添加相应的依赖项。例如,在Maven项目中,可以在pom.xml文件中添加对mockito-inline的依赖。
mockito-all
- 功能:
mockito-all是Mockito早期版本中的一个综合模块,它包含了Mockito框架的所有核心功能和一些额外的功能。然而,从Mockito 2.x版本开始,mockito-all的发行已经停止,并被拆分为多个更小的模块,如mockito-core、mockito-junit-jupiter等。 - 使用场景:由于
mockito-all已经停止发行,并且被拆分为多个更小的模块,因此在新版本的Mockito框架中,通常不再推荐使用mockito-all。相反,应该根据具体需求选择使用mockito-core、mockito-junit-jupiter等更小的模块。 - 依赖:对于还在使用旧版本Mockito框架的项目来说,可能仍然会看到
mockito-all的依赖。但在新版本的项目中,应该避免使用mockito-all,并改为使用更具体的模块依赖。
总结
mockito-inline是Mockito框架的一个扩展模块,用于模拟final类、final方法和静态方法。mockito-all是Mockito早期版本中的一个综合模块,但已经停止发行,并被拆分为多个更小的模块。- 在新项目或升级现有项目时,应该根据具体需求选择使用
mockito-core、mockito-junit-jupiter等更小的模块,而不是使用已经停止发行的mockito-all。
此外,Mockito框架还提供了丰富的API和注解来支持单元测试,如@Mock、@InjectMocks、@RunWith(MockitoJUnitRunner.class)等,这些都可以帮助开发者更高效地编写和运行单元测试。
普通maven项目中配置mockito
在普通
maven项目中配置和使用mockito的详细请参考https://gitee.com/dexterleslie/demonstration/tree/master/demo-java/demo-library/demo-testing/demo-mockito-java
在pom.xml添加mockito的依赖
<project>
...
<dependencies>
<!-- mockito依赖 -->
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>3.9.0</version>
<scope>test</scope>
</dependency>
</dependencies>
...
</project>验证mockito是否成功引入代码
@Test
public void verify_if_function_called(){
// 创建List mock对象
List mockListObject = Mockito.mock(List.class);
mockListObject.add("val1");
Mockito.verify(mockListObject).add("val1");
}spring-boot项目中配置mockito
在
spring-boot项目中配置和使用mockito的详细请参考https://gitee.com/dexterleslie/demonstration/tree/master/demo-java/demo-library/demo-testing/demo-mockito-java
注意:mockito依赖已经包含在org.springframework.boot:spring-boot-starter-test中,所以不需要独立引入mockito依赖。
@RunWith(MockitoJUnitRunner.class)使用
详细用法请参考示例的 AnnotationMock1Test 测试用例。
@RunWith(MockitoJUnitRunner.class) 是 Mockito 和 JUnit 结合使用的一个注解,它主要用于初始化 Mockito 的环境,以便在 JUnit 测试中更方便地使用 Mockito。
具体来说,MockitoJUnitRunner 的作用有以下几点:
- 自动初始化 Mock 对象:当你使用
@Mock注解一个接口或类时,MockitoJUnitRunner会在测试运行前自动创建该接口的 Mock 对象,并将其注入到测试类中。这样你就不需要手动调用Mockito.mock()方法来创建 Mock 对象了。 - 自动注入 Mock 对象:如果你的测试类中有一些字段被标记为
@Mock或@InjectMocks,MockitoJUnitRunner会自动将这些字段注入到测试类的实例中。例如,使用@InjectMocks注解的字段会自动将所有标记为@Mock的字段注入进去。
使用例子:
import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.runners.MockitoJUnitRunner;
// 自动扫描@Mock注解的字段并创建其对象注入到测试实例中
// 不需要手动调用Mockito.mock()方法来创建 Mock 对象
@RunWith(MockitoJUnitRunner.class)
public class AnnotationMock1Test {
@Mock
private List mockListObject;
@Test
public void verify_if_function_called(){
mockListObject.add("val1");
Mockito.verify(mockListObject).add("val1");
}
}@Mock + MockitoAnnotations.initMocks()使用
详细用法请参考示例的 AnnotationMock2Test 测试用例。
MockitoAnnotations.initMocks用于手动创建@Mock注解的字段对象并注入到测试实例中
使用例子:
import java.util.List;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
public class AnnotationMock2Test {
@Mock
private List mockListObject;
@Before
public void setup(){
// 解析@Mock注解,否则mockListObject对象为null
MockitoAnnotations.initMocks(this);
}
@Test
public void verify_if_function_called(){
mockListObject.add("val1");
Mockito.verify(mockListObject).add("val1");
}
}匹配指定的参数值
匹配指定的方法调用参数
使用例子:
@Test
public void test_specific_parameters_match(){
List mockListObject=Mockito.mock(List.class);
Mockito.when(mockListObject.add("val1")).thenReturn(false);
Mockito.when(mockListObject.add("val2")).thenReturn(true);
Assert.assertFalse(mockListObject.add("val1"));
Assert.assertTrue(mockListObject.add("val2"));
}匹配指定参数类型的任意值
匹配指定参数类型的任意值方法调用
使用例子:
@Test
public void test_any_parameters_value_match(){
List mockListObject=Mockito.mock(List.class);
Mockito.when(mockListObject.add(Mockito.anyInt())).thenReturn(true);
for(int i=0;i<1000;i++){
Assert.assertTrue(mockListObject.add(i));
}
}清除之前mock调用信息
// 使用Mockito.clearInvocations清除之前的mock调用信息
// https://stackoverflow.com/questions/30081161/mockito-does-verify-method-reboot-number-of-times
@Test
public void testClearInvocations() {
List mockList = Mockito.mock(List.class);
mockList.add("1");
Mockito.verify(mockList).add("1");
mockList.add("2");
Mockito.verify(mockList).add("2");
Mockito.verify(mockList).add("1");
// 使用clearInvocations清除之前的调用信息
Mockito.clearInvocations(mockList);
mockList.add("2");
Mockito.verify(mockList).add("2");
try {
// 被clearInvocations,所以之前add("1")的调用信息不存在
Mockito.verify(mockList).add("1");
Assert.fail("预期异常没有抛出");
} catch (Throwable throwable) {
Assert.assertTrue(throwable instanceof WantedButNotInvoked);
}
}使用ArgumentMatcher匹配指定参数
todo ...
ArgumentCaptor使用
/**
* 验证调用时的参数
* https://stackoverflow.com/questions/3555472/mockito-verify-method-arguments
* https://ioflood.com/blog/mockito-verify/
*/
@Test
public void testArgumentCaptor() {
List mockList = Mockito.mock(List.class);
mockList.add(new MyArgument("Dexter"));
ArgumentCaptor<MyArgument> argumentCaptor = ArgumentCaptor.forClass(MyArgument.class);
Mockito.verify(mockList).add(argumentCaptor.capture());
// 验证方法使用预期的参数调用
Assert.assertEquals("Dexter", argumentCaptor.getValue().getName());
}
public static class MyArgument {
private String name;
public MyArgument(String name) {
this.name = name;
}
public String getName() {
return name;
}
}模拟抛出异常
todo ...
doReturn和doAnswer区别
Mockito中的doReturn和doAnswer都是用于在模拟对象(mock objects)上配置方法调用的返回值或行为的重要工具,但它们之间存在一些关键区别。以下是它们的主要区别:
doReturn
用途:
doReturn主要用于直接指定模拟对象的方法调用时应返回的固定值或一系列值。适用场景:当你需要模拟一个方法返回简单的、固定的结果时,
doReturn是最佳选择。使用方式:通常与
when一起使用,但doReturn用于链式调用中,特别是在处理void方法或需要强调“不调用真实方法”的场景时。示例:
javaMockito.doReturn("mockedValue").when(mockObject).methodToMock();这里,当
mockObject的methodToMock方法被调用时,将直接返回"mockedValue"。
doAnswer
用途:
doAnswer用于在模拟对象的方法调用时执行自定义的Answer逻辑,从而允许更复杂的返回值生成逻辑,包括基于方法参数或其他外部因素的计算。适用场景:当你需要模拟的方法返回值不是一个简单的值,而是需要根据方法参数或其他因素动态计算的结果时,
doAnswer是更合适的选择。使用方式:通过提供一个实现了
Answer接口的匿名类或使用lambda表达式来定义自定义行为。示例:
javaMockito.doAnswer(invocation -> { // 获取方法参数 Object[] args = invocation.getArguments(); // 根据参数计算返回值 int result = (int) args[0] + (int) args[1]; // 返回计算结果 return result; }).when(mockObject).add(anyInt(), anyInt());在这个例子中,当
mockObject的add方法被调用时,会执行自定义的Answer逻辑,根据传入的参数计算并返回结果。
总结
| doReturn | doAnswer | |
|---|---|---|
| 用途 | 直接指定方法调用的返回值 | 在方法调用时执行自定义的Answer逻辑 |
| 适用场景 | 简单的、固定的返回值 | 复杂的、基于参数或外部因素的计算结果 |
| 使用方式 | 通常与when一起使用,但也可用于链式调用中 | 提供一个实现了Answer接口的匿名类或lambda表达式 |
选择doReturn还是doAnswer主要取决于你的测试需求以及你希望模拟的方法行为的复杂度。对于简单的返回值模拟,doReturn通常更简洁、更直接。而对于需要更复杂逻辑的情况,doAnswer提供了更高的灵活性和控制力。
doReturn和doAnswer详细用法请参考示例 MockitoTest 的 testDifferentWithDoReturnAndDoAnswer 测试用例
/**
* 测试doReturn和doAnswer的区别
*/
@Test
public void testDifferentWithDoReturnAndDoAnswer() {
List<String> testList = Mockito.mock(List.class);
// 测试doReturn
Mockito.doReturn("hello").when(testList).get(Mockito.anyInt());
String str = testList.get(0);
Assert.assertEquals("hello", str);
// 测试doAnswer
Mockito.doAnswer(invocationOnMock -> {
// 根据输入参数返回hello-x字符串
return "hello-" + invocationOnMock.getArguments()[0];
}).when(testList).get(Mockito.anyInt());
str = testList.get(0);
Assert.assertEquals("hello-0", str);
str = testList.get(1);
Assert.assertEquals("hello-1", str);
}验证调用次数或者未调用
/**
* 验证调用次数或者未调用
*/
@Test
public void verifying_number_of_invocations() {
List list = Mockito.mock(List.class);
list.add(1);
list.add(2);
list.add(2);
list.add(3);
list.add(3);
list.add(3);
//验证是否被调用一次,等效于下面的times(1)
Mockito.verify(list).add(1);
Mockito.verify(list, Mockito.times(1)).add(1);
//验证是否被调用2次
Mockito.verify(list, Mockito.times(2)).add(2);
//验证是否被调用3次
Mockito.verify(list, Mockito.times(3)).add(3);
//验证是否从未被调用过
Mockito.verify(list, Mockito.never()).add(4);
//验证至少调用一次
Mockito.verify(list, Mockito.atLeastOnce()).add(1);
//验证至少调用2次
Mockito.verify(list, Mockito.atLeast(2)).add(2);
//验证至多调用3次
Mockito.verify(list, Mockito.atMost(3)).add(3);
}验证调用顺序
/**
* 验证调用顺序
*/
@Test
public void verification_in_order() {
List list = Mockito.mock(List.class);
List list2 = Mockito.mock(List.class);
list.add(1);
list2.add("hello");
list.add(2);
list2.add("world");
//将需要排序的mock对象放入InOrder
InOrder inOrder = Mockito.inOrder(list, list2);
//下面的代码不能颠倒顺序,验证执行顺序
inOrder.verify(list).add(1);
inOrder.verify(list2).add("hello");
inOrder.verify(list).add(2);
inOrder.verify(list2).add("world");
}真实对象spy
todo ...
真实对象部分mock
todo ...
静态log字段注入替换为mock logger
详细使用请参考
https://gitee.com/dexterleslie/demonstration/blob/master/demo-mockito/demo-mockito-springboot/src/test/java/com/future/demo/StaticLoggerFieldInjectionTests.java外部参考链接:
替换通过lombok @Slf4j注解注入的log字段为mock logger
例子:
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestTemplate;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
@RunWith(SpringRunner.class)
@SpringBootTest(
classes={Application.class},
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT
)
public class StaticLoggerFieldInjectionTests {
@LocalServerPort
int port;
// 用于注入mock的logger
@Mock
Logger log;
// 用于替换通过lombok @Slf4j注入的静态log字段
@Autowired
ApiController apiController;
@Autowired
private RestTemplate restTemplate = null;
@Before
public void setup() throws Exception {
// 初始化@Mock注解的字段
MockitoAnnotations.initMocks(this);
// 替换ApiController对象中的静态log为mock logger
setFinalStatic(ApiController.class.getDeclaredField("log"), log);
}
// 替换静态字段
public static void setFinalStatic(Field field, Object newValue) throws Exception {
field.setAccessible(true);
Field modifiersField = Field.class.getDeclaredField("modifiers");
modifiersField.setAccessible(true);
modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL);
field.set(null, newValue);
}
@Test
public void test1() {
ResponseEntity<String> response = this.restTemplate.getForEntity(
"http://localhost:"+ port + "/api/test1",
String.class);
Assert.assertEquals("Hello ....", response.getBody());
// 用于验证是否使用指定的参数调用log.info(...)方法
Mockito.verify(log).info("Api for testing is called.");
}
}mock final声明的类
示例的详细用法请参考 MockitoTest 测试用例的 testMockFinalClass 方法
maven配置引用mockito-inline依赖,否则无法mock final声明的类
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>3.9.0</version>
<scope>test</scope>
</dependency>使用final生命类
/**
* 用于协助演示mockito mock final声明的类
*/
public final class MyFinalClass {
public String sayHello() {
return "hello";
}
}mock final声明的类测试
/**
* 演示mock final类
*/
@Test
public void testMockFinalClass() {
MyFinalClass myFinalClass = Mockito.mock(MyFinalClass.class);
Mockito.doReturn("H").when(myFinalClass).sayHello();
String str = myFinalClass.sayHello();
Assert.assertEquals("H", str);
}@MockBean
详细使用请参考
https://gitee.com/dexterleslie/demonstration/blob/master/demo-mockito/demo-mockito-springboot/src/test/java/com/future/demo/MockBeanTests.java
自动创建 mock bean 并注入到 spring 容器中,自动替换所有同类型的 bean
@InjectMocks + @Mock
详细使用请参考
https://gitee.com/dexterleslie/demonstration/blob/master/demo-mockito/demo-mockito-springboot/src/test/java/com/future/demo/InjectMocksNMockTests.java
使用以上组合注入 @Mock 注解生成的 bean 到 @InjectMocks bean 中
如果允许情况下建议使用 @MockBean 替换这个使用组合
@InjectMocks+@Mock 和 @MockBean 区别
@InjectMocks + @Mock 和 @MockBean 在 Java 测试框架中有着不同的应用场景和功能。以下是对这两者的详细比较:
一、定义与来源
- @InjectMocks + @Mock
- @InjectMocks:是 Mockito 测试框架提供的一个注解,用于将模拟对象(mock objects)注入到被测试对象(System Under Test, SUT)中。
- @Mock:同样是 Mockito 提供的注解,用于创建模拟对象。这些对象的方法默认都是空的,可以通过 Mockito 提供的 API(如 when()、thenReturn() 等)来配置它们的行为。
- @MockBean
- 定义:是 Spring Boot 测试模块提供的特定于 Spring Boot 的注解,用于在测试中创建一个模拟对象,并将其注入到 Spring 上下文中,替换掉原来的真实 Bean。
- 来源:Spring Boot 测试模块(spring-boot-test 包),是对 Mockito 注解的扩展和与 Spring 生态系统的集成。
二、使用场景
- @InjectMocks + @Mock
- 主要用于单元测试(Unit Testing),特别是当被测试对象不依赖于 Spring 上下文时。
- 通过使用 @Mock 创建模拟对象,并使用 @InjectMocks 将这些模拟对象注入到被测试对象中,从而隔离被测试对象的依赖项,使其可以在不受外部系统或组件干扰的情况下进行测试。
- @MockBean
- 主要用于集成测试(Integration Testing),特别是当被测试对象依赖于 Spring 上下文中的 Bean 时。
- 使用 @MockBean 可以创建一个模拟对象,并将其注入到 Spring 上下文中,替换掉原来的真实 Bean。这样,可以在不启动整个外部系统或组件的情况下,对被测试对象进行测试。
三、初始化与注入
- @InjectMocks + @Mock
- 模拟对象(@Mock)需要在测试类中显式声明。
- 被测试对象(@InjectMocks)会自动创建,并将模拟对象注入其中。
- 注入方式可以是构造函数注入、setter 方法注入或属性注入。
- @MockBean
- 模拟对象会在测试上下文设置期间由 Spring Boot 测试框架自动初始化。
- 模拟对象会自动注入到 Spring 上下文中,替换掉原来的真实 Bean。
- 注入方式遵循 Spring 的依赖注入规则。
四、影响范围
- @InjectMocks + @Mock
- 仅影响被测试对象及其依赖项,不影响 Spring 上下文中的其他 Bean。
- @MockBean
- 在测试期间,会影响 Spring 上下文中的特定 Bean,将其替换为模拟对象。
- 这可能会影响其他依赖于该 Bean 的组件。
五、示例代码
- @InjectMocks + @Mock 示例
import static org.mockito.Mockito.*;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
public class UserServiceTest {
@Mock
private UserDao userDao;
@InjectMocks
private UserService userService;
@Test
public void testGetUser() {
User user = new User();
user.setId(1);
user.setName("John Doe");
when(userDao.findById(1)).thenReturn(user);
User result = userService.getUser(1);
assertEquals("John Doe", result.getName());
verify(userDao, times(1)).findById(1);
}
}- @MockBean 示例
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
@SpringBootTest
public class UserServiceIntegrationTest {
@Autowired
private UserService userService;
@MockBean
private UserRepository userRepository;
@Test
public void testGetUser() {
User user = new User();
user.setId(1);
user.setName("Jane Doe");
when(userRepository.findById(1)).thenReturn(java.util.Optional.of(user));
User result = userService.getUser(1);
assertEquals("Jane Doe", result.getName());
verify(userRepository, times(1)).findById(1);
}
}在上面的示例中,@InjectMocks + @Mock 用于单元测试,而 @MockBean 用于集成测试。在单元测试中,我们直接模拟 UserDao 并将其注入到 UserService 中。在集成测试中,我们模拟 UserRepository 并将其注入到 Spring 上下文中,以便在测试 UserService 时使用。
综上所述,@InjectMocks + @Mock 和 @MockBean 在 Java 测试框架中具有不同的应用场景和功能。选择哪个注解取决于具体的测试需求和上下文环境。
@SpyBean
详细使用请参考
https://gitee.com/dexterleslie/demonstration/blob/master/demo-mockito/demo-mockito-springboot/src/test/java/com/future/demo/SpyBeanTests.java
自动创建 mock bean 并注入到 spring 容器中,自动替换所有同类型的 bean,没有被定义 mock 规则的方法默认执行原始逻辑。
@InjectMocks + @Spy
详细使用请参考
https://gitee.com/dexterleslie/demonstration/blob/master/demo-mockito/demo-mockito-springboot/src/test/java/com/future/demo/InjectMocksNSpyTests.java
和 @InjectMocks + @Mock 区别是,如果没有定义 mock 规则(Mockito.doReturn("param2=p2").when(this.myServiceInner).test2(Mockito.anyString())😉 则 @Spy 注入的 bean 会执行原来没有被 mock 的代码逻辑(实现一个实例部分接口被 mock的目的),而 @Mock 注入的 bean 没有定义 mock 规则只会返回默认值(String类型返回返回值为null,int类型返回返回值为0)。
如果允许情况下建议使用 @SpyBean 替换这个使用组合
SpringBoot 项目测试
SpringBoot 项目测试方案参考 链接