作为一名Java 领域的程序员,我见过太多团队踩过 “测试” 的坑 —— 要么是测试环境和生产环境配置对不上,测了个寂寞;要么是跑个数据层测试把数据库改得一团糟,要么是 Web 接口测试写得比业务代码还复杂…… 其实 SpringBoot 早就为我们准备了一套 “测试全家桶”,今天就从 5 个核心场景入手,带大家手把手搞定 SpringBoot 测试,让你的测试代码既省心又靠谱!

目录

一、加载测试专用属性:告别 “配置混乱症”

1. 最直接的方式:用 application-test.yml

2. 更灵活的方式:@TestPropertySource

二、加载测试专用配置:让 “测试 Bean” 按需生效

1. 用 @TestConfiguration 注解:局部生效的测试配置

2. 用 @Profile ("test"):按环境隔离配置

三、Web 环境模拟测试:不用启动 Tomcat,也能测接口

1. 用 @WebMvcTest:专注测 Controller 层

2. 用 @SpringBootTest+@AutoConfigureMockMvc:全环境模拟

四、数据层测试回滚:测完数据自动清空,再也不怕脏数据

1. 用 @Transactional+@Rollback:自动回滚事务

2. 用 @DataJpaTest:专注数据层测试

五、测试用例数据设定:用 @Sql 和 @CsvSource,告别硬编码

1. 用 @Sql:执行 SQL 脚本准备数据

2. 用 @CsvSource: inline 方式准备数据(适合少量数据)

最后:SpringBoot 测试的核心原则


一、加载测试专用属性:告别 “配置混乱症”

你是不是也遇到过这种情况:本地跑测试用的数据库是test_db,生产用的是prod_db,每次切换环境都要改application.yml?其实 SpringBoot 早就想到了 “测试专属配置” 的方案,根本不用手动改!

你是不是也遇到过这种情况:本地跑测试用的数据库是test_db,生产用的是prod_db,每次切换环境都要改application.yml?其实 SpringBoot 早就想到了 “测试专属配置” 的方案,根本不用手动改!

1. 最直接的方式:用 application-test.yml

SpringBoot 默认会识别src/test/resources下的application-test.yml(或.properties),只要在测试类上加个@ActiveProfiles("test"),就能自动加载这套测试配置。比如这样:

@SpringBootTest
@ActiveProfiles("test") // 激活test环境的配置
public class UserServiceTest {
    // 测试代码里直接用测试环境的配置,比如test_db的连接信息
}

这时你的application-test.yml里就可以放心写测试专用配置:

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test_db?useSSL=false
    username: test_user
    password: test123

好处是 “环境隔离” 做得彻底,测试配置和生产配置完全分开,再也不怕手抖提交错配置。

2. 更灵活的方式:@TestPropertySource

如果只是想临时覆盖几个配置,不用单独建配置文件,直接用@TestPropertySource注解就行。比如临时改个端口或者开关某个功能:

@SpringBootTest
@TestPropertySource(properties = {
    "server.port=8081", // 测试时用8081端口
    "spring.datasource.url=jdbc:mysql://localhost:3306/temp_test_db" // 临时用另一个数据库
})
public class OrderControllerTest {
    // 测试代码
}

这种方式适合 “局部微调”,比如某个测试用例需要特殊配置,不用动全局文件。

二、加载测试专用配置:让 “测试 Bean” 按需生效

有时候我们需要测试专用的 Bean—— 比如生产环境用RedisCache,测试环境用CaffeineCache(本地缓存,更快更省心);或者生产用真实的PaymentService,测试用模拟的MockPaymentService。这时候怎么让 SpringBoot 只加载测试专用的配置呢?

1. 用 @TestConfiguration 注解:局部生效的测试配置

在测试类所在的包下,写一个测试专用的配置类,加上@TestConfiguration注解,这个配置只会在测试时生效,生产环境完全不会加载:

// 测试专用的缓存配置
@TestConfiguration
public class TestCacheConfig {
    @Bean
    public CacheManager cacheManager() {
        // 测试用Caffeine缓存,不用连Redis
        return CaffeineCacheManager.builder()
                .expireAfterWrite(5, TimeUnit.MINUTES)
                .build();
    }
}

然后在测试类里用@Import引入这个配置,或者直接让测试类扫描到这个配置(比如放在同一个包下):

@SpringBootTest
@Import(TestCacheConfig.class) // 引入测试专用配置
public class ProductServiceTest {
    @Autowired
    private CacheManager cacheManager; // 这里注入的就是CaffeineCacheManager
    
    // 测试代码
}

2. 用 @Profile ("test"):按环境隔离配置

如果测试专用配置比较多,也可以用@Profile("test")标记,配合前面的@ActiveProfiles("test")一起用。比如:

// 只有test环境才会加载这个配置
@Configuration
@Profile("test")
public class TestGlobalConfig {
    // 测试专用的Bean1
    @Bean
    public PaymentService paymentService() {
        return new MockPaymentService(); // 模拟支付服务,不用真的调用第三方
    }
    
    // 测试专用的Bean2
    @Bean
    public LogService logService() {
        return new ConsoleLogService(); // 测试时日志直接打控制台,不用写文件
    }
}

这样只要测试类激活了test环境,这些 Bean 就会自动加载,生产环境因为没有激活test profile,完全不会用到这些 Bean—— 隔离性拉满!

三、Web 环境模拟测试:不用启动 Tomcat,也能测接口

很多人测 Web 接口时,习惯直接启动整个 SpringBoot 应用(用@SpringBootTest默认不启动 Web 容器),然后用 Postman 或者 HttpClient 调接口。但这样有两个问题:一是启动慢(要加载所有 Bean),二是依赖外部环境(比如端口不能被占用)。其实 SpringBoot 提供了 “Web 环境模拟” 功能,不用启动真实 Tomcat,就能快速测接口!

1. 用 @WebMvcTest:专注测 Controller 层

如果只需要测试 Controller 层(比如参数校验、接口映射、返回值处理),用@WebMvcTest注解最合适,它只会加载 Controller 相关的 Bean(比如@Controller@RestController@RequestMapping等),其他 Bean(比如 Service、Repository)会自动忽略,启动速度超快:

// 只加载UserController相关的Bean
@WebMvcTest(UserController.class)
public class UserControllerTest {
    // 注入MockMvc,用来模拟HTTP请求
    @Autowired
    private MockMvc mockMvc;
    
    // 注入Mock的Service(因为@WebMvcTest不会加载真实的Service)
    @MockBean
    private UserService userService;
    
    @Test
    public void testGetUserById() throws Exception {
        // 1. 模拟Service的返回值
        User mockUser = new User(1L, "测试用户", 25);
        Mockito.when(userService.getUserById(1L)).thenReturn(mockUser);
        
        // 2. 模拟HTTP GET请求:/api/users/1
        mockMvc.perform(get("/api/users/1")
                .contentType(MediaType.APPLICATION_JSON))
                // 3. 断言响应结果
                .andExpect(status().isOk()) // 状态码200
                .andExpect(jsonPath("$.id").value(1)) // 响应体id=1
                .andExpect(jsonPath("$.name").value("测试用户")); // 响应体name=测试用户
    }
}

这里有两个关键点:

  • @MockBean:用来模拟 Service 层(因为@WebMvcTest不加载真实 Service),可以自定义 Service 的返回值,完全脱离数据库依赖;
  • MockMvc:用来模拟 HTTP 请求(GET/POST/PUT/DELETE),不用启动 Tomcat,直接在 JVM 内部调用 Controller,速度快到飞起。

2. 用 @SpringBootTest+@AutoConfigureMockMvc:全环境模拟

如果需要测试整个链路(Controller→Service→Repository),但又不想启动真实 Web 容器,可以用@SpringBootTest配合@AutoConfigureMockMvc

