avatarIvan Franchin

Summary

The provided content is a comprehensive guide on implementing unit tests for Spring Boot applications using Kafka with Spring Cloud Stream, specifically for News Producer and News Consumer applications.

Abstract

The article details the process of unit testing in two Spring Boot applications that interact with Kafka: a News Producer and a News Consumer. It begins by outlining the creation of test packages and classes, such as NewsDtoTest, NewsControllerTest, NewsTest, and NewsPublisherTest for the producer, and NewsTest and NewsListenerTest for the consumer. The guide explains how to use Spring's testing utilities like @JsonTest, @WebMvcTest, and @SpringBootTest to test JSON serialization/deserialization, controller endpoints, and message publishing/listening functionality. It also demonstrates the use of MockMvc for simulating HTTP requests and OutputDestination and InputDestination for testing message exchange with Kafka. The article concludes with instructions on running the tests and emphasizes the importance of ensuring all tests pass to certify the application's functionality. Additionally, it provides links to further reading on related topics such as end-to-end testing, distributed tracing with Zipkin, using Cloudevents, and deploying to Minikube. The author encourages reader engagement and support through claps, shares, follows on social media, and subscriptions to their newsletter.

Opinions

  • The author believes that unit testing is crucial for verifying the correctness of individual components within a software application.
  • The use of Spring Boot's testing annotations and utilities is advocated as a best practice for creating efficient and effective unit tests.
  • Mocking dependencies, such as the NewsPublisher, is presented as a valuable technique to isolate components during testing.
  • The article implies that thorough testing, including unit tests, is essential for the reliability and maintainability of Kafka-based applications in a Spring Cloud Stream environment.
  • The author suggests that following the step-by-step guide will lead to a successful implementation of unit tests for the News Producer and News Consumer applications.
  • By providing additional readings, the author indicates the importance of continuous learning and exploration of related technologies and practices to enhance the development experience and application robustness.
  • The call to action for reader engagement and support indicates the author's commitment to community involvement and the value they place on reader feedback and interaction.

Implementing Unit Tests for a Kafka Producer and Consumer that uses Spring Cloud Stream

Step-by-step guide on how to implement Unit tests for News Producer and Consumer apps using Spring Testing Library

Photo by Kyle Glenn on Unsplash

In this article, we will explain how to implement Unit Tests in two Spring Boot Kafka applications: News Producer and News Consumer.

You can find the complete code and implementation in the article linked below. Feel free to follow the steps explained in the article and get started.

Unit test is a software testing approach that focuses on isolating and evaluating individual components or units of a software application in isolation.

In the context of the News Producer and News Consumer, unit test would involve testing each component, class, or method separately to ensure that they perform their specific functions correctly.

For the News Producer, this will include testing the code responsible for publishing news, and for the News Consumer, it will involve testing the logic for consuming news.

Besides, we will explore the annotations and utilities that Spring Boot provides in order to unit test the applications.

So, let’s get started!

Updating News Producer

Create some packages

In the src/test/java folder, let’s create the following packages inside the com.example.newsproducer root package: controller and publisher.

Create the NewsDtoTest class

In the controller package, let’s create a new class called NewsDtoTest, with the following content:

package com.example.newsproducer.controller;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.boot.test.json.JsonContent;

import java.io.IOException;

import static org.assertj.core.api.Assertions.assertThat;

@JsonTest
class NewsDtoTest {

    @Autowired
    private JacksonTester<NewsDto> jacksonTester;

    @Test
    void testSerialize() throws IOException {
        NewsDto newsDto = new NewsDto("title test");

        JsonContent<NewsDto> jsonContent = jacksonTester.write(newsDto);

        assertThat(jsonContent)
                .hasJsonPathStringValue("@.title")
                .extractingJsonPathStringValue("@.title").isEqualTo("title test");
    }

    @Test
    void testDeserialize() throws IOException {
        String content = "{\"title\":\"title test\"}";

        NewsDto newsDto = jacksonTester.parseObject(content);

        assertThat(newsDto.title()).isEqualTo("title test");
    }
}

This test class is used to verify the correct serialization and deserialization of a NewsDto object using Jackson. The @JsonTest annotation signals that this class is designed for JSON testing and the JacksonTester utility, facilitates JSON testing.

Create the NewsControllerTest class

In the controller package, create the NewsControllerTest class with the content below:

package com.example.newsproducer.controller;

