In this comprehensive guide, we will demonstrate how to create a unit test for the REST controller endpoint that exposes the "/fetch-dataset" GET endpoint. The endpoint uses a service method, "fetchDatasetFromJson", which returns a reactive Mono. The implementation subscribes to the Mono and logs the parsed dataset. Although the endpoint always returns a 200 OK status, it is crucial to validate the service call, ensuring that any asynchronous operations are performed as intended.
For this unit test, we use the following components:
The combination of these components provides a fast and focused unit testing environment for Spring Boot REST controllers.
The test will be structured using a JUnit 5 test class annotated with @WebMvcTest
to specify the controller under test. The service is mocked with @MockBean
, and MockMvc
is used to simulate the HTTP call to the endpoint. One of the key aspects is that our controller method, although asynchronous in nature because it calls subscribe on the Mono, still returns an HTTP 200 status synchronously. Hence, the test will focus on verifying the status code and ensuring that the service method is invoked with the expected parameter.
// Import necessary classes and annotations
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import reactor.core.publisher.Mono;
// Import your controller and service packages as necessary
// Assume YourController is the class that defines the fetchDataset method
// and AggregateDatasetService is the dependency service.
@WebMvcTest(YourController.class)
public class YourControllerTest {
@Autowired
private MockMvc mockMvc;
@MockBean
private AggregateDatasetService aggregateDatasetService;
@BeforeEach
public void setUp() {
// Ensure that mocks are initialized before each test if necessary.
MockitoAnnotations.openMocks(this);
}
@Test
public void testFetchDataset_SuccessfulCall() throws Exception {
String datasetJsonUrl = "https://example.com/dataset.json";
// Create a dummy object for the expected dataset
Object expectedDataset = new Object(); // Replace with an actual dataset object if available
// Define mock behavior: when the service method is called, return a Mono containing the expected dataset.
when(aggregateDatasetService.fetchDatasetFromJson(datasetJsonUrl))
.thenReturn(Mono.just(expectedDataset));
// Perform a GET request using MockMvc to test the endpoint.
mockMvc.perform(get("/fetch-dataset")
.param("datasetJsonUrl", datasetJsonUrl)
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk());
// Verify that the service method was indeed called with the provided URL.
verify(aggregateDatasetService).fetchDatasetFromJson(datasetJsonUrl);
}
}
In this test case:
Despite using subscribe() to trigger the asynchronous call in the controller, the test does not need to wait for the asynchronous processing to complete, as the HTTP status is synchronous. However, should the operation need deeper validation such as logging or side effects on the response body, additional techniques (like using StepVerifier for reactive streams) might be considered. In the current context, the crucial part is ensuring that the service is called as expected.
While the primary test ensures a successful path, additional scenarios can be developed:
Invalid URL: If the "datasetJsonUrl" is not in a valid format, the controller may potentially return a bad request (HTTP 400) if validation is built into the endpoint. To test such a case, craft a test where the invalid URL is provided and assert that the endpoint responds appropriately.
Service Error: Simulate cases where the service returns an error (for example, a Mono.error() call). In this scenario, test that the endpoint might gracefully handle the error by returning a HTTP 5xx status code. Although our default implementation always returns OK, a fully implemented error handling strategy should be verified.
Scenario | Description | Expected Outcome |
---|---|---|
Successful Call | Valid dataset URL provided and service returns a dataset. | HTTP 200 OK, service method called with correct parameter. |
Invalid URL Format | Provided URL is not correctly formatted. | HTTP 400 Bad Request (if URL validation exists). |
Service Error | The service call results in an error due to unexpected conditions. | HTTP 5xx error status (if error handling is implemented). |
In scenarios where the controller's asynchronous processing needs to be validated in detail, developers can integrate reactive testing utilities such as StepVerifier from Project Reactor. For instance, if the response body contained data derived from the asynchronous operation, StepVerifier could be employed to assert on each element emitted. However, in our current case, since the endpoint uses subscribe merely for logging and returns an empty ResponseEntity immediately, the traditional synchronous verification suffices.
Ensure your project's build file includes the necessary dependencies for testing with Spring Boot and Mockito. Here is an example snippet for a Maven-based project:
<!--
Dependency for Spring Boot Testing
-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
With these dependencies, you ensure that the testing framework and mocking libraries are available and configured appropriately.
Incorporating these unit tests within your continuous integration (CI) pipeline ensures that the functionality of your controller remains intact as the code evolves. Automated tests are executed with each commit, providing timely feedback and reducing the chance of regressions.
It is a best practice in unit testing to isolate the component under test. Make use of mocking frameworks like Mockito to ensure that only the behavior of the controller is in scope. This avoids unnecessary complexity and potential side effects from external dependencies such as asynchronous service calls.
Although the controller logs the result, you can verify that however the asynchronous processing is structured (for example, through a side-effect mechanism), the service interactions are consistently maintained. Keep tests focused on observable outcomes, such as HTTP response statuses and invoked service methods.
When designing unit tests, consider additional scenarios: