1. 项目概述:为什么我们需要一个完整的Java测试框架教程?
如果你正在学习Java,或者已经是一名Java开发者,那么“测试”这个词对你来说一定不陌生。从最初在main方法里写几个System.out.println来验证逻辑,到后来听说JUnit、Mockito这些名词,再到项目里真正开始写测试代码,这个过程里踩过的坑,我猜你我都经历过。网上关于JUnit或Mockito的零散教程很多,但往往只讲一个点,比如“如何使用@Test注解”,或者“如何用Mockito.when打桩”。当你真正想构建一个健壮、可维护的测试体系时,会发现这些知识点是割裂的。你需要知道JUnit如何组织测试生命周期,Mockito如何模拟外部依赖,以及它们如何协同工作来应对复杂的业务场景。这正是我写这篇完整教程的初衷:不是简单地罗列API,而是帮你建立起从单元测试到集成测试的完整知识框架和实操能力。无论你是刚入门的新手,还是想系统梳理测试知识的中级开发者,这篇文章都将带你从JUnit的基础断言,一直走到Mockito的高级Mock技巧,并理解它们背后的设计思想。
2. 测试框架核心价值与生态全景
在深入代码之前,我们必须先搞清楚,为什么测试框架如此重要,以及Java测试生态里都有哪些“玩家”。
2.1 测试的价值:不仅仅是找Bug
很多人把测试等同于“找Bug”,这其实低估了它的价值。在我看来,一套好的测试至少带来四个核心价值:
- 保障正确性:这是最基本的功能,确保代码在各种边界条件下都能按预期工作。
- 支撑重构:没有测试覆盖的代码,重构时如同在黑暗中拆解炸弹,心惊胆战。有了测试,你可以放心地优化代码结构,测试会告诉你是否引入了回归错误。
- 活文档:测试用例是对代码功能和行为最准确的描述。相比可能过时的文档,运行通过的测试更能说明代码“现在”是如何工作的。
- 设计驱动:编写可测试的代码,往往会迫使你思考更好的设计,比如遵循单一职责原则、依赖注入等,这能显著提升代码质量。
2.2 Java测试框架生态一览
Java测试领域远不止JUnit和Mockito。理解整个生态,能帮助你在不同场景下选择最合适的工具。
- 测试运行器与框架:
- JUnit 4/5:事实上的Java单元测试标准。JUnit 5是当前主流,它模块化设计,提供了更丰富的扩展能力。
- TestNG:另一个强大的测试框架,灵感来源于JUnit,但在配置灵活性、依赖测试、参数化测试等方面有独特优势,常用于更复杂的集成测试场景。
- Mock与Stub框架:
- Mockito:最流行的Java Mock框架,以其简洁流畅的API著称。它主要用于在测试中创建“模拟对象”(Mock),替代真实的、不易构造或具有副作用的依赖(如数据库、网络服务)。
- EasyMock:Mockito的前辈,API相对陈旧,但仍有部分老项目在使用。
- PowerMock:一个“增强器”,它可以Mock静态方法、构造方法、final类等Mockito本身无法处理的情况。但它的使用会破坏测试的可读性,通常被视为最后的手段。
- 断言库:
- JUnit Assertions:JUnit内置的断言方法,如
assertEquals,assertTrue。 - Hamcrest:提供了一套可读性更强的“匹配器”(Matcher)语法,例如
assertThat(actual, is(equalTo(expected)))。 - AssertJ:目前最受推崇的流式断言库,链式调用让断言读起来像自然语言,例如
assertThat(actual).isEqualTo(expected).isNotNull()。
- JUnit Assertions:JUnit内置的断言方法,如
- 行为验证与BDD:
- Mockito Verify:用于验证模拟对象上的方法是否被以特定的方式调用过。
- BDDMockito:Mockito提供的BDD(行为驱动开发)风格API,使用
given,when,then等关键词,让测试逻辑更清晰。
- 集成与端到端测试:
- Spring Boot Test:为Spring Boot应用提供全方位的测试支持,包括Mock MVC(测试Web层)、数据JPA测试、切片测试等。
- Selenium:用于Web应用的浏览器自动化测试。
- RestAssured:用于测试RESTful API的DSL(领域特定语言),语法非常简洁。
在这个生态中,JUnit + Mockito + AssertJ构成了现代Java单元测试的“黄金三角”。本教程将聚焦于这个核心组合,因为它们能解决你日常开发中80%以上的测试需求。
3. JUnit 5深度解析:现代单元测试的基石
JUnit 5是一个彻底的革新,它由三个主要子模块组成:JUnit Platform(在JVM上启动测试框架的基础)、JUnit Jupiter(编写测试的新编程模型和扩展模型)、JUnit Vintage(用于兼容运行JUnit 3/4的测试)。我们主要关注JUnit Jupiter。
3.1 核心注解与测试生命周期
理解JUnit 5,首先要理解它的注解和测试执行的生命周期。
import org.junit.jupiter.api.*; import static org.junit.jupiter.api.Assertions.*; class CalculatorTest { private Calculator calculator; @BeforeAll static void initAll() { // 在整个测试类开始前执行一次,必须是静态方法 System.out.println("初始化全局资源,如数据库连接池"); } @BeforeEach void init() { // 在每个@Test方法执行前都执行一次 calculator = new Calculator(); System.out.println("初始化Calculator实例"); } @Test @DisplayName("测试加法运算") void testAddition() { int result = calculator.add(2, 3); assertEquals(5, result, "2 + 3 应该等于 5"); // 第三个参数是可选的消息,仅在断言失败时输出,便于定位问题 } @Test @Disabled("暂时跳过这个测试,因为功能尚未实现") void testFeatureNotImplemented() { // 这个测试不会运行 } @Test @DisplayName("测试除以零应抛出异常") void testDivideByZero() { // 使用assertThrows来验证是否抛出了预期的异常 ArithmeticException exception = assertThrows(ArithmeticException.class, () -> calculator.divide(1, 0)); // 还可以进一步断言异常信息 assertEquals("/ by zero", exception.getMessage()); } @AfterEach void tearDown() { // 在每个@Test方法执行后都执行一次,用于清理资源 System.out.println("清理测试现场"); // 例如,如果calculator持有需要关闭的资源,可以在这里关闭 } @AfterAll static void tearDownAll() { // 在整个测试类结束后执行一次,必须是静态方法 System.out.println("释放全局资源"); } }生命周期顺序:@BeforeAll-> (@BeforeEach->@Test->@AfterEach) * N ->@AfterAll。
注意:
@BeforeAll和@AfterAll注解的方法必须是static的,因为它们在所有测试实例创建之前/之后运行。而@BeforeEach和@AfterEach通常不是静态的,因为它们作用于每个具体的测试实例。
3.2 参数化测试:用数据驱动测试
当同一个测试逻辑需要针对多组输入数据进行验证时,手动写多个@Test方法非常繁琐。JUnit 5的参数化测试完美解决了这个问题。
import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.*; import java.util.stream.Stream; import static org.junit.jupiter.api.Assertions.*; class ParameterizedCalculatorTest { private Calculator calculator = new Calculator(); // 1. 使用 @ValueSource 提供一组字面值 @ParameterizedTest @ValueSource(ints = {1, 2, 3}) void testSquareWithValueSource(int number) { int squared = calculator.square(number); assertEquals(number * number, squared); } // 2. 使用 @CsvSource 提供CSV格式的数据,适合多参数 @ParameterizedTest @CsvSource({ "1, 2, 3", "0, 5, 5", "-3, 3, 0" }) @DisplayName("CSV参数化加法测试") void testAdditionWithCsv(int a, int b, int expectedSum) { assertEquals(expectedSum, calculator.add(a, b)); } // 3. 使用 @MethodSource 引用一个返回Stream<Arguments>的工厂方法 @ParameterizedTest @MethodSource("provideStringsForLengthTest") void testStringLength(String input, int expectedLength) { assertEquals(expectedLength, input.length()); } static Stream<Arguments> provideStringsForLengthTest() { return Stream.of( Arguments.of("hello", 5), Arguments.of("JUnit", 5), Arguments.of("", 0) ); } // 4. 使用 @EnumSource 枚举所有枚举值 @ParameterizedTest @EnumSource(TimeUnit.class) // 使用java.util.concurrent.TimeUnit枚举 void testEnumSource(TimeUnit timeUnit) { assertNotNull(timeUnit); } }参数化测试极大地减少了重复代码,让测试意图更清晰。@CsvFileSource还可以从外部的CSV文件加载测试数据,非常适合数据量大的场景。
3.3 断言进阶:从JUnit到AssertJ
虽然JUnit自带的断言够用,但AssertJ提供了更强大、更优雅的断言方式。
import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.*; class AssertJTest { @Test void testWithAssertJ() { String name = "Java"; List<String> list = Arrays.asList("JUnit", "Mockito", "AssertJ"); // 链式调用,读起来像句子 assertThat(name) .isEqualTo("Java") .startsWith("J") .endsWith("a") .hasSize(4); // 对集合的断言非常强大 assertThat(list) .hasSize(3) .contains("Mockito") .doesNotContain("EasyMock") .containsExactly("JUnit", "Mockito", "AssertJ") // 顺序也必须一致 .containsExactlyInAnyOrder("Mockito", "AssertJ", "JUnit"); // 顺序无关 // 异常断言 assertThatThrownBy(() -> { throw new IllegalArgumentException("wrong arg"); }) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining("wrong arg"); // 也可以使用 catchThrowable 方式 Throwable thrown = catchThrowable(() -> { throw new RuntimeException("error"); }); assertThat(thrown).isInstanceOf(RuntimeException.class); } }为什么推荐AssertJ?
- 可读性极佳:链式调用让断言逻辑一目了然。
- 错误信息清晰:断言失败时,AssertJ提供的错误信息通常比JUnit更详细,能直接告诉你哪里不对。
- 功能丰富:对集合、Map、日期、文件、Optional等类型提供了大量开箱即用的断言方法。
- IDE友好:大多数IDE能提供完善的代码补全。
在实际项目中,我几乎总是用AssertJ替代JUnit的标准断言。你只需要在pom.xml或build.gradle中引入AssertJ依赖即可。
4. Mockito核心实战:优雅地模拟与验证
单元测试的核心是“隔离”。我们要测试的通常是某个类(如ServiceA)的一个方法,但这个方法内部可能依赖了RepositoryB、HttpClientC等外部组件。Mockito的作用就是创建这些依赖的“替身”,让我们可以控制替身的行为,从而将测试焦点完全集中在ServiceA的逻辑上。
4.1 Mock与Stub:创建并配置模拟对象
Mock(模拟):创建一个对象的虚假实例,你可以记录这个实例上的交互(比如某个方法被调用了多少次)。Stub(打桩):为模拟对象上的方法调用预设返回值或异常。
在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 java.util.List; import static org.mockito.Mockito.*; import static org.assertj.core.api.Assertions.*; // 使用Mockito的JUnit 5扩展,自动初始化@Mock和@InjectMocks注解的字段 @ExtendWith(MockitoExtension.class) class OrderServiceTest { @Mock private InventoryRepository inventoryRepository; // 被模拟的依赖 @Mock private PaymentGateway paymentGateway; // 另一个被模拟的依赖 @InjectMocks private OrderService orderService; // 被测试的主体,其依赖会自动注入模拟对象 @Test void placeOrder_Success() { // 1. 准备测试数据 String itemId = "item-123"; int quantity = 2; Order order = new Order(itemId, quantity); // 2. 打桩(Stubbing):定义模拟对象的行为 // 当inventoryRepository.checkStock被调用,且参数是"item-123"和2时,返回true when(inventoryRepository.checkStock(itemId, quantity)).thenReturn(true); // 当paymentGateway.charge被调用,且参数是order时,返回一个成功的Transaction Transaction successTx = new Transaction("tx-001", "SUCCESS"); when(paymentGateway.charge(order)).thenReturn(successTx); // 3. 执行被测方法 OrderResult result = orderService.placeOrder(order); // 4. 断言结果 assertThat(result.isSuccess()).isTrue(); assertThat(result.getTransactionId()).isEqualTo("tx-001"); // 5. 验证交互(Verification):确保模拟对象的方法被以预期的方式调用 verify(inventoryRepository).checkStock(itemId, quantity); // 验证checkStock被调用了一次 verify(paymentGateway).charge(order); // 验证charge被调用了一次 // 可以验证调用次数:verify(paymentGateway, times(1)).charge(order); // 可以验证从未被调用:verify(paymentGateway, never()).refund(any()); } @Test void placeOrder_OutOfStock() { String itemId = "item-456"; Order order = new Order(itemId, 10); // 打桩:模拟库存不足 when(inventoryRepository.checkStock(itemId, 10)).thenReturn(false); OrderResult result = orderService.placeOrder(order); assertThat(result.isSuccess()).isFalse(); assertThat(result.getMessage()).contains("out of stock"); // 验证:因为库存不足,所以支付网关的charge方法不应该被调用 verify(paymentGateway, never()).charge(any(Order.class)); // 但库存检查应该被调用过 verify(inventoryRepository).checkStock(itemId, 10); } }关键点解析:
@ExtendWith(MockitoExtension.class):这是JUnit 5的扩展机制,它会在每个测试方法执行前,自动初始化被@Mock和@InjectMocks注解的字段。这是最推荐的方式,代码简洁。@Mock:标注一个需要被模拟的依赖字段。@InjectMocks:标注被测试的类实例。Mockito会尝试通过构造器注入、setter注入或字段注入的方式,将@Mock字段注入到这个实例中。when(...).thenReturn(...):这是最常用的打桩语法。它定义了“当某个方法以特定参数被调用时,返回什么值”。verify(mock):用于验证模拟对象上的方法是否被调用,以及调用的次数、参数等。这是Mock的核心功能之一,确保被测对象与依赖的正确协作。
4.2 参数匹配器:让验证更灵活
有时我们并不关心方法调用的具体参数值,或者参数是某个复杂对象。这时可以使用Mockito的参数匹配器(Argument Matchers)。
@Test void testWithArgumentMatchers() { List<String> mockedList = mock(List.class); // 使用 any() 匹配任何参数 when(mockedList.get(anyInt())).thenReturn("element"); // 使用 eq() 匹配特定值(当与其他匹配器混用时,所有参数都必须使用匹配器) when(mockedList.set(eq(0), anyString())).thenReturn("old-value"); // 使用 startsWith() 等匹配字符串 when(mockedList.add(startsWith("test"))).thenReturn(true); // 执行 mockedList.get(999); // 返回 "element" mockedList.add("test123"); // 返回 true // 验证 verify(mockedList).get(anyInt()); verify(mockedList).add(startsWith("test")); // 注意:如果验证时使用匹配器,则所有参数都必须使用匹配器,不能混用字面值。 // verify(mockedList).add("test123"); // 这样写也可以,但上面用匹配器更灵活。 }重要规则:如果在一个方法调用中使用了一个参数匹配器(如anyInt()),那么该方法的所有参数都必须使用匹配器,不能混合使用字面值和匹配器。唯一的例外是eq()匹配器,它用来明确指定一个具体的值。
4.3 模拟void方法与异常抛出
对于返回void的方法,或者需要模拟抛出异常的场景,Mockito也有对应的语法。
@Test void testVoidMethodAndException() { EmailService emailService = mock(EmailService.class); UserService userService = new UserService(emailService); String userEmail = "user@example.com"; // 1. 模拟void方法:使用 doNothing(), doThrow(), doReturn(), doAnswer() // 默认情况下,Mockito对void方法什么也不做(即doNothing) // 我们可以让它抛出一个异常 doThrow(new RuntimeException("SMTP server down")) .when(emailService) .sendWelcomeEmail(userEmail); // 执行,预期会抛出异常 assertThatThrownBy(() -> userService.register(userEmail)) .isInstanceOf(RuntimeException.class) .hasMessageContaining("SMTP server down"); // 2. 模拟非void方法抛出异常(除了用when().thenThrow(),也可以用doThrow) DataRepository repo = mock(DataRepository.class); doThrow(new IllegalArgumentException("Invalid ID")) .when(repo) .findById(-1L); assertThatThrownBy(() -> repo.findById(-1L)) .isInstanceOf(IllegalArgumentException.class); }doThrow(...).when(mock).methodCall(...)的语法结构可能一开始看起来有点反直觉,但它非常强大,可以统一处理void和非void方法的异常模拟。
4.4 Spy:部分模拟真实对象
Mock创建的是一个完全空白的、所有方法默认返回空值或0的“假”对象。而Spy(间谍)则不同,它包装了一个真实的对象。除非你显式地打桩(Stub),否则Spy会调用真实对象的方法。这在测试遗留代码或只想模拟某个大对象的少数几个方法时非常有用。
@Test void testSpy() { List<String> realList = new ArrayList<>(); List<String> spiedList = spy(realList); // 创建真实List的Spy // 可以调用真实方法 spiedList.add("real element"); assertThat(spiedList).containsExactly("real element"); // 也可以对特定方法打桩 doReturn(100).when(spiedList).size(); // 无论真实size是多少,现在都返回100 assertThat(spiedList.size()).isEqualTo(100); // 返回打桩值 assertThat(realList.size()).isEqualTo(1); // 真实对象不受影响 // 注意:对Spy对象使用 when(...).thenReturn(...) 要小心! // when(spiedList.get(0)).thenReturn("stubbed"); // 这行代码会先调用真实的spiedList.get(0),可能抛IndexOutOfBoundsException! // 正确做法是使用 doReturn: doReturn("stubbed").when(spiedList).get(0); }警告:对Spy对象使用
when(spy.method()).thenReturn(...)语法时,spy.method()会被真实地调用一次(为了获取当前返回值,以便后续打桩)。如果这个方法有副作用或者当前状态不满足调用条件(比如列表为空时调用get(0)),就会导致问题。因此,对Spy打桩时,强烈推荐使用doReturn、doThrow、doAnswer等doXxx系列方法,它们不会调用真实方法。
5. 高级技巧与集成实践
掌握了基础,我们来看看如何应对更复杂的测试场景,以及如何将JUnit和Mockito集成到真实的项目(比如Spring Boot)中。
5.1 测试私有方法?不,请重新思考设计
一个常见的问题是:“如何用JUnit测试一个类的私有方法?” 我的建议是:尽量不要直接测试私有方法。
私有方法是实现细节,应该通过对公有方法的测试来间接覆盖。如果你觉得必须单独测试一个私有方法,这通常是一个信号:这个私有方法可能承担了过多的职责,应该被提取到一个独立的、可公开测试的类(遵循单一职责原则)。或者,你可以考虑将方法的访问权限改为package-private(默认)并在同一个包内进行测试,但这仍是次优选择。
5.2 处理静态方法、final类与构造方法
Mockito本身无法Mock静态方法、final类/方法、构造方法以及私有方法。这是Mockito的一个设计选择,旨在鼓励良好的、可测试的代码设计(比如依赖注入,而非静态工具类)。
如果确实需要处理遗留代码中的静态方法,可以考虑使用PowerMock。但请注意,PowerMock通过修改字节码来实现,可能会带来一些奇怪的问题,并且让你的测试变得笨重。将其作为最后的手段。
// 示例:使用PowerMockito Mock静态方法(不推荐作为首选) @RunWith(PowerMockRunner.class) // 特殊的Runner @PrepareForTest({StaticUtility.class}) // 告诉PowerMock准备修改哪个类 public class PowerMockTest { @Test public void testStaticMethod() { // 1. Mock静态类 PowerMockito.mockStatic(StaticUtility.class); // 2. 打桩 PowerMockito.when(StaticUtility.generateId()).thenReturn("fixed-id"); // 3. 执行和断言 assertThat(MyService.doSomething()).isEqualTo("fixed-id"); } }更好的做法是,将静态方法调用包装在一个实例方法中,或者通过依赖注入一个提供了该功能的接口,这样就能用普通的Mockito来模拟了。
5.3 与Spring Boot集成测试
在现代Spring Boot应用中,测试通常分为几个层次:
- 单元测试:使用
@ExtendWith(MockitoExtension.class),只测试单个Service或Component,所有依赖都用Mockito模拟。快速、隔离。 - 切片测试:使用Spring Boot Test提供的注解如
@WebMvcTest(只加载Web层)、@DataJpaTest(只加载JPA相关组件)、@JsonTest(只测试JSON序列化)等。针对特定层次,加载部分Spring上下文。 - 集成测试:使用
@SpringBootTest,加载完整的Spring应用上下文,可能会启动内嵌的Web服务器、数据库等。全面但缓慢。
单元测试示例(纯Mockito):
@ExtendWith(MockitoExtension.class) class UserServiceUnitTest { @Mock private UserRepository userRepository; @InjectMocks private UserService userService; // ... 测试逻辑,不启动Spring }Web层切片测试示例:
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.web.servlet.MockMvc; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; @WebMvcTest(UserController.class) // 只加载Web层相关的Bean,特别是UserController class UserControllerTest { @Autowired private MockMvc mockMvc; // 用于模拟HTTP请求 @MockBean // Spring特有的注解,用于在ApplicationContext中注入一个Mockito mock private UserService userService; @Test void getUserById() throws Exception { User mockUser = new User(1L, "testUser"); when(userService.getUserById(1L)).thenReturn(mockUser); mockMvc.perform(get("/api/users/1")) .andExpect(status().isOk()) .andExpect(jsonPath("$.username").value("testUser")); verify(userService).getUserById(1L); } }@MockBean是Spring Boot Test提供的注解,它会在Spring的测试ApplicationContext中注册一个Mockito mock,并替换掉同类型的真实Bean。这让你可以在集成度更高的测试中,仍然对特定依赖进行模拟。
6. 测试策略、常见陷阱与最佳实践
写测试不仅仅是技术活,更是一种工程实践。以下是我从多年项目中总结出的经验和教训。
6.1 测试金字塔与FIRST原则
测试金字塔是一个重要的指导模型:
- 底层(最多):单元测试。快速、隔离、成本低。应占测试总量的70%以上。
- 中层:集成测试。验证模块间的协作。占比约20%。
- 顶层(最少):端到端(E2E)测试。模拟真实用户操作,验证完整流程。占比约10%。
FIRST原则是写好单元测试的准则:
- Fast(快速):测试应该能快速运行(毫秒级),这样你才愿意频繁运行它们。
- Independent(独立):测试之间不应该有依赖,可以以任何顺序运行。
- Repeatable(可重复):在任何环境(开发机、CI服务器)下都能得到相同的结果。
- Self-Validating(自验证):测试结果应该是布尔值(成功/失败),不需要人工检查日志或输出。
- Timely(及时):理想情况下,测试应该在产品代码之前或同时编写(TDD)。
6.2 常见陷阱与避坑指南
过度验证(Over-verification):
// 不好:验证了太多不重要的交互 verify(mockedList).add("a"); verify(mockedList).clear(); verify(mockedList).size(); verifyNoMoreInteractions(mockedList); // 过于严格,任何未预料到的调用都会失败建议:只验证那些对被测方法正确性至关重要的交互。过度验证会让测试变得脆弱,一旦实现细节改变(比如多调用了一个日志方法),测试就会失败,即使功能是正确的。
测试实现而非行为:
// 不好:测试了内部调用顺序,这是实现细节 InOrder inOrder = inOrder(dependencyA, dependencyB); inOrder.verify(dependencyA).step1(); inOrder.verify(dependencyB).step2();建议:测试应该关注输出结果和对外部的关键影响(如状态改变、消息发送),而不是内部的调用顺序。除非调用顺序本身就是业务契约的一部分。
脆弱的测试(Brittle Tests):
- 硬编码数据:在测试中硬编码当前日期、数据库ID等,导致测试很快过期。
- 依赖未隔离的外部服务:测试调用真实的数据库或第三方API,导致测试不稳定、速度慢。建议:使用测试数据构建器(Test Data Builder)、随机生成器(如Java Faker)来创建数据。使用Mockito彻底隔离外部依赖。
Mockito的
when()陷阱:// 错误:这行代码会立即调用 someMethod(),可能导致NPE或非预期行为 when(mock.someMethod()).thenReturn(someValue); // 如果someMethod()返回void,这行代码甚至无法编译正确:对于void方法,总是使用
doReturn().when()或doThrow().when()等doXxx语法。忽略异常测试:只测试“阳光路径”(happy path),忽略了边界条件和异常情况。建议:使用
assertThrows或AssertJ的assertThatThrownBy来专门测试异常抛出场景。
6.3 测试命名与组织
好的测试名应该清晰地表达在什么条件下,执行什么操作,期望什么结果。一个流行的命名约定是:methodName_StateUnderTest_ExpectedBehavior。
@Test void withdrawMoney_AccountHasSufficientFunds_AmountIsDeducted() { ... } @Test void withdrawMoney_AccountHasInsufficientFunds_ThrowsInsufficientFundsException() { ... } @Test void placeOrder_ItemOutOfStock_OrderIsRejected() { ... }在IDE中,当测试失败时,这样的名称能让你一眼看出是哪个场景出了问题。
6.4 持续集成中的测试
将测试集成到CI/CD流水线中是保证代码质量的关键。通常的步骤是:
- 代码提交触发构建。
- 运行所有单元测试:必须全部通过,且速度要快(几分钟内)。
- 运行集成测试:可能涉及数据库、消息队列等,可以放在单独的阶段。
- 运行端到端测试:通常更耗时,可能安排在夜间或合并到主分支前进行。
使用Maven或Gradle,你可以轻松地通过命令(如mvn test、mvn verify或gradle test、gradle integrationTest)来运行不同范围的测试。利用@Tag注解(JUnit 5)或Maven的profile/Gradle的task可以灵活控制哪些测试在哪个阶段运行。
最后,记住测试的终极目标不是追求100%的覆盖率,而是通过可执行的规范来提升信心、促进设计和加速开发。从编写一个简单的@Test开始,逐步应用Mockito处理依赖,你的代码质量和开发体验将会得到质的飞跃。