import com.example.newsproducer.publisher.News;
import com.example.newsproducer.publisher.NewsPublisher;
import org.junit.jupiter.api.Test;
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 org.springframework.test.web.servlet.ResultActions;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doNothing;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(NewsController.class)
class NewsControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private NewsPublisher newsPublisher;

    @Test
    void testPublishNewsWhenNewsDtoIsCorrectlyInformed() throws Exception {
        doNothing().when(newsPublisher).send(any(News.class));

        String content = "{\"title\":\"title test\"}";
        ResultActions resultActions = mockMvc.perform(post("/api/news")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(content))
                .andDo(print());

        resultActions.andExpect(status().isCreated());
    }

    @Test
    void testPublishNewsWhenNewsDtoIsIncorrectlyInformed() throws Exception {
        doNothing().when(newsPublisher).send(any(News.class));

        String content = "{}";
        ResultActions resultActions = mockMvc.perform(post("/api/news")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(content))
                .andDo(print());

        resultActions.andExpect(status().isBadRequest());
    }
}

The NewsControllerTest class is a set of unit tests for the NewsController in a Spring MVC context. It focuses on testing the functionality of publishing news through the controller using the MockMvc framework for simulating HTTP requests and responses.

In the testPublishNewsWhenNewsDtoIsCorrectlyInformed method, the test simulates a POST request to the /api/news endpoint with correctly informed news data in JSON format. The NewsPublisher is mocked to do nothing when the send method is called. The test then expects a successful response with a status code of 201 (CREATED), indicating that the news was successfully published.

In the testPublishNewsWhenNewsDtoIsIncorrectlyInformed method, the test simulates a POST request with incorrectly informed news data (an empty JSON object). Similarly, the NewsPublisher is mocked to do nothing. The test expects a response with a status code of 400 (BAD REQUEST), indicating that the request was malformed or missing required parameters.

Create the NewsTest class

In the publisher package, create the NewsTest class with the following content:

package com.example.newsproducer.publisher;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.boot.test.json.JsonContent;

import java.io.IOException;
import java.time.Instant;

import static org.assertj.core.api.Assertions.assertThat;

@JsonTest
class NewsTest {

    @Autowired
    private JacksonTester<News> jacksonTester;

    @Test
    void testSerialize() throws IOException {
        News news = new News("title test", Instant.parse("2023-10-14T12:56:04Z"));

        JsonContent<News> jsonContent = jacksonTester.write(news);

        assertThat(jsonContent)
                .hasJsonPathStringValue("@.title")
                .extractingJsonPathStringValue("@.title").isEqualTo("title test");
        assertThat(jsonContent)
                .hasJsonPathStringValue("@.createdOn")
                .extractingJsonPathStringValue("@.createdOn").isEqualTo("2023-10-14T12:56:04Z");
    }

    @Test
    void testDeserialize() throws IOException {
        String content = "{\"title\":\"title test\",\"createdOn\":\"2023-10-14T12:56:04Z\"}";

        News news = jacksonTester.parseObject(content);

        assertThat(news.title()).isEqualTo("title test");
        assertThat(news.createdOn()).isEqualTo("2023-10-14T12:56:04Z");
    }
}

Similar to the NewsDtoTest, this class uses the @JsonTest annotation and the JacksonTester utility to test the serialization and deserialization of a News object.

Create the NewsPublisherTest class

In the publisher package, create the NewsPublisherTest class with the content below:

package com.example.newsproducer.publisher;

import org.json.JSONException;
import org.json.JSONObject;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.stream.binder.test.OutputDestination;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.messaging.Message;
import org.springframework.messaging.MessageHeaders;

import java.time.Instant;

import static org.assertj.core.api.Assertions.assertThat;

@SpringBootTest
@Import(TestChannelBinderConfiguration.class)
class NewsPublisherTest {

    @Autowired
    private OutputDestination outputDestination;

    @Autowired
    private NewsPublisher newsPublisher;

    @Test
    void testSendNews() throws JSONException {
        String title = "title test";
        String createdOn = "2023-10-14T12:56:04.147095Z";

        newsPublisher.send(new News(title, Instant.parse(createdOn)));

        Message<byte[]> outputMessage = outputDestination.receive(0, "com.example.news-producer.news");

        assertThat(outputMessage).isNotNull();

        MessageHeaders headers = outputMessage.getHeaders();
        assertThat(headers.get(MessageHeaders.CONTENT_TYPE)).isEqualTo(MediaType.APPLICATION_JSON_VALUE);

        JSONObject payloadJson = new JSONObject(new String(outputMessage.getPayload()));
        assertThat(payloadJson.getString("title")).isEqualTo(title);
        assertThat(payloadJson.getString("createdOn")).isEqualTo(createdOn);
    }
}

The NewsPublisherTest class is a unit test for the NewsPublisher component, which is responsible for sending news messages. This test uses the Spring @SpringBootTest annotation to load the entire application context and @Import to include the TestChannelBinderConfiguration for setting up test bindings.

In the testSendNews method, the test simulates the sending of a News object with a title and a timestamp to the newsPublisher. It then checks the output destination to verify that the news message has been sent correctly. The test expects a message to be received on the specified channel (com.example.news-producer.news) and validates its content.

