1. 从“能跑就行”到“敢动代码”单元测试的认知升级我见过太多项目代码库像一座年久失修的古城没人敢轻易动里面的任何一块砖。每次需求变更或修复Bug都像是一次小心翼翼的考古发掘生怕碰倒了哪根承重柱引发连锁崩塌。这种恐惧的根源往往不是代码本身有多复杂而是我们缺少一套可靠的“安全网”——单元测试。而当我们终于下定决心要编织这张网时Mockito和JUnit这对黄金搭档几乎是Java开发者绕不开的选择。你可能已经无数次在简历上写下“熟悉单元测试”或者在面试中被问到“Mockito的when().thenReturn()怎么用”。但真正在项目中落地一套健壮、可维护的单元测试远不止记住几个API那么简单。它关乎我们对代码职责的理解、对依赖关系的管理以及对“测试什么”和“如何测试”的深度思考。Mockito和JUnit提供了强大的工具但工具用得好不好取决于使用者的“测试思维”。这篇文章我想和你分享的不是一份干巴巴的API手册而是我这些年从踩坑到熟练从写“为了覆盖率而写”的测试到写出“能真正赋予重构勇气”的测试的完整心路和实践总结。我们会深入JUnit 5的现代用法拆解Mockito各种打桩Stub与验证Verify场景下的精妙与陷阱并直面Spring Boot、MyBatis Plus、WebFlux等现代框架集成测试时的那些“坑”。目标很简单让你手里的Mockito和JUnit从“玩具”变成“武器”。2. JUnit 5告别旧时代的仪式感拥抱现代测试架构很多人对JUnit的印象还停留在JUnit 4的Test、Before、After上。JUnit 5又称JUnit Jupiter是一次彻底的架构革新它解决了JUnit 4的诸多历史包袱带来了更清晰的分层和更强大的扩展能力。直接上干货看看怎么用才顺手。2.1 核心注解语义化与生命周期的进化JUnit 5的注解设计更加语义化。最核心的Test注解现在来自org.junit.jupiter.api.Test不再需要指定超时或异常有独立注解。但变化最大的是生命周期注解。BeforeEach/AfterEach替代了Before/After。这个名字改得好因为它明确指出这是在“每个”测试方法执行前后运行而不是一个模糊的“之前/之后”。同理BeforeAll/AfterAll替代了BeforeClass/AfterClass并且它们修饰的方法**必须是static**的因为它们在所有测试实例创建之前执行。import org.junit.jupiter.api.*; class OrderServiceTest { // 在所有测试方法前执行一次用于重量级资源初始化如数据库连接池 BeforeAll static void initAll() { System.out.println(初始化全局测试环境...); } // 在每个测试方法前执行用于准备干净的测试上下文 BeforeEach void setUp() { System.out.println(准备测试数据...); // 通常在这里初始化被测试类实例以及Mock对象 // orderService new OrderService(mockRepository, mockPaymentClient); } Test void testCreateOrder() { // 具体的测试逻辑 } AfterEach void tearDown() { System.out.println(清理测试数据...); // 可选用于清理资源或重置Mock状态 // Mockito.reset(mockRepository); } AfterAll static void tearDownAll() { System.out.println(清理全局测试环境...); } }注意一个常见的坑是忘了将BeforeAll/AfterAll方法声明为static导致运行时异常。这是因为JUnit 5的设计哲学是类级别的初始化和清理不应该依赖于任何测试实例的状态。2.2 断言Assertions从JUnit 4到AssertJ的平滑过渡JUnit 5提供了功能更强大的内置断言库org.junit.jupiter.api.Assertions。它支持lambda表达式使得断言失败时的错误信息可以延迟计算提升性能。import static org.junit.jupiter.api.Assertions.*; Test void testBasicAssertions() { // 断言相等 assertEquals(2, 1 1); // 使用Supplier延迟生成错误信息只有断言失败时才执行 assertEquals(2, 1 1, () - 加法计算错误); // 断言为真/假 assertTrue(Hello.length() 0); assertFalse(.length() 0); // 断言为空/非空 assertNull(null); assertNotNull(new Object()); // 断言异常 Exception exception assertThrows(ArithmeticException.class, () - { int result 1 / 0; }); assertEquals(/ by zero, exception.getMessage()); // 断言超时 assertTimeout(Duration.ofSeconds(2), () - { // 执行耗时操作 Thread.sleep(1900); }); }虽然JUnit 5的断言有了很大改进但在实际项目中我强烈推荐使用AssertJ。它提供了流式APIFluent API断言语句读起来就像自然语言并且支持的对象类型极其丰富集合、字符串、Optional、日期等错误信息也异常清晰。import static org.assertj.core.api.Assertions.*; Test void testWithAssertJ() { String name Mockito; ListString list Arrays.asList(JUnit, Mockito, AssertJ); // 流式断言链式调用 assertThat(name) .isNotBlank() .startsWith(Mock) .hasSize(7); // 集合断言 assertThat(list) .hasSize(3) .contains(Mockito) .doesNotContain(TestNG) .containsExactlyInAnyOrder(AssertJ, JUnit, Mockito); // 顺序无关包含所有元素 // 异常断言 assertThatThrownBy(() - { throw new IllegalArgumentException(参数错误); }) .isInstanceOf(IllegalArgumentException.class) .hasMessageContaining(参数); }从JUnit 4的断言迁移到AssertJ几乎没有成本但带来的可读性和维护性提升是巨大的。你的测试代码会因此变得更清晰。2.3 参数化测试Parameterized Tests告别重复的测试方法测试同一个逻辑的不同输入输出时你还在复制粘贴多个Test方法吗参数化测试可以优雅地解决这个问题。import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.*; ParameterizedTest ValueSource(strings {racecar, radar, level}) // 参数来源值数组 void testPalindromes(String candidate) { assertTrue(StringUtils.isPalindrome(candidate)); } ParameterizedTest CsvSource({ // 参数来源CSV格式字符串 1, 1, 2, 2, 3, 5, 10, -5, 5 }) void testAddition(int a, int b, int expectedSum) { Calculator calculator new Calculator(); assertEquals(expectedSum, calculator.add(a, b)); } ParameterizedTest MethodSource(stringProvider) // 参数来源工厂方法 void testWithMethodSource(String argument) { assertNotNull(argument); } static StreamString stringProvider() { return Stream.of(apple, banana); }参数化测试极大地减少了样板代码让测试用例的意图更集中。对于需要测试大量边界值或等价类的情况它是不可或缺的工具。3. Mockito核心不止于when().thenReturn()的打桩艺术Mockito的核心功能是创建“模拟对象”Mock Object并定义其行为打桩Stubbing和验证其交互Verification。很多人对Mockito的认知停留在基础打桩但真正用好它需要理解其更精细的控制能力。3.1 创建Mock注解与手工的权衡创建Mock对象有两种主流方式使用Mockito.mock()方法或使用JUnit 5扩展配合Mock注解。方式一手工创建显式控制Test void testWithManualMock() { // 手工创建Mock UserRepository mockRepository Mockito.mock(UserRepository.class); UserService userService new UserService(mockRepository); // 定义行为 when(mockRepository.findById(1L)).thenReturn(Optional.of(new User(1L, Alice))); // 执行测试 User user userService.getUserById(1L); // 验证 assertThat(user.getName()).isEqualTo(Alice); }这种方式简单直接适合简单的测试类或当你需要精细控制Mock生命周期时。方式二注解驱动推荐需要添加Mockito的JUnit 5扩展依赖mockito-junit-jupiter并在测试类上使用ExtendWith(MockitoExtension.class)。import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; ExtendWith(MockitoExtension.class) // 关键启用Mockito注解支持 class UserServiceTest { Mock private UserRepository userRepository; // 将被Mock的依赖 InjectMocks private UserService userService; // 被测试类Mock会自动注入 Test void testGetUserById() { // 给定 (Given) User mockUser new User(1L, Alice); when(userRepository.findById(1L)).thenReturn(Optional.of(mockUser)); // 当 (When) User result userService.getUserById(1L); // 那么 (Then) assertThat(result).isEqualTo(mockUser); verify(userRepository).findById(1L); // 验证交互 } }使用InjectMocks时Mockito会尝试通过构造函数、setter方法或字段反射的方式将Mock标记的依赖注入到被测试对象中。这种方式让测试类结构更清晰是更现代和推荐的做法。3.2 进阶打桩应对复杂场景when().thenReturn()是最基本的但真实场景往往更复杂。1. 连续打桩Consecutive Stubbing模拟同一个方法多次调用返回不同值。Test void testConsecutiveCalls() { IteratorString mockIterator mock(Iterator.class); when(mockIterator.next()) .thenReturn(first) // 第一次调用返回 .thenReturn(second) // 第二次调用返回 .thenThrow(new RuntimeException(no more elements)); // 第三次及以后抛出异常 assertEquals(first, mockIterator.next()); assertEquals(second, mockIterator.next()); assertThrows(RuntimeException.class, mockIterator::next); }2. 回调打桩Answer当返回结果需要根据输入参数动态计算时使用。Test void testAnswer() { CalculatorService mockService mock(CalculatorService.class); when(mockService.add(anyInt(), anyInt())).thenAnswer(invocation - { int a invocation.getArgument(0); int b invocation.getArgument(1); return a b; // 模拟实际计算 }); assertEquals(5, mockService.add(2, 3)); }这在模拟一些具有复杂逻辑的接口时非常有用但需谨慎使用避免在测试中引入过多业务逻辑。3. 对void方法打桩使用doNothing(),doThrow(),doAnswer(),doCallRealMethod()系列方法。Test void testVoidMethod() { EmailSender mockSender mock(EmailSender.class); // 默认void方法什么都不做但我们可以让它抛出异常 doThrow(new RuntimeException(网络错误)).when(mockSender).send(anyString()); assertThrows(RuntimeException.class, () - mockSender.send(testemail.com)); }4. 参数匹配器Argument Matchersany(),eq(),isNull(),startsWith()等匹配器让验证更灵活。Test void testArgumentMatchers() { ListString mockList mock(List.class); when(mockList.get(anyInt())).thenReturn(element); when(mockList.add(startsWith(test))).thenReturn(true); assertEquals(element, mockList.get(999)); // 任何索引都返回element assertTrue(mockList.add(test123)); // 验证时也可使用 verify(mockList).get(eq(999)); // 验证get(999)被调用 verify(mockList).add(startsWith(test)); }注意一旦在打桩或验证中使用了一个参数匹配器所有参数都必须使用匹配器。不能混用具体值和匹配器eq()匹配器除外它用于指定具体值。3.3 验证Verification确认交互的发生与未发生验证是Mockito的另一半灵魂用于断言Mock对象是否按预期被调用。基础验证verify(mockRepository).findById(1L); // 验证被调用1次默认 verify(mockRepository, times(1)).findById(1L); // 明确验证调用次数 verify(mockRepository, atLeastOnce()).findAll(); // 至少一次 verify(mockRepository, atMost(5)).save(any()); // 最多5次 verify(mockRepository, never()).deleteById(999L); // 从未被调用验证调用顺序InOrder inOrder inOrder(mockServiceA, mockServiceB); inOrder.verify(mockServiceA).doFirst(); inOrder.verify(mockServiceB).doSecond();这在测试有严格顺序要求的流程时非常有用。超时验证// 验证在100毫秒内某个方法被调用了一次 verify(mockService, timeout(100)).asyncMethod();适用于测试异步交互。重置Mock谨慎使用Mockito.reset(mockObject)可以重置一个Mock对象的所有打桩和交互记录。但在测试中过度使用reset是坏味道通常意味着单个测试方法承担了过多职责应该考虑拆分成多个测试。每个测试方法应该独立且专注于一个场景。4. 集成测试实战应对Spring Boot、数据库与Web层的挑战单元测试的理想状态是隔离的、快速的。但有些时候我们确实需要启动部分容器或连接真实数据库来进行“集成单元测试”或“切片测试”。Spring Boot Test提供了强大的支持但也带来了复杂的配置问题。4.1 Spring Boot切片测试WebMvcTest, DataJpaTest, JsonTestSpring Boot的切片测试允许你只加载应用程序的特定“切片”而不是整个应用上下文从而让测试更快、更专注。WebMvcTest控制器层测试这是你搜索“vue单元测试报错”或“spring webtestclient 单元测试”时可能真正需要的。它只加载Web层相关的Bean如Controller, ControllerAdvice, Filter不会加载Service、Repository等。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) // 只加载UserController及其依赖的Web组件 class UserControllerTest { Autowired private MockMvc mockMvc; // 注入MockMvc用于模拟HTTP请求 MockBean // 特别注意这里用MockBean不是Mock。它会将Mock对象注册到Spring测试上下文。 private UserService userService; Test void getUserById_ShouldReturnUser() throws Exception { User mockUser new User(1L, Alice); when(userService.getUserById(1L)).thenReturn(mockUser); mockMvc.perform(get(/api/users/1)) // 发起GET请求 .andExpect(status().isOk()) // 断言状态码200 .andExpect(jsonPath($.name).value(Alice)); // 使用JsonPath断言JSON响应体 } }踩坑点在切片测试中所有被控制器依赖的Service、Repository等都必须使用MockBean来提供Mock实现。如果使用MockSpring上下文会因为找不到对应的Bean而启动失败。DataJpaTest数据层测试用于测试JPA Repository。它会配置一个内存数据库如H2并自动扫描Entity类和Spring Data JPA Repository。import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; import org.springframework.beans.factory.annotation.Autowired; DataJpaTest // 只加载JPA相关的配置 class UserRepositoryTest { Autowired private TestEntityManager entityManager; // 用于持久化测试数据 Autowired private UserRepository userRepository; Test void whenFindByName_thenReturnUser() { // 给定 User user new User(Alice, aliceexample.com); entityManager.persist(user); entityManager.flush(); // 当 User found userRepository.findByName(Alice); // 那么 assertThat(found.getEmail()).isEqualTo(user.getEmail()); } }它默认会回滚事务确保测试隔离。JsonTestJSON序列化/反序列化测试专门测试Jackson的ObjectMapper配置是否正确。import org.springframework.boot.test.autoconfigure.json.JsonTest; import org.springframework.beans.factory.annotation.Autowired; import com.fasterxml.jackson.databind.ObjectMapper; JsonTest class UserJsonTest { Autowired private ObjectMapper objectMapper; // 注入Spring Boot配置好的ObjectMapper Test void testSerialization() throws Exception { User user new User(1L, Alice); String json objectMapper.writeValueAsString(user); assertThat(json).contains(\name\:\Alice\); } Test void testDeserialization() throws Exception { String json {\id\:1,\name\:\Alice\}; User user objectMapper.readValue(json, User.class); assertThat(user.getName()).isEqualTo(Alice); } }4.2 处理配置文件TestPropertySource与ActiveProfiles“单元测试如何配置test下的配置文件”是一个高频问题。Spring Boot Test提供了多种方式。1. 使用TestPropertySource注解直接在测试类上指定属性文件或内联属性。SpringBootTest TestPropertySource(locations classpath:application-test.properties) // 指定文件 // 或者 TestPropertySource(properties { spring.datasource.urljdbc:h2:mem:testdb, my.custom.propertytest-value // 内联属性 }) class MyIntegrationTest { // ... }2. 使用ActiveProfiles激活Profile在src/test/resources/下创建application-test.yml然后在测试类上使用ActiveProfiles(test)Spring Boot就会加载该Profile的配置。# src/test/resources/application-test.yml spring: datasource: url: jdbc:h2:mem:testdb;MODEMySQL driver-class-name: org.h2.Driver jpa: hibernate: ddl-auto: create-drop show-sql: trueSpringBootTest ActiveProfiles(test) class MyIntegrationTest { // ... }3. 配置文件优先级测试环境下的配置加载优先级从高到低测试类上的TestPropertySource测试类上的ActiveProfiles指定的application-{profile}.properties/yml测试资源目录下的application.properties/yml主资源目录下的application.properties/yml理解这个顺序可以帮你解决大部分配置冲突问题。4.3 应对MyBatis Plus与WebFluxMyBatis Plus单元测试对于“mybatisplus 单元测试 mybatisutils.getmybatismapperproxy”这类问题核心在于如何正确注入Mapper。在Spring Boot Test中使用MybatisPlusTest注解如果可用或标准的SpringBootTest即可。import org.springframework.boot.test.context.SpringBootTest; import org.springframework.beans.factory.annotation.Autowired; import com.baomidou.mybatisplus.test.autoconfigure.MybatisPlusTest; // 如果存在 // 方式1使用MyBatis Plus专用切片如果依赖提供了 // MybatisPlusTest // 方式2使用SpringBootTest但通过配置缩小范围 SpringBootTest(properties spring.main.web-application-typenone) // 非Web应用 AutoConfigureTestDatabase(replace AutoConfigureTestDatabase.Replace.NONE) // 使用真实数据源配置 class UserMapperTest { Autowired private UserMapper userMapper; // 直接注入MyBatis Plus的Mapper Test void testSelectById() { User user userMapper.selectById(1L); assertThat(user).isNotNull(); // 注意这里需要测试数据库中有对应数据或者使用Sql脚本预先准备数据 } }关键点如果Mapper中使用了MyBatisUtils.getMybatisMapperProxy这类动态代理确保你的测试Spring上下文能够正确扫描到Mapper接口。通常MapperScan注解配置在启动类上测试时需要能加载到相同的配置。Spring WebFlux单元测试对于响应式编程Spring提供了WebTestClient来测试Controller或RestController。import org.springframework.boot.test.autoconfigure.web.reactive.WebFluxTest; import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.test.web.reactive.server.WebTestClient; WebFluxTest(UserController.class) // 类似于WebMvcTest用于WebFlux class UserControllerWebFluxTest { Autowired private WebTestClient webTestClient; // 注入WebTestClient MockBean private UserService userService; Test void getUserById_ShouldReturnUser() { User mockUser new User(1L, Alice); when(userService.getUserById(1L)).thenReturn(Mono.just(mockUser)); webTestClient.get() .uri(/api/users/1) .exchange() // 发起请求 .expectStatus().isOk() .expectBody(User.class) .isEqualTo(mockUser); } }WebTestClient是异步的测试方法本身不需要是响应式的它会阻塞直到收到响应或超时。5. 测试策略与设计模式写出可维护的测试代码工具用得再熟如果测试代码本身难以维护最终也会被抛弃。好的测试应该是文档是设计工具而不仅仅是验证工具。5.1 测试命名传达意图而非细节糟糕的命名test1(),testSaveUser()。 良好的命名应遵循被测试方法名_测试场景_期望结果的约定即Given-When-Then的语义化。// 好例子 Test void getUserById_WhenUserExists_ThenReturnUser() { ... } Test void getUserById_WhenUserNotFound_ThenThrowNotFoundException() { ... } Test void calculateDiscount_WhenOrderAmountExceeds1000_ThenApply10PercentDiscount() { ... }这样的命名即使不看测试体也能立刻明白这个测试在验证什么。5.2 测试结构Given-When-Then模式这是组织测试逻辑的黄金法则让测试清晰可读。Given (准备)设置测试前提包括创建测试数据、Mock对象行为打桩。When (执行)调用被测试的方法。Then (断言)验证结果状态断言和交互行为断言。Test void transferMoney_WhenSufficientBalance_ThenSucceedsAndUpdatesBalances() { // Given Account fromAccount new Account(A123, 1000.00); Account toAccount new Account(B456, 500.00); when(accountRepository.findById(A123)).thenReturn(Optional.of(fromAccount)); when(accountRepository.findById(B456)).thenReturn(Optional.of(toAccount)); // When transferService.transfer(A123, B456, 200.00); // Then assertThat(fromAccount.getBalance()).isEqualTo(800.00); // 状态断言 assertThat(toAccount.getBalance()).isEqualTo(700.00); verify(accountRepository).save(fromAccount); // 行为断言 verify(accountRepository).save(toAccount); }5.3 测试替身Test Double的选择Mock vs. Stub vs. SpyMockito主要提供Mock但理解这些概念有助于正确选择工具。Mock我们最常用的。验证对象间的交互是否被调用、调用次数、参数。我们关心的是“行为”。Stub为方法调用提供预设的返回值。我们关心的是“状态”。Mockito的when().thenReturn()就是在创建Stub。一个对象可以同时是Mock和Stub。Spy部分真实对象。你可以监视一个真实对象验证其部分方法的调用同时其他方法保持真实行为。使用需谨慎因为它会调用真实方法。ListString realList new ArrayList(); ListString spyList Mockito.spy(realList); // 创建Spy // 可以打桩Spy的某个方法 doReturn(fake).when(spyList).get(0); // 其他未打桩的方法会调用真实对象的方法 spyList.add(real element); assertThat(spyList.size()).isEqualTo(1); // 真实行为何时用Spy当你需要测试一个庞大、复杂的类且只想替换其中一两个难以构造依赖的方法时可以考虑Spy。但多数情况下更好的设计是将这个类拆分成更小、职责更单一的类然后对它们进行Mock。5.4 常见陷阱与最佳实践不要Mock你不拥有的类型第三方库如HttpClient、DateTime或语言核心类如List。Mock这些类型会让测试变得脆弱且你无法控制它们的未来行为变化。应该使用适配器模式包装它们然后Mock你自己的适配器接口。避免过度指定Over-specification只验证与被测试功能直接相关的交互。验证那些不重要的、实现细节的调用会导致测试脆弱一旦重构就失败。例如测试业务逻辑时不应该去验证内部调用了哪个具体的日志方法。每个测试一个断言One Assert Per Test这是一个指导原则并非铁律。目标是让一个测试只失败于一个原因。如果一个测试方法里有多个不相关的断言当它失败时你需要花时间定位是哪个条件没满足。相关的断言如验证一个对象的多个属性可以放在一起。使用Nested组织复杂测试类当一个类的测试场景很多时使用JUnit 5的Nested注解进行逻辑分组让测试类结构更清晰。class UserServiceTest { Nested class GetUserById { Test void whenUserExists_thenReturnUser() { ... } Test void whenUserNotFound_thenThrowException() { ... } } Nested class CreateUser { Test void withValidData_thenSucceed() { ... } Test void withDuplicateEmail_thenFail() { ... } } }保持测试独立与可重复每个测试不应该依赖其他测试的运行状态也不应该依赖外部环境如数据库特定ID、当前时间。使用BeforeEach准备干净状态使用Mock隔离外部依赖。6. 高级话题静态方法、构造方法Mock与测试覆盖率当遗留代码或第三方库充斥着静态方法时纯Mockito就力不从心了。这时需要用到PowerMock或Mockito 3.4的Inline Mock Maker。6.1 Mock静态方法Mockito 3.4从Mockito 3.4.0开始实验性地支持Mock静态方法。需要添加mockito-inline依赖。// pom.xml // dependency // groupIdorg.mockito/groupId // artifactIdmockito-inline/artifactId // scopetest/scope // /dependency Test void testStaticMethod() { try (MockedStaticUtilityClass mockedStatic Mockito.mockStatic(UtilityClass.class)) { // 打桩静态方法 mockedStatic.when(UtilityClass::staticMethod).thenReturn(mocked value); // 执行测试 String result UtilityClass.staticMethod(); // 断言 assertThat(result).isEqualTo(mocked value); // 验证静态方法被调用 mockedStatic.verify(UtilityClass::staticMethod); } // try-with-resources块结束静态Mock自动关闭 }重要静态Mock必须在try-with-resources或BeforeEach/AfterEach中管理其生命周期确保在测试结束后被关闭避免影响其他测试。6.2 关于测试覆盖率追求高测试覆盖率是好事但它只是一个指标而非目标。100%的覆盖率不等于100%的质量。覆盖率的陷阱在于覆盖了但没断言执行了代码路径但没有验证结果是否正确。覆盖了错误代码代码逻辑本身就是错的测试也按照错的逻辑通过了。忽视了复杂场景只覆盖了Happy Path没覆盖边界条件、异常流程。工具如JaCoCo可以帮助你发现未被测试的代码但更重要的是设计出能发现缺陷的测试用例。应该更关注代码的复杂度和分支覆盖而不仅仅是行覆盖。最后我想说的是单元测试是一种技能更是一种纪律。它初期会拖慢开发速度但长期来看它是保证软件质量、支撑持续重构、提升开发信心的最有效投资。从今天开始尝试为你修改的每一段代码都加上一个测试你会发现你对代码的理解会更深你也会更有勇气去做出那些早就该做的改进。Mockito和JUnit是你的得力助手但真正强大的是你通过它们所构建起来的、对自身代码的深刻理解和掌控力。