In the whole software delivery process, the unit test stage is the stage where problems can be found at the earliest, and problems can be repeatedly regressed. The more adequate the test is done in the unit test stage, the better the software quality can be guaranteed.
For specific code, refer to the sample project https://github.com/qihaiyan/springcamp/tree/master/spring-unit-test
I. Overview
A functional full-link test often depends on many external components, such as databases, redis, kafka, third-party interfaces, etc. The execution environment of unit tests may be limited by the network and cannot access these external services. Therefore, we hope that through some technical means, we can use unit testing technology to conduct complete functional testing without relying on external services.
2. Test of REST interface
springboot provides the testRestTemplate tool for testing interfaces in unit tests. This tool only needs to specify the relative path of the interface, and does not need to specify the domain name and port. This feature is very useful, because the web service of springboot's unit test runtime environment is a random port, which is specified by the following annotation:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
/remote
Here is how to test the interface we developed through testRestTemplate :
@Test
public void testRemoteCallRest() {
String resp = testRestTemplate.getForObject("/remote", String.class);
System.out.println("remote result : " + resp);
assertThat(resp, is("{\"code\": 200}"));
}
3. Dependence on third-party interfaces
In the above example, our remote interface will call a third-party interface http://someservice/foo
. Our build server may be restricted by the network and cannot access this third-party interface, which will cause the unit test to fail to execute. We can use MockRestServiceServer
the tools to solve this problem.
First define a MockRestServiceServer variable
private MockRestServiceServer mockRestServiceServer;
Initialization during the initialization phase of unit tests
@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));
}
In this way, when the interface is called in our unit test program http://someservice/foo
, the return value will be returned {"code": 200}
instead of actually accessing the third-party interface.
Fourth, the dependence of the database
The dependence on the database is relatively simple, just use the embedded database h2 directly, and all database operations are performed in the embedded database h2.
The gradle configuration is taken as an example:
testImplementation 'com.h2database:h2'
The database connection in the unit test configuration file uses h2:
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:
MyDomain myDomain = new MyDomain();
myDomain.setName("test");
myDomain = myDomainRepository.save(myDomain);
When we call the interface to query the records in the database, we can correctly query the results:
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 paged data, special processing is required, otherwise a json serialization error will be reported.
Define your own Page class:
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<>());
}
}
The call interface returns a custom Page class:
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 returned result is generic, testRestTemplate.exchange
a method needs to be used, and the get method does not support returning generics.
Five, redis dependency
There is an open source redis mockserver on the Internet, which imitates most of the redis instructions, we only need to import this redis-mockserver.
The original version was developed by a Chinese. In the example, a foreigner’s fork version was introduced. Some instructions were added, but the source code could not be found. I forked another version, adding setex and zscore instructions. If necessary You can compile it yourself. Code connection https://github.com/qihaiyan/redis-mock
The gradle configuration is taken as an example:
testImplementation 'com.github.fppt:jedis-mock:0.1.16'
The database connection in the unit test configuration file uses redis mockserver:
spring:
redis:
port: 10033
Add a separate redis configuration file for starting redis mockserver in unit tests:
@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's dependency
Spring provides a kafka test component that can start an embedded kafka service EmbeddedKafka during unit testing to simulate real kafka operations.
The gradle configuration is taken as an example:
testImplementation "org.springframework.kafka:spring-kafka-test"
Initialize EmbeddedKafka through ClassRule, there are two topics: testEmbeddedIn and testEmbeddedOut.
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 the two kafka topics
cloud.stream.bindings:
handle-out-0.destination: testEmbeddedOut
handle-in-0.destination: testEmbeddedIn
handle-in-0.group: embeddedKafkaApplication