Oops File Cannot Be Loaded

Unit-Testing And Integration-Testing In SpringBoot Application

This blog post will provide unit-testing and integration-testing example in SpringBoot Application with springBootTest, mockito, testcontainers and others tech. The example code can be found in https://github.com/kenwu565657/blogMicroService.

Part 1: Testing In Different Layer Of SpringBoot Application

SpringBoot Application have 3 layers commonly, they are controller, service and persistence layers. SpringBoot and other library provide very good support for testing. We will demonstrate them in this part.

Part 1.1: Test Service Layers By Mocking

if there is no dependency in the service class, it will be easy to write unit-test, cause we just need to create a new instance in the testing method. When there is any dependency, maybe some DAO commonly, it becomes difficult to write unit-testing, cause we need to set up db for the DAO, and even we set up a db, someone may argue that this is not a unit-testing. Mocking takes place to solve this problem. There are examples on using Mockito and Self Mocking below.

Part 1.1.2: Unit-Testing on Service Layer By Mockito

JAVACopy
1// blog-service/blog-persistence/src/test/java/com/contentfarm/blog/service/persistence/service/blogpost/BlogPostPersistenceQueryServiceTest.java 2@SpringBootTest(classes = {BlogPostPersistenceQueryServiceTestConfiguration.class}) 3class BlogPostPersistenceQueryServiceTest { 4 5 @Autowired 6 private BlogPostPersistenceQueryService blogPostPersistenceQueryService; 7 8 @MockitoBean // (1) 9 BlogPostDao blogPostDao; 10 11 @MockitoBean // (1) 12 FileStorageService fileStorageService; 13 14 @MockitoBean // (1) 15 BlogPostTagDao blogPostTagDao; 16 17 @BeforeEach 18 void beforeEach() { 19 var testingBlogPostDataList = getTestingBlogPostEntityDataList(); 20 Mockito.when(blogPostDao.findAll()).thenReturn(testingBlogPostDataList); // (2) 21 Mockito.when(blogPostDao.getReferenceById("testingId1")).thenReturn(testingBlogPostDataList.getFirst()); 22 Mockito.when(blogPostDao.getReferenceById("testingId2")).thenReturn(testingBlogPostDataList.get(1)); 23 Mockito.when(blogPostDao.getReferenceById("testingId3")).thenReturn(testingBlogPostDataList.get(2)); 24 ... 25 var testingBlogPostTagDataList = getTestingBlogPostTagEntityDataList(); 26 Mockito.when(blogPostTagDao.findAll()).thenReturn(testingBlogPostTagDataList); 27 28 Mockito.when(fileStorageService.downloadFile("contentfarmblogpost", "blog-post-content/testingFileName1.md")).thenReturn(HexFormat.of().parseHex("e04fd020ea3a6910a2d808002b30309d")); 29 Mockito.when(fileStorageService.downloadFile("contentfarmblogpost", "blog-post-content/testingFileName2.md")).thenReturn(HexFormat.of().parseHex("e04fd020ea3a6910a2d808002b30309d")); 30 Mockito.when(fileStorageService.downloadFile("contentfarmblogpost", "blog-post-content/testingFileName3.md")).thenReturn(HexFormat.of().parseHex("e04fd020ea3a6910a2d808002b30309d")); 31 } 32 33 @Test 34 ... Your Testing Method 35}

(1): In this Example, we are testing BlogPostPersistenceQueryService Class. This class has 3 dependency, BlogPostDao, FileStorageService and BlogPostTagDao. We can inject mock by adding @MockitoBean.

(2): Affter Adding @MockitoBean, we can write the expected mock behaviour by Mockito.when().thenReturn().

Part 1.1.3: Unit-Testing on Service Layer By Self Mocking

For example, we want to test BlogPostDomainService class and there is 2 dependency, IBlogPostPersistenceQueryService and IBlogPostPersistenceCommandService.

JAVACopy
1// blog-service/blog-domain/src/main/java/com/contentfarm/blog/service/domain/domainservice/blogpost/BlogPostDomainService.java 2@RequiredArgsConstructor 3@Service 4public class BlogPostDomainService { 5 private final IBlogPostPersistenceQueryService blogPostPersistenceQueryService; 6 private final IBlogPostPersistenceCommandService blogPostPersistenceCommandService; 7 ... 8}

Then we can implement a mock implementation of the dependency, and inject them to the service in the unit-testing code. For example, IBlogPostPersistenceQueryService is work as a DAO to get and store entity. Then we can mock this function by collection in Java.

