exit lab
Backend Engineering
Level 7·testing·guide·intermediate

Backend Testing Strategy

Build a risk-based Java and Spring test suite across services, controllers, security, MyBatis, real PostgreSQL, transactions, idempotency, and concurrency.

60 minutes Spring Boot 3.5 test support, JUnit Jupiter, Testcontainers for Java Updated 2026-07-16
JUnit 5AssertJMockitoMockMvcTestcontainersPostgreSQL

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

TestReal componentsBest at
UnitOne class plus controlled collaboratorsRules, branches, error translation
MVC sliceSpring MVC mapping/validation/filter subsetHTTP contract and security decisions
Database integrationMyBatis + real PostgreSQLSQL, result maps, constraints, transaction behavior
Application integrationFull Spring context + external test dependenciesWiring and use-case flow
End to endBuilt/deployed application through public interfaceCritical user journey and deployment confidence

Simple example

VoucherRulesTest.java
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

TaskControllerTest.java
@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"));
    }
}
TaskMapperIT.java
@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. 1

    Risk inventory

    List money, security, privacy, compatibility, availability, migration, and operational failure modes.

  2. 2

    Boundary

    Choose the smallest boundary that contains the real behavior: do not mock PostgreSQL when verifying a PostgreSQL constraint.

  3. 3

    Determinism

    Control time, randomness, IDs, network behavior, data cleanup, and parallel execution.

  4. 4

    Failure evidence

    Assert durable rows, affected counts, emitted events, status and error shape—not only that no exception was thrown.

Critical scenarioEvidence
Transaction rollbackNo partial rows after an injected failure
IdempotencySame key/request returns same outcome; changed request conflicts
Concurrent redemptionOne business result and valid audit history
AuthorizationAnonymous, wrong role, wrong owner, and correct owner paths
MigrationUpgrade 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

TechniqueBenefitCost
MockFast, isolates collaborator behaviorCan encode implementation and unrealistic assumptions
FakeReusable deterministic behaviorMay drift from the real system
ContainerReal database or broker semanticsStartup and resource cost
Full contextWiring confidenceSlower 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. 1

    Matrix

    Create a Task API test matrix for happy, validation, authentication, authorization, conflict, rollback, idempotency, and concurrency paths.

  2. 2

    Implement

    Add a pure service test, MockMvc security test, and PostgreSQL mapper/transaction test.

  3. 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.