本文目录导读:

在Java开发中,规范的自测流程不仅能提升代码质量,还能大幅减少Bug修复成本,以下是一套结构化的Java自测规范流程,覆盖从单元测试到集成测试的完整环节:
自测阶段划分
| 阶段 | 目标 | 工具/框架 | 关键检查点 |
|---|---|---|---|
| 单元测试 | 验证最小代码单元(方法/类) | JUnit 5 + Mockito | 分支覆盖、边界值、异常路径 |
| 集成测试 | 验证模块间交互 | SpringBoot Test + Testcontainers | 数据库/Redis/消息队列真实交互 |
| 接口测试 | 验证RESTful接口 | MockMvc / RestAssured | 状态码、响应结构、鉴权 |
| 性能测试 | 验证核心链路性能 | JMH / JMeter | 响应时间、吞吐量、内存泄漏 |
单元测试规范(核心)
测试类命名规范
- 与待测类同名,加
Test后缀:UserService -> UserServiceTest - 方法命名:
should_预期行为_when_触发条件(如should_returnUser_when_idExists)
标准测试模板
class UserServiceTest {
@Mock
private UserRepository userRepository; // 模拟依赖
@InjectMocks
private UserService userService; // 注入被测对象
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this); // 初始化Mock
}
@Test
void should_returnUser_when_idExists() {
// 1. 准备测试数据
Long userId = 1L;
User mockUser = new User(userId, "test@example.com");
when(userRepository.findById(userId)).thenReturn(Optional.of(mockUser));
// 2. 执行测试方法
User result = userService.getUserById(userId);
// 3. 验证结果
assertThat(result.getEmail()).isEqualTo("test@example.com");
verify(userRepository, times(1)).findById(userId); // 验证调用次数
}
@Test
void should_throwException_when_idNotExists() {
Long userId = 999L;
when(userRepository.findById(userId)).thenReturn(Optional.empty());
assertThrows(ResourceNotFoundException.class,
() -> userService.getUserById(userId));
}
}
覆盖率要求(推荐目标)
| 指标 | 标准 | 说明 |
|---|---|---|
| 行覆盖率 | ≥ 80% | 每个if/for分支至少执行一次 |
| 分支覆盖率 | ≥ 70% | 所有if-else/switch-case覆盖 |
| 方法覆盖率 | 100% | 所有public方法必须测试 |
| 异常路径 | 100% | 每个catch块至少覆盖一条异常路径 |
需要覆盖的典型场景
- 正常流程:返回正确结果
- 边界值:空值、最大值、最小值、空集合
- 异常流程:异常类型、异常消息、异常抛出位置
- 幂等性:同一输入多次执行结果一致
- 并发安全(必要时):使用
ExecutorService模拟并发
集成测试规范
测试环境配置
# application-test.yml
spring:
datasource:
url: jdbc:h2:mem:testdb # 内存数据库
redis:
host: localhost
port: 6379
kafka:
bootstrap-servers: ${embedded.kafka.brokerList}
使用Testcontainers模拟真实中间件
@SpringBootTest
@ContextConfiguration
@Testcontainers
class OrderRepositoryIntegrationTest {
@Container
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:15")
.withDatabaseName("testdb");
@Container
static GenericContainer<?> redis = new GenericContainer<>("redis:7")
.withExposedPorts(6379);
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", postgres::getJdbcUrl);
registry.add("spring.redis.host", redis::getHost);
registry.add("spring.redis.port", () -> redis.getMappedPort(6379));
}
@Autowired
private OrderRepository orderRepository;
@Test
void should_persistOrder_viaRealDatabase() {
// 测试真实的数据库/Redis交互
}
}
数据库回滚策略
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.AUTO) @Transactional // 每个测试方法自动回滚
接口测试规范(Spring MVC)
使用MockMvc测试Controller
@WebMvcTest(UserController.class)
class UserControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private UserService userService;
@Test
void should_returnUser_when_validRequest() throws Exception {
String email = "test@example.com";
User mockUser = new User(1L, email);
when(userService.getUserByEmail(email)).thenReturn(mockUser);
mockMvc.perform(get("/api/users/{email}", email)
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.email").value(email));
}
@Test
void should_return404_when_userNotFound() throws Exception {
String email = "notfound@example.com";
when(userService.getUserByEmail(email)).thenThrow(new ResourceNotFoundException());
mockMvc.perform(get("/api/users/{email}", email))
.andExpect(status().isNotFound());
}
}
需要覆盖的请求场景
- 成功请求(200/201)
- 参数错误(400,缺少必填参数、格式错误)
- 身份未认证(401,未带Token)
- 权限不足(403,带Token无权限)
- 资源不存在(404)
- 请求超时/服务内部错误(500/503)
性能测试规范(核心链路)
使用JMH做微基准测试
@BenchmarkMode(Mode.Throughput)
@OutputTimeUnit(TimeUnit.SECONDS)
@Warmup(iterations = 3, time = 1)
@Measurement(iterations = 5, time = 2)
public class UserServiceBenchmark {
@State(Scope.Thread)
public static class BenchmarkState {
UserService userService = new UserService();
@Setup
public void setup() {
// 初始化数据
}
}
@Benchmark
public void benchmarkGetUser(BenchmarkState state) {
state.userService.getUserById(1L);
}
}
性能指标基线
| 操作类型 | 最大响应时间 | 目标吞吐量 |
|---|---|---|
| 简单查询(单表) | 10ms | > 1000/s |
| 复杂查询(多表join) | 50ms | > 200/s |
| 写操作(insert/update) | 30ms | > 500/s |
| 外部API调用 | 200ms | > 50/s |
自动化执行规范
构建阶段集成
<!-- pom.xml 插件配置 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<excludes>
<exclude>**/*IntegrationTest.java</exclude>
</excludes>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-failsafe-plugin</artifactId>
<configuration>
<includes>
<include>**/*IntegrationTest.java</include>
</includes>
</configuration>
</plugin>
Git Hooks + CI 流水线
# pre-push hook
mvn test # 单元测试
mvn verify # 集成测试(需要Docker)
mvn jacoco:report # 覆盖率报告
Jenkins/GitLab CI 配置:
test:
script:
- mvn clean test
- mvn verify
- mvn jacoco:report
artifacts:
paths:
- target/site/jacoco/
自测完成标准检查清单
| 检查项 | 通过标准 | 验证方式 |
|---|---|---|
| 测试用例总数 | 每个public方法至少1个 | IDE统计 |
| 行覆盖率 | ≥ 80% | JaCoCo报告 |
| 异常路径覆盖 | 100% catch块有测试 | 代码审查 + EclEmma |
| 集成测试 | 核心数据库/消息队列交互已测试 | Testcontainers执行 |
| 接口测试 | 所有REST API有正向+异常测试 | MockMvc执行 |
| 边界值 | 空值、最大值、最小值、空集合已覆盖 | 手动检查测试用例 |
| 性能基线 | 核心接口响应时间<基线值 | JMH报告 |
常见反模式与规避
| 错误做法 | 正确做法 |
|---|---|
| 只测成功路径 | 必须包含异常边界 |
| 测试中硬编码数据库连接 | 使用内存数据库或Testcontainers |
| 不验证Mock交互次数 | 使用 verify(mock, times(N)) |
| 大而全的测试方法 | 拆分为多个原子性测试 |
| 测试数据依赖顺序 | 每个测试独立准备数据(@BeforeEach + @Transactional) |
通过以上规范的五层自测体系(单元→集成→接口→性能→CI),可以确保Java代码在提交前经过充分验证,建议在项目中结合 SonarQube 进行测试质量门禁,强制执行覆盖率阈值。