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
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 testAt 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 testAt 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.