JAVACopy
1// blog-service/blog-domain/src/test/java/com/contentfarm/blog/service/domain/domainservice/blogpost/BlogPostDomainServiceTest.java 2 private static class IBlogPostPersistenceQueryServiceSpy implements IBlogPostPersistenceQueryService { 3 ... 4 private final List<BlogPostDomainModel> blogPostDomainModelList = List.of( 5 BlogPostDomainModel.builder().id("testingId1").build(), 6 BlogPostDomainModel.builder().id("testingId2").authorId("testingAuthorId2").build(), 7 BlogPostDomainModel.builder().id("testingId3").authorId("testingAuthorId1").build() 8 ); 9 10 @Override 11 public BlogPostDomainModel getById(String id) { 12 return blogPostDomainModelList.stream().filter(x -> x.getId().equals(id)).findFirst().orElse(null); 13 } 14 ... 15}

Finally, we can construct the service layer class not by springBoot but ourselves. This is purely unit-testing as we even no need the @SpringBootTest and prevent to start SpringBoot for even test one method locally.

JAVACopy
1// blog-service/blog-domain/src/test/java/com/contentfarm/blog/service/domain/domainservice/blogpost/BlogPostDomainServiceTest.java 2... 3private static BlogPostDomainService blogPostDomainService = new BlogPostDomainService(new IBlogPostPersistenceQueryServiceSpy(), getBlogPostPersistenceCommandService()); 4...

Part 1.2: Test Persistence Layer by Testcontainer

unit-testing in persistence layer is difficult as it usually depends on Database. Testcontainers can overcome this problem by start a containerize Database for testing. In this part, we will set up a testcontainer to test the DAO. There are 3 steps.

Part 1.2.1: Add Dependency

XMLCopy
1<!-- blog-service/blog-persistence/pom.xml --> 2<dependency> 3 <groupId>org.springframework.boot</groupId> 4 <artifactId>spring-boot-starter-data-jpa</artifactId> 5</dependency> 6<dependency> 7 <groupId>org.postgresql</groupId> 8 <artifactId>postgresql</artifactId> 9</dependency> 10<dependency> 11 <groupId>org.testcontainers</groupId> 12 <artifactId>junit-jupiter</artifactId> 13 <scope>test</scope> 14</dependency> 15<dependency> 16 <groupId>org.testcontainers</groupId> 17 <artifactId>postgresql</artifactId> 18 <scope>test</scope> 19</dependency> 20<dependency> 21 <groupId>org.springframework.boot</groupId> 22 <artifactId>spring-boot-testcontainers</artifactId> 23 <scope>test</scope> 24</dependency>

Part 1.2.2: Write Testcontainer Class

JAVACopy
1// blog-service/blog-persistence/src/test/java/com/contentfarm/blog/service/persistence/TestPostgreSQLContainer.java 2@Testcontainers 3public interface TestPostgreSQLContainer { 4 @ServiceConnection 5 @Container 6 PostgreSQLContainer<?> postgreSQLContainer = new PostgreSQLContainer<>("postgres:17.4") 7 .withDatabaseName("testing") 8 .withUsername("postgres") 9 .withPassword("postgres") 10 .withExposedPorts(5432) 11 .withCreateContainerCmdModifier(cmd -> cmd.withHostConfig( 12 new HostConfig().withPortBindings(new PortBinding(Ports.Binding.bindPort(5433), new ExposedPort(5432))) 13 )); // (1) 14}

(1): In this example, we set the bindPort to 5433, this is because we may have a running Database or Container listening port 5432 in local development. This may cause problem when we run the test in local environment on our own, for example, the data changes may effect on the running local Database or Container.

Part 1.2.3: Add testing application.yml And database schema

YMLCopy
1blog-service/blog-persistence/src/test/resources/application.yml 2spring: 3 sql: 4 init: 5 mode: always //(1) 6 datasource: 7 url: jdbc:postgresql://localhost:5433/testing //(2) 8 username: postgres 9 password: postgres

(1): As we will start a new container for each times of test, we set sql init mode to always so the db schema will be create for us when the application start.

(2): As we set 5433 above, we need to add testing config

Part 1.2.4: Import Testcontainer class in SpringBootTest

JAVACopy
1// blog-service/blog-persistence/src/test/java/com/contentfarm/blog/service/persistence/dao/blogpost/BlogPostDaoTest.java 2@TestInstance(TestInstance.Lifecycle.PER_CLASS) 3@ImportTestcontainers(TestPostgreSQLContainer.class) // (1) 4@SpringBootTest(classes = BlogPostDaoTestConfiguration.class) 5class BlogPostDaoTest { 6 @Autowired 7 BlogPostDao blogPostDao; 8 9 @Test 10 void ...Your Testing Method 11}

