Spring Boot Cheatsheet

Testing

Use this Spring Boot reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Test Dependencies

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

Includes: JUnit 5, Mockito, AssertJ, MockMvc, Hamcrest, JsonPath, H2 (in-memory DB), Testcontainers integration.

Test Slices — Choosing the Right Annotation

AnnotationLoadsUse For
@SpringBootTestFull ApplicationContextIntegration tests
@WebMvcTest(Ctrl.class)Web layer only (no DB)Controller unit tests
@DataJpaTestJPA + H2/Testcontainer onlyRepository tests
@JsonTestJackson mapper onlyJSON serialization tests
@RestClientTestRestTemplate/WebClient + mock serverHTTP client tests
@WebFluxTestReactive web layerWebFlux controller tests
@DataMongoTestMongoDB layerMongo repository tests

Full Integration Test (@SpringBootTest)

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
class OrderIntegrationTest {

    @Autowired TestRestTemplate restTemplate;

    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16")
        .withDatabaseName("testdb");

    @DynamicPropertySource
    static void props(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", postgres::getJdbcUrl);
        registry.add("spring.datasource.username", postgres::getUsername);
        registry.add("spring.datasource.password", postgres::getPassword);
    }

    @Test
    void createOrderReturns201() {
        var request = new CreateOrderRequest("item-1", 3);
        var response = restTemplate.postForEntity("/api/orders", request, OrderDto.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
        assertThat(response.getBody()).isNotNull();
        assertThat(response.getBody().quantity()).isEqualTo(3);
    }
}

WebEnvironment Options

ValueBehavior
RANDOM_PORTStarts real server on random port; use TestRestTemplate or WebTestClient
DEFINED_PORTUses server.port (default 8080)
MOCKMockMvc-based; no real server (default)
NONENo web environment at all

Controller Tests (@WebMvcTest)

@WebMvcTest(ProductController.class)
class ProductControllerTest {

    @Autowired MockMvc mvc;
    @MockBean ProductService productService;   // replaces real bean

    @Test
    void getProductReturns200() throws Exception {
        var dto = new ProductDto(1L, "Widget", new BigDecimal("9.99"));
        given(productService.findById(1L)).willReturn(dto);

        mvc.perform(get("/api/products/1")
                .accept(MediaType.APPLICATION_JSON))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$.name").value("Widget"))
            .andExpect(jsonPath("$.price").value(9.99));
    }

    @Test
    void createProductWithInvalidBodyReturns400() throws Exception {
        String body = """
            { "name": "", "price": -1 }
            """;
        mvc.perform(post("/api/products")
                .contentType(MediaType.APPLICATION_JSON)
                .content(body))
            .andExpect(status().isBadRequest());
    }

    @Test
    @WithMockUser(roles = "ADMIN")
    void deleteRequiresAdminRole() throws Exception {
        mvc.perform(delete("/api/products/1"))
            .andExpect(status().isNoContent());
    }
}

Common MockMvc Methods

// Request builders
get("/path")
post("/path").contentType(APPLICATION_JSON).content(json)
put("/path/{id}", 1L)
delete("/path/{id}", 1L)
multipart("/path").file("file", bytes)

// RequestBuilders fluent options
.param("page", "0")
.header("Authorization", "Bearer token")
.accept(MediaType.APPLICATION_JSON)
.contentType(MediaType.APPLICATION_JSON)

// Result matchers
.andExpect(status().isOk())
.andExpect(status().isCreated())
.andExpect(status().isBadRequest())
.andExpect(content().contentType(MediaType.APPLICATION_JSON))
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.items", hasSize(3)))
.andExpect(jsonPath("$.items[0].name").value("Widget"))
.andExpect(header().string("Location", containsString("/api/products/")))

// Print request/response to console (debugging)
.andDo(print())

// Capture the result
MvcResult result = mvc.perform(get("/api/products")).andReturn();
String json = result.getResponse().getContentAsString();

Repository Tests (@DataJpaTest)

@DataJpaTest   // auto-configures H2, JPA, and Flyway/schema; @Transactional per test
class ProductRepositoryTest {

    @Autowired ProductRepository repo;
    @Autowired TestEntityManager em;

    @Test
    void findBySkuReturnsProduct() {
        var product = new Product(null, "Widget", "SKU-001", Status.ACTIVE);
        em.persistAndFlush(product);

        Optional<Product> found = repo.findBySku("SKU-001");

        assertThat(found).isPresent();
        assertThat(found.get().getName()).isEqualTo("Widget");
    }
}

Use Real DB with @DataJpaTest

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@Testcontainers
class ProductRepositoryTest {
    @Container
    static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");

    @DynamicPropertySource
    static void props(DynamicPropertyRegistry reg) {
        reg.add("spring.datasource.url", postgres::getJdbcUrl);
        reg.add("spring.datasource.username", postgres::getUsername);
        reg.add("spring.datasource.password", postgres::getPassword);
    }
    // ...
}

Pure Unit Tests (No Spring Context)

class OrderServiceTest {

    @Mock OrderRepository orderRepo;
    @Mock PaymentService paymentService;
    @InjectMocks OrderService orderService;   // Mockito injects mocks

    @BeforeEach
    void setUp() {
        MockitoAnnotations.openMocks(this);
    }

    // Or use @ExtendWith(MockitoExtension.class) on the class