@SpringBootTest
@AutoConfigureMockMvc // 自动配置MockMvc
@ActiveProfiles("test") // 加载测试配置
public class OrderControllerFullTest {
    @Autowired
    private MockMvc mockMvc;
    
    @Autowired
    private OrderRepository orderRepository; // 真实的Repository(但用测试数据库)
    
    @Test
    public void testCreateOrder() throws Exception {
        // 1. 准备测试数据(插入测试数据库)
        Order testOrder = new Order(null, "测试订单", 99.9, LocalDateTime.now());
        
        // 2. 模拟POST请求:/api/orders
        mockMvc.perform(post("/api/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{\"name\":\"测试订单\",\"amount\":99.9}")) // 请求体
                .andExpect(status().isCreated()) // 状态码201
                .andExpect(jsonPath("$.name").value("测试订单"));
        
        // 3. 断言数据库确实插入了数据
        Assertions.assertEquals(1, orderRepository.countByOrderName("测试订单"));
    }
}

这种方式会加载整个 SpringBoot 上下文,但用MockMvc模拟 Web 请求,不用启动 Tomcat,兼顾了 “全链路测试” 和 “快速启动”。

四、数据层测试回滚:测完数据自动清空,再也不怕脏数据

数据层测试(Repository 层)最头疼的问题就是 “脏数据”—— 比如测试插入一条数据后,没删除,下次再跑测试就会因为 “唯一键冲突” 失败;或者测试删除数据后,把测试数据库的真实数据删了,导致其他测试用例报错。其实 SpringBoot 早就帮我们解决了这个问题:测试数据自动回滚

1. 用 @Transactional+@Rollback:自动回滚事务

只要在测试类或测试方法上加@Transactional注解,SpringBoot 就会在测试结束后自动回滚事务,不管测试成功还是失败,都不会留下脏数据:

@SpringBootTest
@ActiveProfiles("test") // 用测试数据库
public class UserRepositoryTest {
    @Autowired
    private UserRepository userRepository;
    
    @Test
    @Transactional // 开启事务
    @Rollback(true) // 测试结束后回滚(默认就是true,可省略)
    public void testSaveUser() {
        // 1. 插入测试数据
        User user = new User(null, "测试用户", 30);
        User savedUser = userRepository.save(user);
        
        // 2. 断言插入成功
        Assertions.assertNotNull(savedUser.getId());
        
        // 3. 测试结束后,事务自动回滚,数据库里不会留下这条数据
    }
}

这里要注意:@Transactional只对@SpringBootTest@DataJpaTest等加载了 Spring 上下文的测试有效,而且必须用支持事务的数据库(比如 MySQL、PostgreSQL,SQLite 不支持事务)。

2. 用 @DataJpaTest:专注数据层测试

如果只需要测试 Repository 层,不用加载 Service 和 Controller,可以用@DataJpaTest注解,它会自动配置 Repository 相关的 Bean(比如EntityManagerJpaRepository),并且默认开启事务回滚:

@DataJpaTest // 只加载Repository层Bean,默认事务回滚
@ActiveProfiles("test")
public class ProductRepositoryTest {
    @Autowired
    private ProductRepository productRepository;
    
    @Test
    public void testFindByProductName() {
        // 1. 插入测试数据(测试结束后自动回滚)
        Product product = new Product(null, "测试商品", 199.9);
        productRepository.save(product);
        
        // 2. 测试查询方法
        Optional<Product> foundProduct = productRepository.findByProductName("测试商品");
        
        // 3. 断言查询结果
        Assertions.assertTrue(foundProduct.isPresent());
        Assertions.assertEquals(199.9, foundProduct.get().getPrice());
    }
}

@DataJpaTest@SpringBootTest启动更快,因为它只加载数据层相关的 Bean,适合专注测试 Repository 的场景。

五、测试用例数据设定:用 @Sql 和 @CsvSource,告别硬编码

