Skip to main content

Spring REST Testing

Introduction

Testing is a critical part of developing robust and reliable REST APIs. Spring provides extensive support for testing REST endpoints at various levels, from unit tests to full integration tests. This guide will introduce you to the concepts and tools needed to effectively test your Spring REST applications.

When building REST APIs with Spring Boot, it's important to verify that your endpoints behave as expected, handle requests properly, and return the correct responses. Spring's testing framework makes this process straightforward with specialized tools designed specifically for testing web applications.

Why Test Spring REST APIs?

Before diving into the technical details, let's understand why testing REST APIs is important:

  1. API Contract Validation - Ensures your API behaves according to its specification
  2. Regression Prevention - Catches issues when changes are made to existing functionality
  3. Documentation - Tests serve as living documentation of how the API should work
  4. Confidence - Provides confidence when refactoring or adding new features

Testing Levels for Spring REST

When testing Spring REST APIs, we typically work with several levels of testing:

  1. Unit Testing - Testing individual components in isolation
  2. Integration Testing - Testing how components work together
  3. API Testing - Testing the API endpoints directly

Let's explore each approach in detail.

Setting Up the Testing Environment

To get started with Spring REST testing, you need the following dependencies in your pom.xml (for Maven):

xml
<dependencies>
<!-- Spring Boot Starter Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Spring Boot Starter Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

The spring-boot-starter-test includes:

  • JUnit 5
  • Spring Test
  • AssertJ
  • Hamcrest
  • Mockito
  • JSONassert
  • JsonPath

Unit Testing REST Controllers

Unit testing focuses on testing individual components in isolation. For REST controllers, we often use MockMvc to simulate HTTP requests without starting a server.

Example: Unit Testing a Controller

Let's say we have a ProductController that provides CRUD operations for products:

java
@RestController
@RequestMapping("/api/products")
public class ProductController {

private final ProductService productService;

public ProductController(ProductService productService) {
this.productService = productService;
}

@GetMapping("/{id}")
public ResponseEntity<Product> getProduct(@PathVariable Long id) {
return productService.findById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}

@PostMapping
public ResponseEntity<Product> createProduct(@RequestBody Product product) {
Product savedProduct = productService.save(product);
URI location = ServletUriComponentsBuilder.fromCurrentRequest()
.path("/{id}")
.buildAndExpand(savedProduct.getId())
.toUri();
return ResponseEntity.created(location).body(savedProduct);
}

// Other CRUD operations...
}

Here's how to write a unit test for this controller:

java
@WebMvcTest(ProductController.class)
class ProductControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private ProductService productService;

@Test
void shouldReturnProductWhenProductExists() throws Exception {
// Given
Product product = new Product(1L, "Smartphone", 599.99);
when(productService.findById(1L)).thenReturn(Optional.of(product));

// When & Then
mockMvc.perform(get("/api/products/1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("Smartphone"))
.andExpect(jsonPath("$.price").value(599.99));
}

@Test
void shouldReturnNotFoundWhenProductDoesNotExist() throws Exception {
// Given
when(productService.findById(99L)).thenReturn(Optional.empty());

// When & Then
mockMvc.perform(get("/api/products/99")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isNotFound());
}

@Test
void shouldCreateProductSuccessfully() throws Exception {
// Given
Product productToCreate = new Product(null, "New Phone", 499.99);
Product createdProduct = new Product(1L, "New Phone", 499.99);
when(productService.save(any(Product.class))).thenReturn(createdProduct);

// When & Then
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"New Phone\",\"price\":499.99}")
.accept(MediaType.APPLICATION_JSON))
.andExpect(status().isCreated())
.andExpect(header().string("Location", containsString("/api/products/1")))
.andExpect(jsonPath("$.id").value(1))
.andExpect(jsonPath("$.name").value("New Phone"))
.andExpect(jsonPath("$.price").value(499.99));
}
}

Key points about this unit test:

  1. @WebMvcTest limits Spring Boot autoconfiguration to components relevant to MVC tests
  2. MockMvc simulates HTTP requests without starting a server
  3. @MockBean creates and injects a Mockito mock for the service dependency
  4. We use MockMvc's fluent API to make requests and assert responses

Integration Testing with @SpringBootTest

While unit tests focus on individual components, integration tests verify that different parts of your application work together correctly.

Example: Integration Test with TestRestTemplate

java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ProductApiIntegrationTest {

@Autowired
private TestRestTemplate restTemplate;

@Autowired
private ProductRepository productRepository;

@BeforeEach
void setup() {
productRepository.deleteAll();
}

@Test
void shouldReturnProductWhenProductExists() {
// Given
Product savedProduct = productRepository.save(new Product(null, "Test Product", 19.99));

// When
ResponseEntity<Product> response = restTemplate.getForEntity(
"/api/products/{id}",
Product.class,
savedProduct.getId()
);

// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getId()).isEqualTo(savedProduct.getId());
assertThat(response.getBody().getName()).isEqualTo("Test Product");
assertThat(response.getBody().getPrice()).isEqualTo(19.99);
}

@Test
void shouldCreateNewProductSuccessfully() {
// Given
Product productToCreate = new Product(null, "New Product", 29.99);

// When
ResponseEntity<Product> response = restTemplate.postForEntity(
"/api/products",
productToCreate,
Product.class
);

// Then
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody()).isNotNull();
assertThat(response.getBody().getId()).isNotNull();
assertThat(response.getBody().getName()).isEqualTo("New Product");
assertThat(response.getBody().getPrice()).isEqualTo(29.99);

// Verify it was actually saved to the database
Optional<Product> savedProduct = productRepository.findById(response.getBody().getId());
assertThat(savedProduct).isPresent();
assertThat(savedProduct.get().getName()).isEqualTo("New Product");
}
}

