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/