数据库测试
Laravel 用
RefreshDatabase+sqlite :memory:跑数据库测试;Spring Boot 用@DataJpaTest(JPA 层)或 Testcontainers(真实数据库)。核心原则一样:测试绝不碰开发/生产库。
方案一:@DataJpaTest(JPA 层测试,内存 H2)
只加载 JPA 相关 Bean,测试 Repository,秒级完成:
package com.example.blog.post;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.*;
@DataJpaTest
class PostRepositoryTest {
@Autowired
private PostRepository postRepository;
@Test
void 按状态查询() {
// 保存测试数据
Post p1 = new Post();
p1.setTitle("发布帖");
p1.setStatus(1);
postRepository.save(p1);
List<Post> published = postRepository.findByStatus(1);
assertEquals(1, published.size());
}
@Test
void 找不到返回空Optional() {
Optional<Post> post = postRepository.findById(999L);
assertTrue(post.isEmpty());
}
}TIP
@DataJpaTest 自动用 H2 + create-drop 建表,每个测试结束自动回滚(事务包裹),数据不会残留。对应 Laravel 的 RefreshDatabase 效果。
测试数据怎么造
JPA 测试没有 Laravel 的 Factory,两个选择:
1. 测试里手动 new + save(上面的例子,简单直接)
2. 用共享 @BeforeEach 造数据:
@BeforeEach
void setUp() {
postRepository.saveAll(List.of(
post("帖子1", 1),
post("帖子2", 1),
post("帖子3", 0)));
}TIP
想要 Laravel 那样的 Factory(自动生成一堆测试数据),可用 easy-random 或 instancio 库随机填充实体字段,但大多数测试手写几条就够。
方案二:@MybatisTest(MyBatis 层测试)
MyBatis 对应 @DataJpaTest 的注解是 @MybatisTest(mybatis-spring-boot-starter-test):
@MybatisTest
class PostMapperTest {
@Autowired
private PostMapper postMapper;
@Test
void 插入后能查到() {
Post post = new Post();
post.setTitle("测试");
post.setStatus(1);
postMapper.insert(post);
assertNotNull(post.getId()); // 主键回填
Post found = postMapper.findById(post.getId());
assertEquals("测试", found.getTitle());
}
}TIP
@MybatisTest 需要 @AutoConfigureTestDatabase(replace = Replace.NONE) 配合实际数据源,或直接用 H2 + 内存库。它和 @DataJpaTest 一样是「切片测试」:只加载数据访问层。
方案三:Testcontainers(真实数据库,最可靠)
H2 兼容 MySQL 大多数语法,但总有差异(JSON 类型、窗口函数、特殊索引)。要 100% 贴近生产,用 Testcontainers 起真实 MySQL 容器:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-testcontainers</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>mysql</artifactId>
<scope>test</scope>
</dependency>@Testcontainers
@SpringBootTest
class PostRepositoryIntegrationTest {
@Container
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0");
@DynamicPropertySource
static void datasourceProps(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
}
@Autowired
private PostRepository postRepository;
@Test
void 真实MySQL上CRUD() {
Post post = new Post();
post.setTitle("真实库测试");
postRepository.save(post);
assertNotNull(post.getId());
}
}TIP
@Testcontainers + @Container 自动管理容器生命周期(起库、测试、销毁)。配合 Flyway 自动迁移,每个测试跑在「生产一致」的 schema 上,是最可信的数据库测试。
覆盖普通数据访问测试的常见断言
// 事务回滚生效
@Test
void 事务异常回滚() {
assertThrows(RuntimeException.class, () -> {
service.transfer(1L, 2L, BigDecimal.TEN); // 内部抛异常
});
// 断言两个账户余额都没变
}
// 分页
@Test
void 分页查询() {
Page<Post> page = postRepository.findAll(
PageRequest.of(0, 2));
assertEquals(2, page.getContent().size());
}方案选型总结
| 场景 | 用什么 |
|---|---|
| JPA 层逻辑(方法名查询、@Query) | @DataJpaTest + H2(快) |
| MyBatis 层逻辑(XML SQL) | @MybatisTest + H2 |
| 复杂 SQL、数据库专属语法 | Testcontainers 真实库 |
| 完整请求链路(Controller→DB) | @SpringBootTest + H2 或 Testcontainers |
| 只测 Controller,不碰库 | @WebMvcTest + Mock 掉 Service |
三句话原则
- 测试库隔离:H2 内存库 或 Testcontainers,绝不碰开发/生产库
- 先切片:能
@DataJpaTest就别@SpringBootTest(快 10 倍) - Flyway 建 schema:测试环境也用迁移建表,和生产一致
下一节:模拟对象(Mockito)。