Throughout the software delivery process, the unit testing phase is one of the earliest to find problems, and can be repeated back to the problematic phase, the more adequate the testing done in the unit testing phase, the more the software quality can be guaranteed. Refer to the sample project for the specific code.

1. Overview

The full-link testing of a function often depends on many external components, such as database, redis, kafka, third-party interfaces, etc. The execution environment of unit testing may have no way to access these external services due to network limitations. Therefore, we would like to use some technical means to be able to perform full functional testing with unit testing techniques without relying on external services.

2. The REST interface testing

springboot provides the testRestTemplate tool for testing interfaces in unit tests. The tool only needs to specify the relative path to the interface, not the domain name and port. This feature is very useful because the web service for springboot’s unit test runtime environment is a random port, which is specified by the following annotation.

1
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

The following is a way to test the /remote interface we developed via testRestTemplate.

1
2
3
4
5
6
@Test
public void testRemoteCallRest() {
    String resp = testRestTemplate.getForObject("/remote", String.class);
    System.out.println("remote result : " + resp);
    assertThat(resp, is("{\"code\": 200}"));
}

3. Third-party interface Dependencies

In the above example, our remote interface will call a third-party interface http://someservice/foo, which may not be accessible by our build server due to network limitations, resulting in unit tests not being executed. We can use the MockRestServiceServer tool provided by springboot to solve this problem.

First define a MockRestServiceServer variable

1
private MockRestServiceServer mockRestServiceServer;

Initialization during the initialization phase of the unit test

1
2
3
4
5
6
7
8
@Before
public void before() {
    mockRestServiceServer = MockRestServiceServer.bindTo(restTemplate).ignoreExpectOrder(true).build();

    this.mockRestServiceServer.expect(manyTimes(), MockRestRequestMatchers.requestTo(Matchers.startsWithIgnoringCase("http://someservice/foo")))
            .andRespond(withSuccess("{\"code\": 200}", MediaType.APPLICATION_JSON));

}

This way, when the http://someservice/foo interface is called in our unit test program, it will fix the return value of {"code": 200} instead of actually accessing the third-party interface.

4. Database Dependencies

The database dependency is relatively simple, directly using h2, the embedded database, all database operations are performed in h2, the embedded database.

Take gradle configuration as an example.

1
testImplementation 'com.h2database:h2'

The database connection in the unit test profile uses h2.

1
2
3
4
5
spring:
  data:
    url: jdbc:h2:mem:ut;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
    username: sa
    password:

Database operations can be performed directly in the unit test program.

1
2
3
MyDomain myDomain = new MyDomain();
myDomain.setName("test");
myDomain = myDomainRepository.save(myDomain);

When we call the interface to query the records in the database, we are able to query the results correctly.

1
2
3
MyDomain resp = testRestTemplate.getForObject("/db?id=" + myDomain.getId(), MyDomain.class);
System.out.println("db result : " + resp);
assertThat(resp.getName(), is("test"));

When the interface returns Page paging data, it needs to do a little special handling, otherwise the json serialization will throw an exception.

Define your own Page class.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
    public class TestRestResponsePage<T> extends PageImpl<T> {
    @JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
    public TestRestResponsePage(@JsonProperty("content") List<T> content,
                                @JsonProperty("number") int number,
                                @JsonProperty("size") int size,
                                @JsonProperty("pageable") JsonNode pageable,
                                @JsonProperty("empty") boolean empty,
                                @JsonProperty("sort") JsonNode sort,
                                @JsonProperty("first") boolean first,
                                @JsonProperty("totalElements") long totalElements,
                                @JsonProperty("totalPages") int totalPages,
                                @JsonProperty("numberOfElements") int numberOfElements) {

        super(content, PageRequest.of(number, size), totalElements);
    }

    public TestRestResponsePage(List<T> content) {
        super(content);
    }

    public TestRestResponsePage() {
        super(new ArrayList<>());
    }
}

Call the interface to return a custom Page class.

1
2
3
4
5
RequestEntity<Void> requestEntity = RequestEntity.get("/dbpage").build();
ResponseEntity<TestRestResponsePage<MyDomain>> pageResp = testRestTemplate.exchange(requestEntity, new ParameterizedTypeReference<TestRestResponsePage<MyDomain>>() {
    });
System.out.println("dbpage result : " + pageResp);
assertThat(pageResp.getBody().getTotalElements(), is(1L));

Since the return result is generic, you need to use the testRestTemplate.exchange method. The get method does not support returning generic results.

5. Redis Dependencies

There is an open source redis mockserver online that mimics most of the redis directives, we just need to import this redis-mockserver. The original version was developed by a Chinese person, and the example introduces a version forked by another person, with some additional instructions. But I couldn’t find the source code, so I forked another version, adding setex and zscore directives, so you can compile it yourself if you need.

https://github.com/qihaiyan/redis-mock

Take the gradle configuration as an example.

1
testImplementation 'com.github.fppt:jedis-mock:1.0.1'

The database connection in the unit test configuration file uses redis mockserver.

1
2
3
spring:
  redis:
    port: 10033

Add a separate redis configuration file for starting the redis mockserver in unit tests.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
@TestConfiguration
public class TestRedisConfiguration {

    private final RedisServer redisServer;

    public TestRedisConfiguration(@Value("${spring.redis.port}") final int redisPort) throws IOException {
        redisServer = RedisServer.newRedisServer(redisPort);
    }

    @PostConstruct
    public void postConstruct() throws IOException {
        redisServer.start();
    }

    @PreDestroy
    public void preDestroy() {
        redisServer.stop();
    }
}

6. Kafka Dependencies

spring provides a kafka test component that can start an embedded kafka service EmbeddedKafka during unit testing to simulate real kafka operations.

Take the gradle configuration as an example.

1
testImplementation "org.springframework.kafka:spring-kafka-test"

EmbeddedKafka is initialized via ClassRule with two topics: testEmbeddedIn and testEmbeddedOut.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
    private static final String INPUT_TOPIC = "testEmbeddedIn";
    private static final String OUTPUT_TOPIC = "testEmbeddedOut";
    private static final String GROUP_NAME = "embeddedKafkaApplication";

    @ClassRule
    public static EmbeddedKafkaRule embeddedKafkaRule = new EmbeddedKafkaRule(1, true, INPUT_TOPIC, OUTPUT_TOPIC);

    public static EmbeddedKafkaBroker embeddedKafka = embeddedKafkaRule.getEmbeddedKafka();

    private static KafkaTemplate<String, String> kafkaTemplate;

    private static Consumer<String, String> consumer;

    @BeforeClass
    public static void setup() {

        Map<String, Object> senderProps = KafkaTestUtils.producerProps(embeddedKafka);
        DefaultKafkaProducerFactory<String, String> pf = new DefaultKafkaProducerFactory<>(senderProps);
        kafkaTemplate = new KafkaTemplate<>(pf, true);

        Map<String, Object> consumerProps = KafkaTestUtils.consumerProps(GROUP_NAME, "false", embeddedKafka);
        DefaultKafkaConsumerFactory<String, String> cf = new DefaultKafkaConsumerFactory<>(consumerProps);
        consumer = cf.createConsumer();
        embeddedKafka.consumeFromAnEmbeddedTopic(consumer, OUTPUT_TOPIC);
    }

In the configuration file of the unit test program, you can specify these 2 kafka topics.

1
2
3
4
cloud.stream.bindings:
    handle-out-0.destination: testEmbeddedOut
    handle-in-0.destination: testEmbeddedIn
    handle-in-0.group: embeddedKafkaApplication

Reference http://springcamp.cn/spring-boot-unit-test/