Adding Isolated Tests to the Microservices

Before finishing the microservices implementation, it is important to add automated tests. These tests help us make sure that our services work as expected.

At this stage, we do not have much business logic, so we do not need many unit tests. Instead, we focus on testing the APIs exposed by our microservices. This means we start the services in integration tests with their embedded web server and use a test client to send HTTP requests. Then we check if the responses are correct.

Spring WebFlux provides a test client called WebTestClient, which makes it easy to send requests and check results in a clear, readable way.

Example: Testing the Composite Course API

Here, we test the course composite API with three main scenarios:

  1. Valid course request:
    • Send a courseId for an existing course.
    • Expect a 200 OK HTTP response.
    • Expect a JSON response containing the requested courseId, one chapter, and one quiz.
  2. Course not found:
    • Send a courseId that does not exist.
    • Expect a 404 Not Found response.
    • Expect a JSON response with relevant error details.
  3. Invalid course ID:
    • Send a courseId that is invalid.
    • Expect a 422 Unprocessable Entity response.
    • Expect a JSON response with an error message explaining the invalid request.

Here is how the tests are implemented in Java using Spring Boot and Mockito:


@SpringBootTest(webEnvironment = RANDOM_PORT)
class CourseCompositeServiceTest {

    private static final String COURSE_COMPOSITE_URL = "/api/v1/course-composite/";

    private static final Long QUIZ_ID_OK = 10L;
    private static final Long CHAPTER_ID_OK = 11L;

    private static final Long COURSE_ID_OK = 1L;
    private static final Long COURSE_ID_INVALID = 2L;
    private static final Long COURSE_ID_NOT_FOUND = 3L;

    @Autowired
    private WebTestClient client;

    @MockitoBean
    private CourseCompositeIntegrationService integrationService;

    @BeforeEach
    void setUp() {
        var courseOkResponse = new Course(COURSE_ID_OK,
                "Introduction to Spring Boot",
                "Learn the basics of Spring Boot 3.",
                "localhost:9879",
                CourseDifficultyLevel.MEDIUM
        );
        when(integrationService.getCourseById(COURSE_ID_OK)).thenReturn(courseOkResponse);

        var chapter = new Chapter(
                CHAPTER_ID_OK,
                COURSE_ID_OK,
                "Chapter 1: Project Setup with Spring Initializr",
                "This chapter guides you through creating a new Spring Boot project...",
                "localhost:8879");
        when(integrationService.getChaptersByCourseId(COURSE_ID_OK)).thenReturn(singletonList(chapter));

        var quiz = new Quiz(QUIZ_ID_OK, CHAPTER_ID_OK,
                "What is the primary web interface used to bootstrap a Spring Boot project?",
                "Spring Initializr",
                "localhost:7879",
                List.of("Maven Central", "Spring Initializr", "Spring Boot CLI", "Apache Maven"));
        when(integrationService.getQuizzesByChapterId(CHAPTER_ID_OK)).thenReturn(singletonList(quiz));

        when(integrationService.getCourseById(COURSE_ID_NOT_FOUND))
                .thenThrow(new NotFoundException("NOT FOUND: " + COURSE_ID_NOT_FOUND));

        when(integrationService.getCourseById(COURSE_ID_INVALID))
                .thenThrow(new InvalidRequestException("INVALID: " + COURSE_ID_INVALID));
    }

    @Test
    void getCourse() {
        client.get()
                .uri(COURSE_COMPOSITE_URL + COURSE_ID_OK)
                .accept(APPLICATION_JSON)
                .exchange()
                .expectStatus().isOk()
                .expectHeader().contentType(APPLICATION_JSON)
                .expectBody()
                .jsonPath("$.courseId").isEqualTo(COURSE_ID_OK)
                .jsonPath("$.chapters.length()").isEqualTo(1)
                .jsonPath("$.chapters[0].quizzes.length()").isEqualTo(1);
    }

    @Test
    void getCourseNotFound() {
        var uri = COURSE_COMPOSITE_URL + COURSE_ID_NOT_FOUND;
        client.get()
                .uri(uri)
                .accept(APPLICATION_JSON)
                .exchange()
                .expectStatus().isNotFound()
                .expectHeader().contentType(APPLICATION_JSON)
                .expectBody()
                .jsonPath("$.path").isEqualTo(uri)
                .jsonPath("$.timestamp").isNotEmpty()
                .jsonPath("$.httpStatus").isEqualTo(HttpStatus.NOT_FOUND.name())
                .jsonPath("$.message.fieldErrors").isArray()
                .jsonPath("$.message.fieldErrors").isEmpty()
                .jsonPath("$.message.bannerMessage").isEqualTo("NOT FOUND: " + COURSE_ID_NOT_FOUND);
    }

    @Test
    void getCourseInvalidInput() {
        var uri = COURSE_COMPOSITE_URL + COURSE_ID_INVALID;
        client.get()
                .uri(uri)
                .accept(APPLICATION_JSON)
                .exchange()
                .expectStatus().isEqualTo(UNPROCESSABLE_ENTITY)
                .expectHeader().contentType(APPLICATION_JSON)
                .expectBody()
                .jsonPath("$.path").isEqualTo(uri)
                .jsonPath("$.timestamp").isNotEmpty()
                .jsonPath("$.httpStatus").isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY.name())
                .jsonPath("$.message.fieldErrors").isArray()
                .jsonPath("$.message.fieldErrors").isEmpty()
                .jsonPath("$.message.bannerMessage").isEqualTo("INVALID: " + COURSE_ID_INVALID);
    }
}

How the Mock Works

By using mocks and WebTestClient, we can safely test microservice APIs without connecting to real databases or external services. This approach makes tests faster and more reliable.

NOTE:

Gradle runs your tests automatically every time you build the project.

 ./gradlew build

If you only want to run the tests—without doing the full build—you can use the following command:

 ./gradlew test

This is useful when you are developing and want quick feedback. Running only the tests is usually faster than building the whole project.

The source code for this article is available over on GitHub.