Springboot+Junit5微服务单元测试编写实践

6,286 阅读3分钟

单元测试的重要性不言而喻,下文简要说明 JUnit5 测试中常用注解与实践方式。常用 IDE 都可自动生成基础测试类。


一、单元测试命名

可参考 7种流行的单元测试命名约定

推荐采用 should...when... 命名方式,如 should_returnUser_when_queryValidId()
但需注意:方法名不宜过长,影响可读性。


二、常用注解

1. 类级注解

  • @SpringBootTest:启动完整 Spring Boot 应用上下文,用于集成测试。一般用于 Controller 层或需要完整环境的测试。
  • @ExtendWith(SpringExtension.class):JUnit5 替代 JUnit4 的 @RunWith(SpringJUnit4ClassRunner.class),用于加载 Spring 上下文。
  • @ContextConfiguration:手动指定加载的配置类或 XML 文件,常与 @ExtendWith(SpringExtension.class) 一起使用。
  • @ExtendWith(MockitoExtension.class):用于纯 Mockito 测试,不依赖 Spring 容器。

2. Mock 相关注解

  • @Mock@InjectMocks@Spy:Mockito 提供。
    • @Mock:创建 Mock 对象。
    • @InjectMocks:创建对象实例并注入 Mock/Spy。
    • @Spy:部分 Mock,会调用真实方法。
  • @MockBean@SpyBean:Spring Boot 封装版本。
    • @MockBean 会将 Mock 对象注册进 Spring 容器。
    • 区别同上,@SpyBean 为部分 Mock。

3. 方法级注解

  • @Test:JUnit 标准测试方法注解。

4. 常用断言与验证

  • Mockito.when(...).thenReturn(...)
  • Mockito.doNothing().when(...)
  • Assertions.assertTrue(...)
  • Assertions.assertNotNull(...)
  • Assertions.assertThrows(...)
  • Mockito.verify(bean, times(n)).method()

三、测试方法结构

推荐遵循 Given - When - Then 三段式结构:

  1. Given:准备数据或 Mock 行为;
  2. When:执行待测方法;
  3. Then:验证结果、断言逻辑。

四、数据库层单元测试

@MybatisPlusTest 仅加载 MyBatis Plus 相关 Bean,可避免 @SpringBootTest 启动完整环境。
来自 MyBatis Plus 3.4.0+ 的 mybatis-plus-boot-starter-test 模块。

示例

@MybatisPlusTest
@MapperScan({"com.example.demo.mapper"})
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // 避免默认使用 H2,可改为 MySQL
@Sql(scripts = {"classpath:db/schema.sql", "classpath:db/data.sql"})
@Import(DataSourceConfig.class)
@ContextConfiguration(classes = {JunitDemo.class})
class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    void should_select5Items_when_afterInit() {
        List<User> users = userMapper.selectList(null);
        Assertions.assertEquals(5, users.size());
    }
}

数据源配置

public class DataSourceConfig {

    @Bean
    public DruidDataSource dataSource() {
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setUrl("jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1");
        return dataSource;
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        return new DataSourceTransactionManager(dataSource());
    }
}

⚠️ 不加 @Configuration 是为了防止被全局扫描影响其他测试。

如需观察数据库变化,可启动 H2 Server:

public class H2ServerTest {

    private static Server h2Server;

    public static void startH2Server() throws Exception {
        h2Server = Server.createTcpServer("-tcp", "-tcpAllowOthers", "-tcpPort", "9092").start();
        System.out.println("H2 server started on port 9092");
    }

    public static void stopH2Server() {
        if (h2Server != null) {
            h2Server.stop();
            System.out.println("H2 server stopped");
        }
    }
}

五、Service 层单元测试

@ExtendWith(MockitoExtension.class)
class UserServiceTest {

    @Mock
    private UserMapper userMapper;

    @InjectMocks
    private UserService userService;

    @Test
    void should_returnResult_when_queryById() {
        // Given
        long userId = 1;
        User mockUser = new User();
        mockUser.setId(userId);
        mockUser.setName("li");
        Mockito.doReturn(mockUser).when(userMapper).selectById(userId);

        // When
        User user = userService.getOnlyUser(userId);

        // Then
        Mockito.verify(userMapper, Mockito.times(1)).selectById(userId);
        Assertions.assertEquals("li", user.getName());
    }

    @Test
    void should_throwException_when_ageLessThan18() {
        Mockito.doNothing().when(userMapper).insert(Mockito.isA(User.class));
        Assertions.assertThrows(BusinessException.class,
                () -> userService.insertUser(buildUserAgeLessThan18()));
    }
}

部分场景,如果service要连接真实数据库,又不想启动整个项目,如下代码可以参考

@ExtendWith(SpringExtension.class)
//@ComponentScan(basePackages = {"com.example.demo.service"})
@ContextConfiguration(classes = {JunitDemo.class, DemoConfig.class})  // 告诉 Spring:测试环境要加载哪些配置类
@MybatisPlusTest
@MapperScan({"com.example.demo.mapper"})
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) // 避免默认使用 H2,可改为 MySQL
@Sql(scripts = {"classpath:db/schema.sql", "classpath:db/data.sql"}) // 需要执行的初始化Sql
@Import(DataSourceConfig.class) // 把某些类手动注册进当前 Spring 容器。
class UserServiceTest {

    @SpyBean //默认执行真实默认,部分方法可mock
    private UserMapper userMapper;

    @Autowired
    private UserService userService;

}

六、Controller 层集成测试

@SpringBootTest(classes = JunitDemo.class)
@AutoConfigureMockMvc
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Test
    void should_returnUser_when_query() throws Exception {
        mockMvc.perform(
                MockMvcRequestBuilders.get("/user/getOnlyUser?userId=1")
                        .contentType(MediaType.APPLICATION_JSON))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("name").value("xiaoming"))
                .andExpect(MockMvcResultMatchers.jsonPath("age").value("18"));
    }
}

七、注意事项

  • 确保 JUnit 版本统一(全部使用 JUnit5 依赖)。
  • 若遇到 unable to find a @SpringBootConfiguration,可手动在 @SpringBootTest 中指定 classes
  • @MybatisPlusTest 默认使用内存数据库,如需连接真实库,请关闭自动替换数据源。

八、依赖示例

<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter-test</artifactId>
  <version>3.4.3</version>
  <scope>test</scope>
</dependency>

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-test</artifactId>
  <scope>test</scope>
</dependency>