Java单元测试实战:JUnit4与Hamcrest核心用法与最佳实践
1. 项目概述:为什么JUnit4与Hamcrest是Java单元测试的黄金搭档
如果你写过Java单元测试,那你肯定用过JUnit。但很多时候,光用JUnit的 assertEquals 、 assertTrue ,写出来的测试代码会变得又臭又长,可读性极差。比如,你想验证一个返回的列表不为空、包含特定元素、并且元素数量大于5,用原生JUnit你得写好几个断言,测试失败时信息也不够清晰。这就是Hamcrest登场的时候了。这个项目标题“Java单元测试必备库JUnit4与Hamcrest实战指南”,直指一个核心痛点:如何写出表达力强、易于维护的单元测试。JUnit4提供了测试框架的骨架,而Hamcrest则提供了丰富、可组合的“匹配器”(Matcher),让断言读起来就像一句自然语言。这对组合不是简单的1+1,而是能让你从“能测试”进化到“写好测试”的关键。无论是刚入门的新手,还是被凌乱断言困扰的老手,掌握它都能让你的测试代码质量上一个台阶。
2. 环境搭建与基础配置
2.1 依赖引入:Maven与Gradle配置详解
万事开头难,但配置依赖其实很简单。现在Java项目管理基本离不开Maven或Gradle,我们分别来看。
对于Maven项目,在你的 pom.xml 文件的 <dependencies> 部分加入以下内容。注意,JUnit4和Hamcrest通常都放在 test 作用域,因为它们是测试专用库,不会打包进最终的生产代码。
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
这里有个细节: hamcrest-core 包含了最基础的匹配器,而 hamcrest-library 扩展了大量实用的匹配器(比如集合、数字、文本匹配)。通常你只需要声明 hamcrest-library ,因为它会依赖 hamcrest-core 。但有些老项目或者为了明确,两者都写上也没问题。版本方面,JUnit 4.13.2是4.x系列的最终版,非常稳定;Hamcrest 2.2也是主流选择。
如果你用的是Gradle(Kotlin DSL示例),在 build.gradle.kts 的 dependencies 块里添加:
dependencies {
testImplementation("junit:junit:4.13.2")
testImplementation("org.hamcrest:hamcrest-library:2.2")
}
testImplementation 就等同于Maven的 test 作用域。配置完成后,记得刷新一下你的项目(Maven的Reimport,Gradle的Refresh),让IDE识别新依赖。
2.2 IDE集成与测试类创建规范
依赖搞定后,IDE(比如IntelliJ IDEA或Eclipse)会自动识别。创建一个测试类非常简单。通常的规范是,在 src/test/java 目录下,建立与生产代码对应的包结构。例如,你的生产类 com.example.service.UserService 在 src/main/java 里,那么测试类 UserServiceTest 就应该放在 src/test/java/com/example/service 下。
在IDEA里,你可以直接在生产类名上按 Alt+Enter (Windows/Linux)或 Option+Enter (Mac),选择“Create Test”,它会帮你自动生成测试类骨架。这里有个实操心得: 测试类的命名强烈建议以 Test 结尾 ,比如 CalculatorTest 。这是Maven Surefire插件(负责执行测试)的默认约定,它能自动发现并运行这些测试。如果你命名为 CalculatorTestSuite 或者 TestCalculator ,可能需要额外配置插件,徒增烦恼。
生成的测试类大概长这样:
import org.junit.Test;
import static org.junit.Assert.*;
public class CalculatorTest {
@Test
public void testAdd() {
// TODO: 测试逻辑
}
}
注意顶部的静态导入: import static org.junit.Assert.*; 。这是使用JUnit断言的标准方式。但接下来,我们会用Hamcrest的 assertThat 来替代它,所以这个导入后续可能会被替换或共存。
3. JUnit4核心机制与生命周期
3.1 注解驱动:@Test, @Before, @After, @BeforeClass, @AfterClass
JUnit4最大的进步就是从继承 TestCase 和命名约束(方法名必须 test 开头)变成了注解驱动。这几个注解构成了测试的生命周期。
-
@Test: 这是核心,标记一个方法为测试方法。方法必须是public void,可以带参数(但通常不带)。你可以通过@Test(timeout=1000)来设置超时(毫秒),如果测试执行超过这个时间就失败;也可以用@Test(expected=NullPointerException.class)来声明期望抛出某个异常。 -
@Before和@After: 这对注解标记的方法会在 每一个@Test方法执行 之前 和 之后 运行。@Before通常用来初始化测试数据(比如每个测试都需要一个新的、干净的对象),@After用来清理资源(比如关闭数据库连接、删除临时文件)。注意,即使@Test方法抛出异常,@After方法也 保证会执行 ,这很重要。 -
@BeforeClass和@AfterClass: 这两个注解标记的方法是static的,它们在 整个测试类 的所有测试方法执行 之前 和 之后 运行,且只运行一次。@BeforeClass适合做重量级的、一次性的初始化,比如建立数据库连接池;@AfterClass则用来关闭这些全局资源。
来看一个完整的例子,假设我们测试一个简单的文件处理器:
import org.junit.*;
import java.io.*;
public class FileProcessorTest {
private static File sharedResource;
private FileProcessor processor;
private File tempFile;
@BeforeClass
public static void setUpClass() {
System.out.println("初始化全局资源,例如数据库连接");
sharedResource = new File("/tmp/shared.log");
}
@AfterClass
public static void tearDownClass() throws IOException {
System.out.println("清理全局资源");
if (sharedResource.exists()) {
sharedResource.delete();
}
}
@Before
public void setUp() throws IOException {
System.out.println("为每个测试准备独立环境");
processor = new FileProcessor();
tempFile = File.createTempFile("test", ".txt");
// 写入一些初始数据到tempFile
}
@After
public void tearDown() {
System.out.println("清理每个测试的现场");
if (tempFile.exists()) {
tempFile.delete();
}
processor = null;
}
@Test
public void testProcessFile() {
// 使用tempFile和processor进行测试
// ...
}
@Test(timeout = 500)
public void testProcessShouldNotTimeout() {
// 一个应该很快完成的测试
// ...
}
}
运行这个测试类,控制台输出的顺序会是: setUpClass -> setUp -> testProcessFile -> tearDown -> setUp -> testProcessShouldNotTimeout -> tearDown -> tearDownClass 。清晰的生命周期管理是编写稳定、独立测试用例的基石。
3.2 测试套件与忽略测试:@RunWith, @Suite, @Ignore
单个测试类没问题了,但项目有成百上千个测试类怎么办?JUnit4提供了组织能力。
-
@RunWith与@Suite: 你可以创建一个“测试套件”来批量运行测试。创建一个空类,用@RunWith(Suite.class)注解,并通过@Suite.SuiteClasses指定要包含的测试类。import org.junit.runner.RunWith; import org.junit.runners.Suite; @RunWith(Suite.class) @Suite.SuiteClasses({ CalculatorTest.class, UserServiceTest.class, PaymentServiceTest.class }) public class AllUnitTestSuite { // 这个类本身没有内容,只是一个套件的容器 }运行
AllUnitTestSuite,就会依次运行它包含的所有测试类。这在CI/CD流水线中指定运行某一组测试时非常有用。 -
@Ignore: 如果一个测试暂时不想运行(比如对应的功能还没实现,或者已知有问题在修复中),你可以在@Test方法或整个测试类上加上@Ignore注解。被忽略的测试不会被执行,但会在测试报告中标记出来,提醒你后续处理。 切忌把@Ignore当成永久解决方案 ,它只是一个临时标记。
4. Hamcrest匹配器深度解析与应用
4.1 核心匹配器:对象、文本与数值断言
Hamcrest的强大在于它那一套描述性极强的匹配器。我们先从静态导入开始,这是流畅写法的关键:
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
// 注意:是 Matchers.* ,里面包含了所有核心匹配器
assertThat 是Hamcrest的断言入口,语法是 assertThat(actualValue, matcher) 。读起来就是“断言实际值符合某个匹配器”。我们来看几个最常用的:
-
对象相等与身份 :
equalTo(obj): 检查对象是否通过equals()方法相等。这是最常用的。sameInstance(obj)或is(obj): 检查是否是同一个对象(==)。notNullValue()/nullValue(): 检查是否为null或非null。
String name = "Alice"; assertThat(name, equalTo("Alice")); assertThat(name, is(notNullValue())); // `is`是一个装饰器,增加可读性,`is(notNullValue())`等价于`notNullValue()` assertThat(name, is(not("Bob"))); // `not`是逻辑非 -
文本匹配 :
containsString(substring): 包含子串。startsWith(prefix)/endsWith(suffix): 以...开头/结尾。equalToIgnoringCase(string): 忽略大小写相等。matchesPattern(regex): 匹配正则表达式。
String message = "Hello, JUnit world!"; assertThat(message, containsString("JUnit")); assertThat(message, startsWith("Hello")); assertThat("HELLO", equalToIgnoringCase("hello")); assertThat("123-45", matchesPattern("\\d{3}-\\d{2}")); -
数值比较 :
closeTo(expected, delta): 对于浮点数(double或float),检查实际值是否在期望值的±delta范围内。这是处理浮点数精度问题的利器。greaterThan(num),greaterThanOrEqualTo(num),lessThan(num),lessThanOrEqualTo(num)。
double computedValue = 3.1415926; assertThat(computedValue, closeTo(3.14, 0.01)); // 允许0.01的误差 assertThat(100, is(greaterThan(50))); assertThat(list.size(), greaterThanOrEqualTo(1));
4.2 集合与数组匹配:验证复杂数据结构
处理集合是单元测试的常事,Hamcrest提供了极其强大的集合匹配器。
-
基础集合检查 :
hasItem(item)/hasItems(item1, item2, ...): 集合中是否包含某个或某些元素。empty()/emptyCollectionOf(Class): 集合是否为空。hasSize(int): 集合大小。
List<String> fruits = Arrays.asList("apple", "banana", "orange"); assertThat(fruits, hasItem("banana")); assertThat(fruits, hasItems("apple", "orange")); assertThat(fruits, hasSize(3)); assertThat(new ArrayList<>(), empty()); -
迭代顺序与内容精确匹配 :
contains(element1, element2, ...): 严格匹配 !它要求迭代项(如List)的元素顺序和数量必须完全一致。这是验证集合内容最严格的方式。containsInAnyOrder(element1, element2, ...): 包含所有指定元素,但顺序任意。
List<Integer> numbers = Arrays.asList(1, 2, 3); assertThat(numbers, contains(1, 2, 3)); // 通过 // assertThat(numbers, contains(1, 3, 2)); // 失败,顺序不对 assertThat(numbers, containsInAnyOrder(3, 1, 2)); // 通过 -
针对数组 :上述大部分匹配器对数组也适用,因为Hamcrest做了适配。你也可以用
array(contaning(...))或arrayWithSize(int)。 -
Map匹配器 :
hasEntry(key, value): Map是否包含特定键值对。hasKey(key)/hasValue(value): 是否包含键或值。
Map<String, Integer> scores = new HashMap<>(); scores.put("Alice", 95); scores.put("Bob", 87); assertThat(scores, hasEntry("Alice", 95)); assertThat(scores, hasKey("Bob"));
4.3 组合匹配器与属性提取:应对复杂对象断言
这才是Hamcrest的精华所在——组合。你可以用逻辑操作符把简单的匹配器组合成复杂的条件,让断言表达复杂的业务逻辑。
-
逻辑组合器 :
allOf(matcher1, matcher2, ...): 逻辑“与”,所有匹配器都必须满足。anyOf(matcher1, matcher2, ...): 逻辑“或”,至少满足一个。not(matcher): 逻辑“非”。
String id = "USER_12345"; // 断言ID以"USER_"开头,并且长度大于8 assertThat(id, allOf(startsWith("USER_"), hasLength(greaterThan(8)))); // 断言状态码是成功(200)或者是重定向(301, 302) int statusCode = 301; assertThat(statusCode, anyOf(is(200), is(301), is(302))); // 断言结果不是错误状态 assertThat(result, not(is(ERROR))); -
Bean属性匹配 :这是处理自定义对象(POJO)的神器。你不需要手动调用getter再断言,可以直接匹配属性。
hasProperty(“propertyName”, matcher): 检查对象是否具有指定名称的属性(通过getter或public字段),并且该属性的值符合给定的匹配器。
public class User { private String name; private int age; // 省略构造器、getter/setter public String getName() { return name; } public int getAge() { return age; } } User user = new User("Tom", 30); // 传统JUnit方式: assertEquals("Tom", user.getName()); assertTrue(user.getAge() > 18); // Hamcrest方式: assertThat(user, hasProperty("name", equalTo("Tom"))); assertThat(user, hasProperty("age", greaterThan(18))); // 甚至可以组合: assertThat(user, allOf( hasProperty("name", equalTo("Tom")), hasProperty("age", greaterThan(18)) ));这个功能在验证方法返回的复杂对象时特别有用,测试代码的可读性直线上升。它通过Java Beans的内省机制来查找
getName()或isName()这类方法,或者直接访问public字段。
5. 实战:从零构建一个可测试的服务层
5.1 设计一个可测试的领域模型
光说不练假把式,我们用一个简单的“用户积分服务”作为例子。假设我们有 User 和 PointsTransaction 两个领域对象。
// User.java
public class User {
private final String id;
private final String username;
private int points;
// 构造器、getter省略...
public void addPoints(int delta) {
if (delta <= 0) {
throw new IllegalArgumentException("增加的积分必须为正数");
}
this.points += delta;
}
public boolean deductPoints(int delta) {
if (delta <= 0) {
throw new IllegalArgumentException("扣除的积分必须为正数");
}
if (this.points >= delta) {
this.points -= delta;
return true;
}
return false;
}
}
// PointsTransaction.java - 记录积分变动
public class PointsTransaction {
private final String userId;
private final int amount;
private final TransactionType type; // enum: EARN, SPEND
private final Instant createdAt;
// 构造器、getter省略...
}
注意 User 类中方法的参数校验和业务逻辑(积分不足时扣除失败),这些都是我们测试的重点。
5.2 编写基于JUnit4与Hamcrest的单元测试
现在为 User 类编写测试。我们创建一个 UserTest 类。
import org.junit.Before;
import org.junit.Test;
import java.util.UUID;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.*;
public class UserTest {
private User testUser;
@Before
public void setUp() {
// 在每个测试前创建一个新的、初始积分为100的用户
testUser = new User(UUID.randomUUID().toString(), "testUser", 100);
}
@Test
public void userShouldBeCreatedWithInitialPoints() {
// 断言对象创建正确
assertThat(testUser, allOf(
hasProperty("username", equalTo("testUser")),
hasProperty("points", is(100)) // `is(100)` 等价于 `equalTo(100)`
));
}
@Test
public void addPointsShouldIncreasePoints() {
// 执行
testUser.addPoints(50);
// 验证
assertThat(testUser.getPoints(), is(150));
}
@Test(expected = IllegalArgumentException.class)
public void addPointsWithNonPositiveShouldThrowException() {
// 预期会抛出IllegalArgumentException
testUser.addPoints(0); // 这里应该触发异常
// 如果没抛出异常,测试会失败
}
@Test
public void deductPointsShouldDecreasePointsWhenSufficient() {
// 执行
boolean success = testUser.deductPoints(30);
// 验证状态和行为
assertThat(success, is(true));
assertThat(testUser.getPoints(), is(70));
}
@Test
public void deductPointsShouldFailAndNotChangePointsWhenInsufficient() {
// 执行
boolean success = testUser.deductPoints(150); // 积分不足
// 验证
assertThat(success, is(false));
assertThat(testUser.getPoints(), is(100)); // 积分应保持不变
}
@Test
public void deductPointsWithNonPositiveShouldThrowException() {
// 这里我们用try-catch来更精细地验证异常信息
try {
testUser.deductPoints(-10);
// 如果上一行没抛异常,说明测试失败
org.junit.Assert.fail("Expected IllegalArgumentException was not thrown");
} catch (IllegalArgumentException e) {
// 验证异常信息
assertThat(e.getMessage(), containsString("扣除的积分必须为正数"));
}
}
}
这个测试类展示了多种场景:正常行为验证、异常测试(通过 @Test(expected) 和try-catch两种方式)、以及使用Hamcrest组合匹配器对对象属性进行声明式断言。 setUp 方法确保了每个测试的独立性。
5.3 集成测试示例:服务层与Mockito结合
单元测试通常要求隔离,对于 UserService 这种依赖 UserRepository (数据库访问)的服务,我们需要用Mockito这样的模拟框架来隔离外部依赖。这里展示JUnit4 + Hamcrest + Mockito的经典组合。
首先,添加Mockito依赖(Maven):
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>5.0.0</version>
<scope>test</scope>
</dependency>
假设我们有如下服务:
public class UserPointsService {
private final UserRepository userRepository;
public UserPointsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public boolean earnPoints(String userId, int points) {
if (points <= 0) return false;
User user = userRepository.findById(userId);
if (user == null) return false;
user.addPoints(points);
return userRepository.save(user);
}
}
对应的测试类:
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@RunWith(MockitoJUnitRunner.class) // 使用Mockito的JUnit运行器
public class UserPointsServiceTest {
@Mock
private UserRepository mockUserRepository; // 模拟依赖
@InjectMocks
private UserPointsService userPointsService; // 被测试对象,自动注入mock
@Test
public void earnPointsShouldSuccessAndSaveUser() {
// 1. 准备数据 (Given)
String userId = "user-1";
User mockUser = new User(userId, "Alice", 50);
when(mockUserRepository.findById(eq(userId))).thenReturn(mockUser);
when(mockUserRepository.save(any(User.class))).thenReturn(true);
// 2. 执行操作 (When)
boolean result = userPointsService.earnPoints(userId, 30);
// 3. 验证行为与状态 (Then)
assertThat(result, is(true));
// 验证用户积分增加了
assertThat(mockUser.getPoints(), is(80));
// 验证repository的save方法被调用了一次,并且传入的用户积分是80
verify(mockUserRepository).save(argThat(user ->
user.getId().equals(userId) && user.getPoints() == 80
));
}
@Test
public void earnPointsShouldReturnFalseWhenUserNotFound() {
// Given
when(mockUserRepository.findById(any())).thenReturn(null);
// When
boolean result = userPointsService.earnPoints("non-existent", 100);
// Then
assertThat(result, is(false));
// 确保save方法没有被调用
verify(mockUserRepository, never()).save(any());
}
}
这里的关键点:
@RunWith(MockitoJUnitRunner.class):让Mockito管理测试中的Mock对象。@Mock:创建一个模拟的UserRepository。@InjectMocks:创建UserPointsService实例,并将@Mock标注的依赖自动注入进去。when(...).thenReturn(...):设定模拟对象的行为。verify(...):验证模拟对象的某个方法是否被调用,以及调用的参数。这里我们结合了Hamcrest的argThat,它接受一个Hamcrest匹配器(我们用了Lambda,它本质上是一个ArgumentMatcher),来对传入的参数进行更灵活的验证。这种组合让验证逻辑非常清晰有力。
6. 高级技巧与最佳实践
6.1 自定义匹配器:封装领域特定断言
当你的领域有复杂的验证逻辑时,重复的Hamcrest组合会显得冗长。这时可以创建自定义匹配器,让测试代码更简洁、更贴近业务语言。
例如,我们经常要验证一个 User 是否是“活跃用户”(假设规则是积分大于100且最近一个月有登录)。我们可以创建一个 IsActiveUser 匹配器。
import org.hamcrest.Description;
import org.hamcrest.TypeSafeMatcher;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
public class IsActiveUser extends TypeSafeMatcher<User> {
// 推荐:提供一个静态工厂方法,让使用更流畅
public static IsActiveUser activeUser() {
return new IsActiveUser();
}
@Override
protected boolean matchesSafely(User user) {
// 你的业务判断逻辑
boolean hasEnoughPoints = user.getPoints() > 100;
boolean recentLogin = user.getLastLoginTime() != null &&
user.getLastLoginTime().isAfter(Instant.now().minus(30, ChronoUnit.DAYS));
return hasEnoughPoints && recentLogin;
}
@Override
public void describeTo(Description description) {
description.appendText("一个活跃用户(积分>100且近30天有登录)");
}
// 可选:提供失败时的描述信息
@Override
protected void describeMismatchSafely(User user, Description mismatchDescription) {
mismatchDescription.appendText("用户" + user.getUsername())
.appendText(" 积分=" + user.getPoints());
if (user.getLastLoginTime() == null) {
mismatchDescription.appendText(", 从未登录");
} else {
long daysSinceLogin = ChronoUnit.DAYS.between(user.getLastLoginTime(), Instant.now());
mismatchDescription.appendText(", 距离上次登录" + daysSinceLogin + "天");
}
}
}
使用这个自定义匹配器:
@Test
public void shouldReturnActiveUsers() {
User activeUser = new User("id1", "Alice", 150);
activeUser.setLastLoginTime(Instant.now().minus(10, ChronoUnit.DAYS));
assertThat(activeUser, IsActiveUser.activeUser());
// 读起来非常自然:“断言这个用户是活跃用户”
}
当断言失败时,控制台会输出清晰的描述,比如“期望是一个活跃用户...,但用户Alice 积分=50, 距离上次登录35天”,极大提升了测试失败信息的可调试性。
6.2 测试代码重构与可读性提升
好的测试代码和好的生产代码一样需要维护。一些提升可读性的技巧:
- 使用有意义的测试方法名 :不要用
test1,要用deductPointsShouldFailWhenBalanceInsufficient这样的句子,清晰地说明测试的场景和预期。 - 遵循Given-When-Then模式 :在测试方法内用空行或注释分隔“准备数据(Given)”、“执行操作(When)”、“验证结果(Then)”三个阶段。这让测试逻辑一目了然。
- 提取公共代码 :如果多个测试需要相同的准备步骤(比如创建一个复杂的测试对象),可以提取到
@Before方法或者单独的工厂方法中。 - 利用Hamcrest的流畅性 :多使用
allOf、anyOf、not来组合断言,让断言本身成为对业务规则的描述。 - 谨慎使用
@Ignore:被忽略的测试容易被人遗忘。如果测试失败,优先修复它;如果功能暂不需要,考虑删除测试代码,等需要时再写。
6.3 常见陷阱与性能考量
- 过度测试实现细节 :单元测试应该关注行为(输出、状态变化、交互),而不是内部实现。如果你发现测试因为一个私有方法的微小重构而大面积失败,那可能是在测试实现细节。这会让测试变得脆弱。
- 测试间的依赖 :确保每个测试都是独立的,不依赖其他测试的执行顺序或产生的数据。这就是为什么我们要在
@Before中初始化,在@After中清理。JUnit并不保证测试方法的执行顺序。 - 耗时操作放在
@BeforeClass/@AfterClass:建立数据库连接、启动嵌入式服务器等操作应该放在@BeforeClass中,避免每个测试都重复执行,影响测试速度。 - 避免“测试一切”的冲动 :单元测试的目标是快速反馈。为每个公有方法都写测试,但优先覆盖核心业务逻辑、复杂分支和边界条件。Getter/Setter简单到一眼就能看对的,可以不测。
- 匹配器选择 :对于集合,想清楚你到底要测什么。
hasItem是“包含”,contains是“完全匹配且顺序一致”。用错了会导致测试行为与预期不符。 - 静态导入冲突 :如果你同时静态导入了JUnit的
Assert.*和Hamcrest的Matchers.*,并且都用了assertThat,编译器会报错。建议只导入Hamcrest的assertThat,因为它的功能更强大。JUnit的老式断言可以逐步替换。
7. 迁移与升级:JUnit4到JUnit5的平滑过渡
虽然标题是JUnit4,但了解未来方向很重要。JUnit5(Jupiter)是新一代测试框架,它模块化更好,功能更强大。好消息是,JUnit5完全支持运行JUnit4的测试。所以迁移可以逐步进行。
核心变化 :
- 包名和注解变化 :
org.junit->org.junit.jupiter.api。@Test注解来自新的包,@Before变成了@BeforeEach,@After变成了@AfterEach,@BeforeClass/@AfterClass变成了@BeforeAll/@AfterAll(且方法必须是static)。 - 断言库 :JUnit5有自己的
Assertions类,功能类似Hamcrest但更现代。不过, Hamcrest仍然可以在JUnit5中完美使用 !你只需要使用org.hamcrest.MatcherAssert.assertThat即可。 - 更多特性 :JUnit5支持参数化测试、动态测试、嵌套测试、测试接口默认方法等高级功能。
混合使用 :在同一个项目中,你可以一部分测试用JUnit4写,一部分用JUnit5写。构建工具(如Maven的Surefire Plugin)需要同时配置两个测试引擎:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M7</version>
<dependencies>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>5.9.2</version>
</dependency>
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<version>5.9.2</version>
</dependency>
</dependencies>
</plugin>
junit-vintage-engine 就是用来运行JUnit4测试的。所以,掌握好JUnit4+Hamcrest这套组合,不仅现在有用,也是未来平滑升级到JUnit5的坚实基础。你可以先在新写的测试中尝试JUnit5,老测试慢慢重构,毫无压力。
更多推荐
所有评论(0)