Key points about this integration test:

  1. @SpringBootTest starts the full application context
  2. webEnvironment = RANDOM_PORT starts an actual web server on a random port
  3. TestRestTemplate makes actual HTTP requests to the running server
  4. We use a real database (or test database) for integration tests
  5. Tests now verify the full request-response flow

Testing with WebTestClient (WebFlux)

If you're using Spring WebFlux for reactive programming, you can use WebTestClient for testing:

java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class ReactiveProductApiTest {

@Autowired
private WebTestClient webTestClient;

@Test
void shouldGetProductById() {
webTestClient.get()
.uri("/api/products/{id}", 1)
.exchange()
.expectStatus().isOk()
.expectBody()
.jsonPath("$.id").isEqualTo(1)
.jsonPath("$.name").isEqualTo("Smartphone")
.jsonPath("$.price").isEqualTo(599.99);
}
}

Testing REST Error Handling

Testing error handling is crucial for robust REST APIs. Let's see how to test error scenarios:

java
@WebMvcTest(ProductController.class)
class ProductControllerErrorHandlingTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private ProductService productService;

@Test
void shouldHandleValidationErrors() throws Exception {
mockMvc.perform(post("/api/products")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"\",\"price\":-1}"))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.errors").isArray())
.andExpect(jsonPath("$.errors[*].field", containsInAnyOrder("name", "price")));
}

@Test
void shouldHandleServiceException() throws Exception {
// Given
when(productService.findById(1L)).thenThrow(new RuntimeException("Database connection failed"));

// When & Then
mockMvc.perform(get("/api/products/1")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isInternalServerError())
.andExpect(jsonPath("$.message").value("Database connection failed"));
}
}

Testing with Profiles and Test Configuration

Sometimes, you need specific configurations for your tests:

java
@SpringBootTest
@ActiveProfiles("test")
class ProductServiceWithTestProfileTest {
// Tests that use the test profile configuration
}

You can create a application-test.properties or application-test.yml file in your test resources with test-specific configuration:

properties
# application-test.properties
spring.datasource.url=jdbc:h2:mem:testdb
spring.jpa.hibernate.ddl-auto=create-drop

Best Practices for Spring REST Testing

  1. Test Different Layers Separately:

    • Controller tests for validation and error handling
    • Service tests for business logic
    • Repository tests for data access
  2. Mock External Dependencies:

    • Use @MockBean for services in controller tests
    • Mock external APIs and services
  3. Use Appropriate Test Scope:

    • Unit tests for focused component testing
    • Integration tests for verifying component interactions
    • End-to-end tests for full flow testing
  4. Test Both Happy Path and Error Scenarios:

    • Valid inputs and expected behaviors
    • Invalid inputs, error conditions, edge cases
  5. Use Test Data Builders or Fixtures:

    • Create helper methods or classes for test data
  6. Clean Up Test Data:

    • Use @BeforeEach and @AfterEach for setup and cleanup

Advanced Topics

Testing Security

If your REST API uses Spring Security, you can test authentication and authorization:

java
@WebMvcTest(ProductController.class)
@WithMockUser(roles = "ADMIN")
class SecuredProductControllerTest {

@Autowired
private MockMvc mockMvc;

@MockBean
private ProductService productService;

@Test
void adminShouldAccessDeleteEndpoint() throws Exception {
mockMvc.perform(delete("/api/products/1"))
.andExpect(status().isOk());
}

@Test
@WithMockUser(roles = "USER") // Override the class-level annotation
void regularUserShouldNotAccessDeleteEndpoint() throws Exception {
mockMvc.perform(delete("/api/products/1"))
.andExpect(status().isForbidden());
}

@Test
@WithAnonymousUser
void anonymousUserShouldBeUnauthorized() throws Exception {
mockMvc.perform(get("/api/products/1"))
.andExpect(status().isUnauthorized());
}
}

Testing with JSON Schema Validation

You can validate that your API responses conform to a JSON Schema:

java
@Test
void responseMatchesJsonSchema() throws Exception {
mockMvc.perform(get("/api/products/1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.id").exists())
.andExpect(jsonPath("$.name").exists())
.andExpect(jsonPath("$.price").exists())
// For more complex schemas, use JSON Schema validation libraries
.andDo(document("product-get",
responseFields(
fieldWithPath("id").description("The product ID"),
fieldWithPath("name").description("The product name"),
fieldWithPath("price").description("The product price")
)
));
}

Summary

Testing Spring REST APIs involves multiple approaches:

  1. Unit Testing with MockMvc focuses on testing controllers in isolation by mocking service dependencies.
  2. Integration Testing with TestRestTemplate or WebTestClient verifies that components work together correctly in a more realistic environment.
  3. Testing techniques include validating status codes, response bodies, headers, and error handling.

Good testing practices help ensure that your Spring REST APIs are robust, reliable, and behave as expected. By combining different testing approaches, you can achieve high confidence in your API's correctness.

Additional Resources

Practice Exercises

  1. Create a simple Spring Boot REST API with CRUD operations for a "Task" entity.
  2. Write unit tests for all controller methods using MockMvc.
  3. Add integration tests using TestRestTemplate.
  4. Implement error handling in your API and write tests to verify the error responses.
  5. Add security to your API and write tests to verify authentication and authorization.

By practicing these exercises, you'll gain hands-on experience with Spring REST testing techniques and improve your ability to build robust and reliable REST APIs.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)