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
JAVACopy1// 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.
JAVACopy1// 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.
JAVACopy1// 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.
JAVACopy1// 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
XMLCopy1<!-- 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
JAVACopy1// 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
YMLCopy1blog-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
JAVACopy1// 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
JAVACopy1// 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.
JAVACopy1// 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}
JAVACopy1@SpringBootTest(classes = BlogPostControllerTestConfiguration.class)
Part 1.3.1.3: Test by mockMvc
JAVACopy1// 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
JAVACopy1// 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
JAVACopy1// 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
XMLCopy1<!--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
JAVACopy1// 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}
JAVACopy1// 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}
YMLCopy1search-service/src/test/resources/application.yml 2elasticsearch: 3 url: "localhost:9201"
Part 2.1.3: Import Testcontainer And Start testing
JAVACopy1// 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
JAVACopy1// 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
JAVACopy1// 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...