Backend Testing Strategy
Build a risk-based Java and Spring test suite across services, controllers, security, MyBatis, real PostgreSQL, transactions, idempotency, and concurrency.
Learning objectives
- Select the cheapest test that can detect a specific backend risk
- Test database, security, rollback, idempotency, and concurrency behavior with production-like boundaries
What it is
A backend testing strategy assigns each important risk to a test boundary: pure unit tests for business decisions, MVC/security slices for HTTP mapping, PostgreSQL integration tests for SQL and transactions, and a small number of end-to-end flows for deployed wiring.
Why it matters
A large test count can still miss the failures that cost money: invalid authorization, incorrect SQL dialect, non-rollback, duplicate requests, time-dependent behavior, or incompatible responses. Strategy links tests to consequences rather than maximizing mocks or coverage percentages.
How it works
| Test | Real components | Best at |
|---|---|---|
| Unit | One class plus controlled collaborators | Rules, branches, error translation |
| MVC slice | Spring MVC mapping/validation/filter subset | HTTP contract and security decisions |
| Database integration | MyBatis + real PostgreSQL | SQL, result maps, constraints, transaction behavior |
| Application integration | Full Spring context + external test dependencies | Wiring and use-case flow |
| End to end | Built/deployed application through public interface | Critical user journey and deployment confidence |
Simple example
class VoucherRulesTest {
private final Clock clock = Clock.fixed(
Instant.parse("2026-07-16T01:00:00Z"), ZoneOffset.UTC);
private final VoucherRules rules = new VoucherRules(clock);
@ParameterizedTest
@CsvSource({
"ISSUED, 2026-07-17T00:00:00Z, true",
"REDEEMED, 2026-07-17T00:00:00Z, false",
"ISSUED, 2026-07-15T00:00:00Z, false"
})
void evaluatesRedemption(String status, Instant expiresAt, boolean expected) {
Voucher voucher = voucher(status, expiresAt);
assertThat(rules.canRedeem(voucher)).isEqualTo(expected);
}
}The fixed Clock removes wall-clock nondeterminism. Parameterization documents a decision table without hiding failures in a loop.
Backend example
@WebMvcTest(TaskController.class)
@Import(SecurityConfig.class)
class TaskControllerTest {
@Autowired MockMvc mvc;
@MockitoBean TaskService taskService;
@Test
@WithMockUser(username = "42", roles = "USER")
void rejectsBlankTitle() throws Exception {
mvc.perform(post("/v1/tasks")
.with(csrf())
.contentType(MediaType.APPLICATION_JSON)
.content("""{"title":""}"""))
.andExpect(status().isBadRequest())
.andExpect(content().contentTypeCompatibleWith("application/problem+json"))
.andExpect(jsonPath("$.errors[0].field").value("title"));
}
}@Testcontainers
@SpringBootTest
class TaskMapperIT {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres =
new PostgreSQLContainer<>("postgres:16-alpine");
@Autowired TaskMapper taskMapper;
@Test
void returnsStableKeysetOrder() {
// Arrange committed rows with equal timestamps and distinct IDs.
// Query two pages and assert no duplicate or missing ID.
}
}Production example
- 1
Risk inventory
List money, security, privacy, compatibility, availability, migration, and operational failure modes.
- 2
Boundary
Choose the smallest boundary that contains the real behavior: do not mock PostgreSQL when verifying a PostgreSQL constraint.
- 3
Determinism
Control time, randomness, IDs, network behavior, data cleanup, and parallel execution.
- 4
Failure evidence
Assert durable rows, affected counts, emitted events, status and error shape—not only that no exception was thrown.
| Critical scenario | Evidence |
|---|---|
| Transaction rollback | No partial rows after an injected failure |
| Idempotency | Same key/request returns same outcome; changed request conflicts |
| Concurrent redemption | One business result and valid audit history |
| Authorization | Anonymous, wrong role, wrong owner, and correct owner paths |
| Migration | Upgrade from realistic previous schema and restart safely |
Common mistakes
- Mocking every collaborator and testing implementation calls
- Using H2 to claim PostgreSQL SQL correctness
- Sharing mutable fixtures across tests
- Depending on test order or current time
- Testing only happy paths
- Using Thread.sleep as synchronization in concurrency tests
- Making CI require production credentials
Best practices
- Name tests as behavior and outcome
- Use AssertJ failure messages for domain intent
- Keep fixtures small and explicit
- Use Testcontainers for dialect and constraint behavior
- Test authorization denial and information disclosure
- Make retries, timeouts, and concurrency bounded in tests
- Quarantine nothing silently; fix or delete flaky tests
Trade-offs
| Technique | Benefit | Cost |
|---|---|---|
| Mock | Fast, isolates collaborator behavior | Can encode implementation and unrealistic assumptions |
| Fake | Reusable deterministic behavior | May drift from the real system |
| Container | Real database or broker semantics | Startup and resource cost |
| Full context | Wiring confidence | Slower and less precise failures |
Interview questions
- What should be unit tested in a Spring service?
- When would you use @WebMvcTest versus @SpringBootTest?
- Why use Testcontainers?
- How do you prove rollback?
- How would you test idempotency under concurrency?
- What makes a test deterministic?
Hands-on task
- 1
Matrix
Create a Task API test matrix for happy, validation, authentication, authorization, conflict, rollback, idempotency, and concurrency paths.
- 2
Implement
Add a pure service test, MockMvc security test, and PostgreSQL mapper/transaction test.
- 3
Gate
Run fast tests on every change and integrate container tests into CI with bounded time and diagnostic output.
References
- Spring: Testing Spring Boot Applications — https://docs.spring.io/spring-boot/reference/testing/spring-boot-applications.html
- JUnit: JUnit 5.14.1 Documentation — https://docs.junit.org/5.14.1/overview.html
- Testcontainers: Testcontainers for Java — JUnit 5 Quickstart — https://java.testcontainers.org/quickstart/junit_5_quickstart/
Retrieval check
Before you continue
- Explain the main idea in your own words without rereading the page.
- Name one production problem this knowledge helps prevent.
- Describe where you would apply it in a Spring Boot project.
Source metadata
Primary references checked on 2026-07-16. Version scope is stated at the top of this page.