单元测试框架选型:JUnit 5 vs TestNG vs Spock
说到单元测试框架,我这些年也算是把主流的都折腾了一遍。从最早的JUnit 4,到后来项目里强推TestNG,再到最近几年沉迷Spock的简洁。说实话,没有银弹,每个框架都有自己的脾气。
今天我就把这三个框架掰开揉碎了讲,顺便聊聊断言库和Mock工具怎么选。嗯,这些都是我在真实项目里踩过的坑,希望能帮你少走弯路。
JUnit 5:生态之王,稳如老狗
JUnit 5是目前Java生态里最主流的测试框架。为什么?因为它背后有整个Spring生态在撑腰。你想想看,Spring Boot 2.2+默认集成的就是JUnit 5,这基本就是官方钦定的节奏。
我个人习惯用JUnit 5做大部分微服务的单元测试。它的架构设计很清晰——JUnit Platform负责启动,JUnit Jupiter提供编程模型,JUnit Vintage兼容老版本。这套设计让我在升级老项目时几乎零成本。
核心特性:
- @Test、@ParameterizedTest、@RepeatedTest 三种测试模式
- @DisplayName 支持中文描述,对非技术人员友好
- @Nested 实现测试类分层,结构清晰
- 动态测试 @TestFactory,运行时生成测试用例
@DisplayName("订单服务单元测试")
class OrderServiceTest {
@Nested
@DisplayName("创建订单场景")
class CreateOrder {
@Test
@DisplayName("正常创建订单应该成功")
void shouldCreateOrderSuccessfully() {
// 测试逻辑
}
@Test
@DisplayName("库存不足应该抛出异常")
void shouldThrowWhenStockInsufficient() {
// 测试逻辑
}
}
}
我在项目中遇到过一个问题:JUnit 5的参数化测试虽然强大,但数据源如果写死在@CsvSource里,维护起来很痛苦。后来我改用@MethodSource,把测试数据抽到单独的方法里,清爽多了。
TestNG:老牌劲旅,灵活至上
TestNG当年可是为了干掉JUnit 4而生的。它的设计理念更贴近企业级测试——支持测试组、依赖测试、并行执行。如果你在做集成测试或者端到端测试,TestNG的灵活性会让你爱不释手。
我记得有个项目需要按模块分组执行测试,JUnit 5当时还没@Tag,TestNG的@Test(groups = "smoke")直接搞定。不过现在JUnit 5也有@Tag了,差距在缩小。
什么时候选TestNG?
- 需要复杂的测试依赖关系(比如A测试失败就不跑B测试)
- 需要按组执行测试(冒烟测试、回归测试分开跑)
- 需要并行执行测试用例(TestNG原生支持多线程)
public class PaymentServiceTest {
@Test(groups = {"smoke", "regression"})
public void testPaymentSuccess() {
// 冒烟测试和回归测试都会执行
}
@Test(dependsOnMethods = {"testPaymentSuccess"})
public void testRefund() {
// 只有支付成功后才执行退款测试
}
}
避坑指南:我曾经在TestNG里滥用dependsOnMethods,结果测试用例之间形成了复杂的依赖链。一旦中间某个测试失败,后面一串都不执行,排查问题非常痛苦。后来我改成每个测试独立,用@BeforeClass做前置准备,反而更稳定。
Spock:Groovy加持,测试如诗
Spock是我个人最喜欢的测试框架,没有之一。它基于Groovy,语法极其简洁。你想想看,一个测试用例用JUnit写可能要30行,Spock 10行搞定。而且它的given-when-then结构,读起来就像在讲故事。
不过Spock有个硬伤——团队需要会Groovy。我有个项目强推Spock,结果新来的Java工程师一脸懵逼,最后又切回JUnit 5了。所以选型时一定要考虑团队的技术栈。
Spock的杀手锏:
- 数据驱动测试(where块)—— 测试数据与逻辑分离
- 交互测试(Mockito集成)—— 验证方法调用次数、参数
- 异常测试(thrown())—— 比JUnit的assertThrows更直观
class InventoryServiceSpec extends Specification {
def "扣减库存-正常场景"() {
given: "商品库存充足"
def product = new Product(stock: 10)
when: "扣减5件"
inventoryService.deduct(product, 5)
then: "库存变为5"
product.stock == 5
}
def "扣减库存-参数化测试"() {
expect: "计算正确"
calculator.add(a, b) == result
where:
a | b || result
1 | 2 || 3
3 | 4 || 7
0 | 0 || 0
}
}
说实话,Spock的where块是我最爱的功能。以前用JUnit写参数化测试,数据源要么是CSV要么是Excel,维护起来很麻烦。Spock直接在测试里定义数据表,一目了然。
断言库:AssertJ vs Hamcrest
断言库的选择其实很简单——我建议无脑上AssertJ。为什么?因为它的链式调用太舒服了。
| 特性 | AssertJ | Hamcrest |
|---|---|---|
| 语法风格 | 链式调用,流畅自然 | 静态方法,略显啰嗦 |
| 错误信息 | 详细且可读性强 | 一般,需要额外配置 |
| 集合断言 | extracting、filteredOn 等强大API | hasItem、hasSize 等基础功能 |
| 自定义断言 | 支持,但需要写模板代码 | 通过Matcher实现 |
// AssertJ 链式调用
assertThat(orders)
.hasSize(3)
.extracting(Order::getStatus)
.containsExactly("PAID", "SHIPPED", "DELIVERED");
// Hamcrest 静态方法
assertThat(orders, hasSize(3));
assertThat(orders.get(0).getStatus(), is("PAID"));
我个人习惯在微服务项目里统一用AssertJ。它的错误信息非常友好,比如assertThat(actual).isEqualTo(expected)失败时,会明确告诉你「期望是X,实际是Y」,而不是像JUnit原生断言那样只抛个AssertionError。
Mockito与PowerMock实战
Mockito是目前Java生态里最主流的Mock框架。它的设计哲学很简单——创建假对象,定义行为,验证交互。对于99%的微服务测试场景,Mockito完全够用。
Mockito核心API速记:
- mock() —— 创建Mock对象
- when().thenReturn() —— 定义返回值
- verify() —— 验证方法调用
- @InjectMocks —— 自动注入Mock对象
@ExtendWith(MockitoExtension.class)
class OrderServiceTest {
@Mock
private InventoryClient inventoryClient;
@Mock
private PaymentClient paymentClient;
@InjectMocks
private OrderService orderService;
@Test
void shouldCreateOrder() {
// 定义Mock行为
when(inventoryClient.checkStock("PROD-001", 2))
.thenReturn(true);
when(paymentClient.charge(any(PaymentRequest.class)))
.thenReturn(new PaymentResponse("SUCCESS"));
// 执行测试
Order order = orderService.createOrder("PROD-001", 2);
// 验证交互
verify(inventoryClient).checkStock("PROD-001", 2);
verify(paymentClient).charge(any(PaymentRequest.class));
}
}
避坑指南:我曾经在Mockito里滥用any()匹配器,结果某个参数类型变了,测试依然通过,上线直接报错。后来我改成用eq()精确匹配,或者用ArgumentCaptor捕获参数做断言,安全多了。
至于PowerMock,说实话,我建议你尽量别用。它通过修改字节码来Mock静态方法、私有方法、构造函数,虽然强大,但副作用也很明显——测试启动慢,兼容性问题多,而且容易掩盖设计缺陷。
PowerMock使用原则:
- 能不用就不用,优先考虑重构代码
- 如果必须用,只Mock静态方法或私有方法
- 不要和Mockito混用,容易出兼容性问题
- 升级JDK版本时,PowerMock往往是第一个出问题的
// PowerMock示例(慎用)
@RunWith(PowerMockRunner.class)
@PrepareForTest(StaticUtils.class)
public class StaticUtilsTest {
@Test
public void testStaticMethod() {
PowerMockito.mockStatic(StaticUtils.class);
when(StaticUtils.getConfig("timeout"))
.thenReturn("5000");
String result = StaticUtils.getConfig("timeout");
assertEquals("5000", result);
}
}
我记得有个遗留项目,代码里到处都是静态方法调用,没法改。当时硬着头皮上了PowerMock,结果每次跑测试都要等30秒加载字节码。后来我们花了两个迭代把静态方法改成依赖注入,测试速度从30秒降到了3秒。所以,PowerMock只是临时止痛药,重构才是根本。
选型建议总结
说了这么多,到底怎么选?我给出一个简单粗暴的建议:
- 新项目、Spring生态 → JUnit 5 + AssertJ + Mockito
- 老项目、需要分组测试 → TestNG + AssertJ + Mockito
- 团队熟悉Groovy、追求简洁 → Spock + Mockito
- 有遗留代码、无法重构 → 临时用PowerMock,但尽快重构
最后说一句,框架只是工具,测试思维才是核心。你想想看,如果代码本身设计得一团糟,再好的测试框架也救不了。所以,先把代码写好,再谈测试框架选型。