(1): we need to import the Testcontainer to the test class, so it will start the container for us.

Part 1.3: Test Controller Layer

There are two famous framework, they are SpringMvc and SpringWebFlux. We will show example for both below.

Part 1.3.1: Test SpringMvc

Part 1.3.1.1: Add @AutoConfigureMockMvc Annotation And Inject MockMvc

JAVACopy
1// blog-service/blog-web/src/test/java/com/contentfarm/blog/service/web/controller/blogpost/BlogPostControllerMockMvcTest.java 2@SpringBootTest 3@AutoConfigureMockMvc 4public class BlogPostControllerMockMvcTest { 5 @Autowired 6 private MockMvc mockMvc; 7 ... 8}

Part 1.3.1.2(Optional): Add Self Mocking Class

(Optional) If you want to do unit-testing and your controller have any dependency, usually service layer, then you can set Configuration in the test.

JAVACopy
1// blog-service/blog-web/src/test/java/com/contentfarm/blog/service/web/controller/blogpost/BlogPostControllerTestConfiguration.java 2public class BlogPostControllerTestConfiguration { 3 @Bean 4 IBlogPostWebDomainService blogPostDomainService() { 5 return new IBlogPostWebDomainServiceSpy(); 6 } 7 8 @Bean 9 IBlogPostTagWebDomainService blogPostTagDomainService() { 10 return new IBlogPostTagWebDomainServiceSpy(); 11 } 12 13 public static class IBlogPostWebDomainServiceSpy implements IBlogPostWebDomainService {...} 14}
JAVACopy
1@SpringBootTest(classes = BlogPostControllerTestConfiguration.class)

Part 1.3.1.3: Test by mockMvc

JAVACopy
1// blog-service/blog-web/src/test/java/com/contentfarm/blog/service/web/controller/blogpost/BlogPostControllerMockMvcTest.java 2@Test 3void findBlogPostSummary() throws Exception { 4 mockMvc.perform(MockMvcRequestBuilders.get("/blogpost/list")) 5 .andDo(MockMvcResultHandlers.print()) 6 .andExpect(MockMvcResultMatchers.status().isOk()) 7 .andExpect(MockMvcResultMatchers.content().contentType(MediaType.APPLICATION_JSON)); 8}

Part 1.3.1: Test SpringWebFlux

Part 1.3.1.1: Add @WebFluxTest Annotation and inject WebTestClient

JAVACopy
1// multimedia-service/src/test/java/com/contentfarm/multimedia/controller/image/MultimediaImageControllerTest.java 2@Import({MultimediaImageControllerTest.TestingConfiguration.class}) 3@WebFluxTest(controllers = MultimediaImageController.class) 4class MultimediaImageControllerTest { 5 6 private final Logger logger = LoggerFactory.getLogger(this.getClass()); 7 8 private byte[] testingFileContent; 9 10 public static class TestingConfiguration { 11 @Bean 12 public IMultimediaImageDownloadService multimediaImageDownloadService() { 13 return new MultimediaImageDownloadService(new MultimediaServiceTestConfiguration.FileStorageServiceSpy()); 14 } 15 } 16 17 @Autowired 18 private WebTestClient webTestClient; 19 ... 20}

Part 1.3.1.2: Write Testing Method

JAVACopy
1// multimedia-service/src/test/java/com/contentfarm/multimedia/controller/image/MultimediaImageControllerTest.java 2... 3@Test 4void getMultimediaImageByFileName() { 5 String urlTemplate = "/multimedia/image/{0}"; 6 String validUrl = MessageFormat.format(urlTemplate, "java.png"); 7 8 EntityExchangeResult<byte[]> result = webTestClient.get().uri(validUrl) 9 .exchange() 10 .expectStatus().isOk() 11 .expectHeader().contentType(MediaType.IMAGE_PNG_VALUE + "+jpeg") 12 .expectBody(byte[].class) 13 .returnResult(); 14 15 Assertions.assertNotNull(result); 16 Assertions.assertNotNull(result.getResponseBody()); 17 Assertions.assertTrue(result.getResponseBody().length > 0); 18 Assertions.assertArrayEquals(testingFileContent, result.getResponseBody()); 19} 20...

Part 2: Testing On Common Middleware

Part 2.1: Testing On ElasticSearch

Assume we are using Spring Data ElasticSearch, the testing is same as testing on persistence layers.

