Spring Boot TDD Workflow
TDD guidance for Spring Boot services with 80%+ coverage (unit + integration).
When to Use
-
New features or endpoints
-
Bug fixes or refactors
-
Adding data access logic or security rules
Workflow
-
Write tests first (they should fail)
-
Implement minimal code to pass
-
Refactor with tests green
-
Enforce coverage (JaCoCo)
Unit Tests (JUnit 5 + Mockito)
@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
Web Layer Tests (MockMvc)
@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 @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 @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
-
Use reusable containers for Postgres/Redis to mirror production
-
Wire via @DynamicPropertySource to inject JDBC URLs into Spring context
Coverage (JaCoCo)
Maven snippet:
<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(...)
Test Data Builders
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
-
Maven: mvn -T 4 test or mvn verify
-
Gradle: ./gradlew test jacocoTestReport
Remember: Keep tests fast, isolated, and deterministic. Test behavior, not implementation details.