测试用例中经常需要准备测试数据 —— 比如测试 “查询用户列表”,需要先插入几条用户数据;测试 “删除订单”,需要先有一条存在的订单。如果每次都在测试方法里用repository.save()硬编码插入数据,代码会很臃肿,而且不好维护。SpringBoot 提供了两种更优雅的方式:@Sql@CsvSource

1. 用 @Sql:执行 SQL 脚本准备数据

如果测试数据比较多,可以写一个 SQL 脚本(比如test-data.sql),放在src/test/resources/sql目录下,然后用@Sql注解在测试前执行脚本:

-- src/test/resources/sql/test-user-data.sql
INSERT INTO user (id, name, age) VALUES (1, '张三', 20);
INSERT INTO user (id, name, age) VALUES (2, '李四', 25);
INSERT INTO user (id, name, age) VALUES (3, '王五', 30);

然后在测试类里用@Sql引入脚本:

@SpringBootTest
@ActiveProfiles("test")
@Sql("/sql/test-user-data.sql") // 测试前执行SQL脚本,插入测试数据
@Transactional // 测试结束后回滚,避免脏数据
public class UserRepositorySqlTest {
    @Autowired
    private UserRepository userRepository;
    
    @Test
    public void testFindByAgeGreaterThan() {
        // 直接查询脚本插入的数据
        List<User> users = userRepository.findByAgeGreaterThan(22);
        
        // 断言结果:李四(25)和王五(30),共2条
        Assertions.assertEquals(2, users.size());
    }
}

@Sql还有更灵活的用法,比如指定脚本执行时机(executionPhase):

@Test
@Sql(
    scripts = "/sql/test-user-data.sql", 
    executionPhase = Sql.ExecutionPhase.BEFORE_TEST_METHOD // 测试方法前执行(默认)
)
@Sql(
    scripts = "/sql/clean-user-data.sql", 
    executionPhase = Sql.ExecutionPhase.AFTER_TEST_METHOD // 测试方法后执行(手动清理)
)
public void testFindByName() {
    // 测试代码
}

2. 用 @CsvSource: inline 方式准备数据(适合少量数据)

如果测试数据比较少,不想写单独的 SQL 脚本,可以用@CsvSource注解,直接在测试方法里写 CSV 格式的数据,配合@ParameterizedTest实现 “多组数据测试”:

@SpringBootTest
@ActiveProfiles("test")
@Transactional
public class UserServiceParamTest {
    @Autowired
    private UserService userService;
    
    // 多组数据测试:参数1是年龄,参数2是预期是否成年
    @ParameterizedTest
    @CsvSource({
        "18, true", // 18岁→成年
        "17, false", // 17岁→未成年
        "20, true", // 20岁→成年
        "0, false" // 0岁→未成年
    })
    public void testIsAdult(Integer age, Boolean expected) {
        // 调用Service方法
        Boolean result = userService.isAdult(age);
        
        // 断言结果
        Assertions.assertEquals(expected, result);
    }
}

这种方式适合 “参数化测试”—— 比如测试一个方法在不同输入下的输出,不用写多个测试方法,一行 CSV 就是一组测试数据,代码简洁又清晰。

最后:SpringBoot 测试的核心原则

其实 SpringBoot 测试的核心不是 “炫技”,而是 “隔离” 和 “高效”:

  • 环境隔离:测试用测试配置、测试数据库,不影响生产;
  • 依赖隔离:用@MockBean模拟外部依赖(比如第三方接口、Redis),让测试不依赖外部服务;
  • 数据隔离:用事务回滚、SQL 脚本清理,避免脏数据;
  • 高效启动:用@WebMvcTest@DataJpaTest等注解只加载必要的 Bean,减少启动时间。

掌握了这 5 个场景,你就能应对 90% 以上的 SpringBoot 测试需求,写出既优雅又靠谱的测试代码。下次再有人说 “测试代码难写”,你就把这篇文章甩给他!

如果大家在实际项目中遇到特殊的测试场景(比如测试 WebSocket、定时任务),或者对某个测试注解有疑问,欢迎在评论区留言,我们一起探讨解决方案!

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