springboot-tdd

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Spring Boot TDD Workflow

Spring Boot TDD工作流程

TDD guidance for Spring Boot services with 80%+ coverage (unit + integration).
针对Spring Boot服务的TDD指南,实现80%以上的测试覆盖率(单元测试+集成测试)。

When to Use

适用场景

  • New features or endpoints
  • Bug fixes or refactors
  • Adding data access logic or security rules
  • 新增功能或接口
  • Bug修复或代码重构
  • 添加数据访问逻辑或安全规则

Workflow

工作流程

  1. Write tests first (they should fail)
  2. Implement minimal code to pass
  3. Refactor with tests green
  4. Enforce coverage (JaCoCo)
  1. 先编写测试(此时测试应失败)
  2. 编写最少代码使测试通过
  3. 在测试全部通过的情况下进行代码重构
  4. 用JaCoCo强制保证测试覆盖率

Unit Tests (JUnit 5 + Mockito)

单元测试(JUnit 5 + Mockito)

java
@ExtendWith(MockitoExtension.class)
class MarketServiceTest {
  @Mock MarketRepository repo;
  @InjectMocks MarketService service;

  @Test
  void createsMarket() {
    CreateMarketRequest req = new CreateMarketRequest("name", "desc", Instant.now(), List.of("cat"));
    when(repo.save(any())).thenAnswer(inv -> inv.getArgument(0));

    Market result = service.create(req);

    assertThat(result.name()).isEqualTo("name");
    verify(repo).save(any());
  }
}
Patterns:
  • Arrange-Act-Assert
  • Avoid partial mocks; prefer explicit stubbing
  • Use
    @ParameterizedTest
    for variants
java
@ExtendWith(MockitoExtension.class)
class MarketServiceTest {
  @Mock MarketRepository repo;
  @InjectMocks MarketService service;

  @Test
  void createsMarket() {
    CreateMarketRequest req = new CreateMarketRequest("name", "desc", Instant.now(), List.of("cat"));
    when(repo.save(any())).thenAnswer(inv -> inv.getArgument(0));

    Market result = service.create(req);

    assertThat(result.name()).isEqualTo("name");
    verify(repo).save(any());
  }
}
常用模式:
  • 准备-执行-断言(Arrange-Act-Assert)
  • 避免部分模拟,优先使用显式存根
  • @ParameterizedTest
    处理多场景测试

Web Layer Tests (MockMvc)

Web层测试(MockMvc)

java
@WebMvcTest(MarketController.class)
class MarketControllerTest {
  @Autowired MockMvc mockMvc;
  @MockBean MarketService marketService;

  @Test
  void returnsMarkets() throws Exception {
    when(marketService.list(any())).thenReturn(Page.empty());

    mockMvc.perform(get("/api/markets"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.content").isArray());
  }
}
java
@WebMvcTest(MarketController.class)
class MarketControllerTest {
  @Autowired MockMvc mockMvc;
  @MockBean MarketService marketService;

  @Test
  void returnsMarkets() throws Exception {
    when(marketService.list(any())).thenReturn(Page.empty());

    mockMvc.perform(get("/api/markets"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.content").isArray());
  }
}

Integration Tests (SpringBootTest)

集成测试(SpringBootTest)

java
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class MarketIntegrationTest {
  @Autowired MockMvc mockMvc;

  @Test
  void createsMarket() throws Exception {
    mockMvc.perform(post("/api/markets")
        .contentType(MediaType.APPLICATION_JSON)
        .content("""
          {"name":"Test","description":"Desc","endDate":"2030-01-01T00:00:00Z","categories":["general"]}
        """))
      .andExpect(status().isCreated());
  }
}
java
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class MarketIntegrationTest {
  @Autowired MockMvc mockMvc;

  @Test
  void createsMarket() throws Exception {
    mockMvc.perform(post("/api/markets")
        .contentType(MediaType.APPLICATION_JSON)
        .content("""
          {"name":"Test","description":"Desc","endDate":"2030-01-01T00:00:00Z","categories":["general"]}
        """))
      .andExpect(status().isCreated());
  }
}

Persistence Tests (DataJpaTest)

持久层测试(DataJpaTest)

java
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(TestContainersConfig.class)
class MarketRepositoryTest {
  @Autowired MarketRepository repo;

  @Test
  void savesAndFinds() {
    MarketEntity entity = new MarketEntity();
    entity.setName("Test");
    repo.save(entity);

    Optional<MarketEntity> found = repo.findByName("Test");
    assertThat(found).isPresent();
  }
}
java
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Import(TestContainersConfig.class)
class MarketRepositoryTest {
  @Autowired MarketRepository repo;

  @Test
  void savesAndFinds() {
    MarketEntity entity = new MarketEntity();
    entity.setName("Test");
    repo.save(entity);

    Optional<MarketEntity> found = repo.findByName("Test");
    assertThat(found).isPresent();
  }
}

Testcontainers

Testcontainers

  • Use reusable containers for Postgres/Redis to mirror production
  • Wire via
    @DynamicPropertySource
    to inject JDBC URLs into Spring context
  • 使用可复用的容器(如Postgres/Redis)模拟生产环境
  • 通过
    @DynamicPropertySource
    将JDBC URL注入Spring上下文

Coverage (JaCoCo)

测试覆盖率(JaCoCo)

Maven snippet:
xml
<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <version>0.8.14</version>
  <executions>
    <execution>
      <goals><goal>prepare-agent</goal></goals>
    </execution>
    <execution>
      <id>report</id>
      <phase>verify</phase>
      <goals><goal>report</goal></goals>
    </execution>
  </executions>
</plugin>
Maven配置片段:
xml
<plugin>
  <groupId>org.jacoco</groupId>
  <artifactId>jacoco-maven-plugin</artifactId>
  <version>0.8.14</version>
  <executions>
    <execution>
      <goals><goal>prepare-agent</goal></goals>
    </execution>
    <execution>
      <id>report</id>
      <phase>verify</phase>
      <goals><goal>report</goal></goals>
    </execution>
  </executions>
</plugin>

Assertions

断言工具

  • Prefer AssertJ (
    assertThat
    ) for readability
  • For JSON responses, use
    jsonPath
  • For exceptions:
    assertThatThrownBy(...)
  • 优先使用AssertJ(
    assertThat
    )以提升可读性
  • 处理JSON响应时使用
    jsonPath
  • 捕获异常:
    assertThatThrownBy(...)

Test Data Builders

测试数据构建器

java
class MarketBuilder {
  private String name = "Test";
  MarketBuilder withName(String name) { this.name = name; return this; }
  Market build() { return new Market(null, name, MarketStatus.ACTIVE); }
}
java
class MarketBuilder {
  private String name = "Test";
  MarketBuilder withName(String name) { this.name = name; return this; }
  Market build() { return new Market(null, name, MarketStatus.ACTIVE); }
}

CI Commands

CI执行命令

  • Maven:
    mvn -T 4 test
    or
    mvn verify
  • Gradle:
    ./gradlew test jacocoTestReport
Remember: Keep tests fast, isolated, and deterministic. Test behavior, not implementation details.
  • Maven:
    mvn -T 4 test
    mvn verify
  • Gradle:
    ./gradlew test jacocoTestReport
注意:保持测试快速、独立且可预测。测试行为而非实现细节。