The assertions include checking the message headers, ensuring that the content type is set to JSON. It then parses the payload, which is expected to be a JSON string, and checks whether the title and createdOn fields match the values used in the test.

Running Unit Tests

In a terminal and inside the news-producer root folder, run the following command to start the tests:

./mvnw clean test

At the end, all the tests should pass.

Updating News Consumer

Create some packages

In the src/test/java folder, let’s create the listener package inside the com.example.newsconsumer root package.

Create the NewsTest class

In the listener package, create the NewsTest class with the content below:

package com.example.newsconsumer.listener;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;
import org.springframework.boot.test.json.JsonContent;

import java.io.IOException;
import java.time.Instant;

import static org.assertj.core.api.Assertions.assertThat;

@JsonTest
class NewsTest {

    @Autowired
    private JacksonTester<News> jacksonTester;

    @Test
    void testSerialize() throws IOException {
        News news = new News("title test", Instant.parse("2023-10-14T12:56:04Z"));

        JsonContent<News> jsonContent = jacksonTester.write(news);

        assertThat(jsonContent)
                .hasJsonPathStringValue("@.title")
                .extractingJsonPathStringValue("@.title").isEqualTo("title test");
        assertThat(jsonContent)
                .hasJsonPathStringValue("@.createdOn")
                .extractingJsonPathStringValue("@.createdOn").isEqualTo("2023-10-14T12:56:04Z");
    }

    @Test
    void testDeserialize() throws IOException {
        String content = "{\"title\":\"title test\",\"createdOn\":\"2023-10-14T12:56:04Z\"}";

        News news = jacksonTester.parseObject(content);

        assertThat(news.title()).isEqualTo("title test");
        assertThat(news.createdOn()).isEqualTo("2023-10-14T12:56:04Z");
    }
}

This test class is used to verify the correct serialization and deserialization of a News object using Jackson. The @JsonTest annotation signals that this class is designed for JSON testing and the JacksonTester utility, facilitates JSON testing.

Create the NewsListenerTest class

In the listener package, create the NewsListenerTest class with the following content:

package com.example.newsconsumer.listener;

import com.example.newsconsumer.NewsConsumerApplication;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.cloud.stream.binder.test.InputDestination;
import org.springframework.cloud.stream.binder.test.TestChannelBinderConfiguration;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;

import java.time.Instant;

import static org.assertj.core.api.Assertions.assertThat;

@ExtendWith(OutputCaptureExtension.class)
class NewsListenerTest {

    @Test
    void testListenNews(CapturedOutput output) {
        try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
                TestChannelBinderConfiguration
                        .getCompleteConfiguration(NewsConsumerApplication.class))
                .web(WebApplicationType.NONE)
                .run()) {
            String title = "title test";
            String createdOn = "2023-10-14T12:56:04Z";

            News news = new News(title, Instant.parse(createdOn));
            Message<News> newsMessage = MessageBuilder.withPayload(news).build();

            InputDestination inputDestination = context.getBean(InputDestination.class);
            inputDestination.send(newsMessage, "com.example.news-producer.news");

            String expected = "Received News! \"%s\" created on '%s'".formatted(title, createdOn);
            assertThat(output).contains(expected);
        }
    }
}

The NewsListenerTest class is a unit test for the NewsListener component, which is responsible for processing news messages. This test uses the JUnit 5 testing framework along with the Spring @ExtendWith annotation to enable the OutputCaptureExtension for capturing the output during the test.

In the testListenNews method, the test simulates the reception of a News message with a title and a timestamp. It creates a News object, builds a message with this payload, and sends it to the specified input channel (com.example.news-producer.news). The test then captures the application's output and checks whether the expected message is present.

The assertion verifies that the output contains the expected string, which is a formatted representation of the news details. This ensures that the NewsListener correctly processes the incoming news message and produces the expected output.

Running Unit Tests

In a terminal and inside the news-consumer root folder, run the following command to start the tests:

./mvnw clean test

At the end, all the tests should pass.

Conclusion

In this article, we have implemented unit tests for two Spring Boot Kafka applications: News Producer and News Consumer. We have added tests for the controller, DTOs, publisher and listener. We ran the test cases in both apps to certify that they are all green and passing.

Additional Readings

Support and Engagement

If you enjoyed this article and would like to show your support, please consider taking the following actions:

  • 👏 Engage by clapping, highlighting, and replying to my story. I’ll be happy to answer any of your questions;
  • 🌐 Share my story on Social Media;
  • 🔔 Follow me on: Medium | LinkedIn | Twitter;
  • ✉️ Subscribe to my newsletter, so you don’t miss out on my latest posts.
Testing
Unit Testing
Spring Boot
Technology
Spring Cloud Stream
Recommended from ReadMedium