    @Test
    void placeOrderCallsPaymentService() {
        var req = new CreateOrderRequest(1L, "ITEM-1", 2);
        var order = new Order(1L, req.userId(), List.of());
        given(orderRepo.save(any())).willReturn(order);

        orderService.place(req);

        verify(paymentService).charge(eq(1L), any(BigDecimal.class));
    }

    @Test
    void placeOrderThrowsWhenItemNotFound() {
        given(orderRepo.save(any())).willThrow(new ResourceNotFoundException("Item", 99L));

        assertThatThrownBy(() -> orderService.place(new CreateOrderRequest(1L, 99L, 1)))
            .isInstanceOf(ResourceNotFoundException.class)
            .hasMessageContaining("99");
    }
}

Mockito Quick Reference

// Stubbing
given(service.findById(1L)).willReturn(dto);
given(service.findById(anyLong())).willThrow(ResourceNotFoundException.class);
given(service.findById(argThat(id -> id > 0))).willReturn(dto);

// Void methods
willDoNothing().given(service).delete(1L);
willThrow(new RuntimeException()).given(service).delete(2L);

// Argument matchers
any(), anyLong(), anyString(), anyList()
eq(1L), isNull(), isNotNull()
argThat(obj -> obj.getId() > 0)

// Verification
verify(service).findById(1L);
verify(service, times(2)).findById(anyLong());
verify(service, never()).delete(anyLong());
verify(service, atLeastOnce()).save(any());
verifyNoMoreInteractions(service);

// Capture arguments
ArgumentCaptor<Order> captor = ArgumentCaptor.forClass(Order.class);
verify(repo).save(captor.capture());
assertThat(captor.getValue().getStatus()).isEqualTo(Status.PENDING);

AssertJ Quick Reference

// Strings
assertThat(name).isEqualTo("Alice").startsWith("Al").hasSize(5);
assertThat(email).contains("@").isNotBlank();

// Numbers
assertThat(price).isGreaterThan(BigDecimal.ZERO).isLessThan(new BigDecimal("100"));
assertThat(count).isEqualTo(3).isPositive();

// Collections
assertThat(list).hasSize(3).contains("a","b").doesNotContain("x");
assertThat(list).extracting("name").containsExactly("Alice","Bob");
assertThat(list).allMatch(item -> item.isActive());

// Optional
assertThat(optional).isPresent().contains(expectedValue);
assertThat(optional).isEmpty();

// Exceptions
assertThatThrownBy(() -> service.method())
    .isInstanceOf(IllegalArgumentException.class)
    .hasMessage("Invalid input");

assertThatCode(() -> service.safeMethod()).doesNotThrowAnyException();

// Objects
assertThat(dto).isNotNull()
    .hasFieldOrPropertyWithValue("name", "Alice")
    .usingRecursiveComparison().isEqualTo(expectedDto);

JSON Tests (@JsonTest)

@JsonTest
class ProductDtoJsonTest {

    @Autowired JacksonTester<ProductDto> json;

    @Test
    void serializesCorrectly() throws Exception {
        var dto = new ProductDto(1L, "Widget", new BigDecimal("9.99"));
        assertThat(json.write(dto))
            .hasJsonPathValue("$.id")
            .hasJsonPathStringValue("$.name", "Widget");
    }

    @Test
    void deserializesCorrectly() throws Exception {
        String content = """
            {"id":1,"name":"Widget","price":9.99}
            """;
        assertThat(json.parse(content).getObject())
            .usingRecursiveComparison()
            .isEqualTo(new ProductDto(1L, "Widget", new BigDecimal("9.99")));
    }
}

Test Properties and Configuration

// Override properties for a test class
@SpringBootTest
@TestPropertySource(properties = {
    "spring.mail.host=localhost",
    "feature.flag.enabled=true"
})

// Or via a test application.properties
// src/test/resources/application-test.properties

// Activate a test profile
@ActiveProfiles("test")

Testcontainers Reuse Pattern

// Shared container across all tests (reuse = true)
@Testcontainers
class AbstractIntegrationTest {

    @Container
    static final PostgreSQLContainer<?> POSTGRES =
        new PostgreSQLContainer<>("postgres:16").withReuse(true);

    @DynamicPropertySource
    static void configureProperties(DynamicPropertyRegistry registry) {
        registry.add("spring.datasource.url", POSTGRES::getJdbcUrl);
        registry.add("spring.datasource.username", POSTGRES::getUsername);
        registry.add("spring.datasource.password", POSTGRES::getPassword);
    }
}

// Then extend:
class UserRepositoryTest extends AbstractIntegrationTest { ... }
class OrderRepositoryTest extends AbstractIntegrationTest { ... }

Common Pitfalls

  • @MockBean resets application context — each unique @MockBean combination causes a new context to load; minimize @MockBean use and share context between tests with @DirtiesContext or common base classes.
  • @DataJpaTest rolls back transactions — by default each test method runs in a transaction that rolls back; add @Commit if you need the data to persist between test methods.
  • @WebMvcTest does not load @Service beans — use @MockBean for all service-layer dependencies, or the test will fail with NoSuchBeanDefinitionException.
  • MockMvc vs TestRestTemplate — MockMvc tests the servlet without starting a server (fast); TestRestTemplate requires WebEnvironment.RANDOM_PORT and tests the full HTTP stack.
  • Security auto-configuration in @WebMvcTest — by default, Spring Security is active in @WebMvcTest; add @WithMockUser or exclude SecurityAutoConfiguration.class.
  • Testcontainers on CI — requires Docker; use @Container on static fields (shared per class) to reduce container startup time.