Part 2.1.1: Add Dependence

XMLCopy
1<!--search-service/pom.xml--> 2<dependency> 3 <groupId>org.springframework.boot</groupId> 4 <artifactId>spring-boot-starter-data-elasticsearch</artifactId> 5 <version>3.4.3</version> 6</dependency> 7<dependency> 8 <groupId>org.testcontainers</groupId> 9 <artifactId>elasticsearch</artifactId> 10 <version>1.20.4</version> 11 <scope>test</scope> 12</dependency> 13<dependency> 14 <groupId>org.testcontainers</groupId> 15 <artifactId>junit-jupiter</artifactId> 16 <scope>test</scope> 17</dependency> 18<dependency> 19 <groupId>org.springframework.boot</groupId> 20 <artifactId>spring-boot-testcontainers</artifactId> 21 <scope>test</scope> 22</dependency>

Part 2.1.2: Set Up ElasticSearch Testcontainer

JAVACopy
1// search-service/src/test/java/com/contentfarm/search/TestElasticSearchContainer.java 2@Testcontainers 3public class TestElasticSearchContainer { 4 @ServiceConnection 5 @Container // 6 public static ElasticsearchContainer container = new ElasticsearchContainer( 7 DockerImageName.parse("docker.elastic.co/elasticsearch/elasticsearch:7.17.28")) 8 .withExposedPorts(9200) 9 .withCreateContainerCmdModifier(cmd -> cmd.withHostConfig( 10 new HostConfig().withPortBindings(new PortBinding(Ports.Binding.bindPort(9201), new ExposedPort(9200))) 11 )); 12}
JAVACopy
1// search-service/src/main/java/com/contentfarm/search/config/ElasticsearchClientConfig.java 2@Configuration 3public class ElasticsearchClientConfig extends ElasticsearchConfiguration { 4 5 @Value("${elasticsearch.url}") 6 private String elasticsearchUrl; 7 8 @Override 9 public ClientConfiguration clientConfiguration() { 10 return ClientConfiguration.builder() 11 .connectedTo(elasticsearchUrl) 12 .build(); 13 } 14}
YMLCopy
1search-service/src/test/resources/application.yml 2elasticsearch: 3 url: "localhost:9201"

Part 2.1.3: Import Testcontainer And Start testing

JAVACopy
1// search-service/src/test/java/com/contentfarm/search/service/blogpost/impl/BlogPostIndexServiceTest.java 2@TestInstance(TestInstance.Lifecycle.PER_CLASS) 3@ImportTestcontainers(TestElasticSearchContainer.class) 4@SpringBootTest 5class BlogPostIndexServiceTest { 6 7 @Autowired 8 IBlogPostIndexService blogPostIndexService; 9 10 @Autowired 11 BlogPostElasticsearchRepository blogPostElasticsearchRepository; 12 13 @BeforeAll 14 void setUp() { 15 blogPostElasticsearchRepository.deleteAll(); 16 } 17 18 @Test 19 void .... 20}

Part 2.2: Testing On Kafka

We may use Testcontainer or @EmbeddedKafka by Spring.

Part 2.2.1: Testing On Kafka by Testcontainer

JAVACopy
1// contentfarm-middleware/contentfarm-kafka-spring-boot-starter/src/test/java/com/contentfarm/kafka/springboot/starter/TestingKafkaTestContainer.java 2@Testcontainers 3public class TestingKafkaTestContainer { 4 private static final String IMAGE_NAME = "apache/kafka:4.0.0-rc0"; 5 6 @ServiceConnection 7 @Container // 8 public static KafkaContainer container = new KafkaContainer( 9 DockerImageName.parse(IMAGE_NAME)) 10 .withEnv("KAFKA_LISTENERS", "PLAINTEXT://:9092,BROKER://:9093,CONTROLLER://:9094"); 11} 12

Part 2.2.2: Testing On Kafka by @EmbeddedKafka

JAVACopy
1// contentfarm-middleware/contentfarm-kafka-spring-boot-starter/src/test/java/com/contentfarm/kafka/springboot/starter/producer/impl/BlogPostMessageProducerTest.java 2@EnableKafka 3@TestInstance(TestInstance.Lifecycle.PER_CLASS) 4@SpringBootTest(classes = TestingConfiguration.class) 5@DirtiesContext 6@EmbeddedKafka(partitions = 1, brokerProperties = { "listeners=PLAINTEXT://localhost:9092", "port=9092" }) 7class BlogPostMessageProducerTest { 8 ... 9}

Keep Updating...