Testing

The starter

spring-boot-starter-test is in every generated project and brings JUnit 5, AssertJ, Mockito, JSONassert and the Spring test support.

./mvnw test

Plain unit tests

Constructor injection means most classes need no Spring at all.

class GreetingServiceTest {

    private final GreetingService service = new GreetingService();

    @Test
    void greetsByName() {
        assertThat(service.greet("Ann")).isEqualTo("Hello, Ann!");
    }
}

Fast, and where the bulk of your tests should live.

Slice tests

Start only the part of the context you need.

AnnotationLoads
@WebMvcTestcontrollers, JSON mapping, no services
@DataJpaTestJPA, repositories, in-memory database
@RestClientTestHTTP clients with a mock server
@JsonTestserialization only
@WebMvcTest(BookController.class)
class BookControllerTest {

    @Autowired RestTestClient client;
    @MockitoBean BookService service;             // replaces the bean in the context

    @Test
    void returnsBook() {
        given(service.byId(1)).willReturn(new Book(1, "Dune"));

        client.get().uri("/api/books/1")
              .exchange()
              .expectStatus().isOk()
              .expectBody().jsonPath("$.title").isEqualTo("Dune");
    }
}
New in Spring Boot 4

RestTestClient is the one fluent test client for both mock and running servers. @MockBean is gone — use @MockitoBean (and @MockitoSpyBean).

Repository tests

@DataJpaTest
class BookRepositoryTest {

    @Autowired BookRepository repo;

    @Test
    void findsByAuthor() {
        repo.save(new Book("Dune", "Herbert", 1965));

        assertThat(repo.findByAuthor("Herbert")).hasSize(1);
    }
}

Each test runs in a transaction that is rolled back afterwards.

Full integration tests

@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class BookApiIT {

    @Autowired RestTestClient client;

    @Test
    void createsAndReadsBack() {
        client.post().uri("/api/books")
              .body(new BookRequest("Dune", "Herbert", 1965))
              .exchange()
              .expectStatus().isCreated();

        client.get().uri("/api/books")
              .exchange()
              .expectBody().jsonPath("$.length()").isEqualTo(1);
    }
}

Slow — a handful of these is enough.

Test configuration

src/test/resources/application.properties overrides settings during tests:

spring.jpa.hibernate.ddl-auto=create-drop
logging.level.org.springframework.web=DEBUG

Or use a profile: @ActiveProfiles("test").

A real database with Testcontainers

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-testcontainers</artifactId>
    <scope>test</scope>
</dependency>
@SpringBootTest
@Testcontainers
class PostgresIT {

    @Container @ServiceConnection
    static PostgreSQLContainer<?> db = new PostgreSQLContainer<>("postgres:17");
}

@ServiceConnection wires the container’s URL, user and password into the context — no property files.

What to test

  • services: business rules, plain unit tests
  • controllers: status codes, validation, JSON shape — @WebMvcTest
  • repositories: custom queries only — @DataJpaTest
  • end to end: one or two happy paths — @SpringBootTest
Warning

A test that starts the whole context to check a string comparison costs seconds every run. Push tests down the pyramid.

★ Exercises

  1. Unit-test a service with no Spring annotations.
  2. Write a @WebMvcTest with @MockitoBean for the service.
  3. Assert that an invalid POST body returns 400 and names the bad field.
  4. Write a @DataJpaTest for a derived query method.
  5. Add one @SpringBootTest covering create-then-read.