introduction:
This paper prefers practice rather than methodology. The writing method of SpringBoot unit test mentioned in this paper is not an official solution, but a writing method that the author thinks is more convenient and efficient. Each team and even each developer in the team may have their own writing habits and styles. As long as the effect of unit testing can be achieved, there is no need to struggle with the simplicity or complexity of writing. Here you are also welcome to express your views or share your single measurement experience to help newcomers like the author grow rapidly.
Why write unit tests?
Testing is a very important part of Devops, but most developers focus on integration testing - as long as the joint commissioning is successful, the features I am going to launch this time must be OK.
To be honest, I used to be like this, and I may still be like this. As a non professional writer, I immediately entered xx factory in Hangzhou after graduation, and successively participated in the construction of internal Devops platform and the reclamation of xx cloud Paas project. In these two projects, Development > testing is a very normal scenario, and even some tests are guest stars of the original development friends: due to the lack of professional testers, Development often needs to take into account integration testing and even online testing. In order to improve efficiency, I maintain some common test cases on the internal automatic test platform. Even so, I can still clearly feel that there are only a few scenes that can be covered by the test, so that every time I confidently launch the big feature, it will be located in the middle of the night due to some strange problems. Fortunately, I met a senior boss later. During the code review, he directly pointed out my bad habit of not writing unit tests, and repeatedly stressed the importance of single test with his painful online lessons.
Of course, the above is only my personal experience, which is barely used as the talk of daily gossip. If you want to deeply understand the importance of unit test, it is recommended to search the keyword of the importance of unit test on Google. You can feel the different understanding of unit test by programmers in different countries and fields, and you must gain more.
Second, why recommend link ideas?
In-depth contact with unit testing, the development will inevitably encounter the following scenarios:
- How should test cases be designed?
- How should I write test cases?
- How to determine the quality of test cases?
At the beginning of learning to write unit tests, I have also referred to and tried various writing methods on the Internet. These methods may use different single test frameworks or focus on different code links (such as a specific service method). At first, I was complacent that I could skillfully use a variety of single test frameworks, but with the progress of my work, I gradually realized that the important thing in unit testing is not framework selection, but how to design a set of excellent use cases. The reason why we use "one set" instead of "one" is that in our business code, logic is often not "plain sailing", and many if else will decorate our business code. Obviously, for this kind of business code, "one" test case can not fully meet all possible scenarios. In order to be lazy, trying to cover the mainstream process with only "one" use case is tantamount to laying a mine for yourself - Online scenarios are not as simple as "one" use case!
I began to focus on the design of test cases, starting with input and output, and re-examine the code I had developed. I found that if a controller method is used as an entry, this set of business processes can be used as a link, and the methods of the service layer, dao layer and api layer associated in the context can be used as all links on the link. By drawing the link diagram, each link is roughly divided into black and white according to whether it is associated with the external system. The whole business process and the potential branches of each link will become clear, and the test cases will naturally change from "one" to "one set". Let's mention more here. The basis of link idea design use case is the code style with clear structure and controllable cycle complexity. If the development still respects "paper style" and "one knife flow" and "long speech" in a single method, the link style will be a huge burden.
In fact, writing test cases is not an arduous task. For the development of business code, writing test cases is like a piece of cake. For me, it takes less time to write test cases than to design test cases (highlighting the importance of use case design, but it may also be that I am not proficient in test case design). In terms of test framework selection, I am more used to the combination of Junit+Mockito because it is only familiar and simple, and reference documents are everywhere. If you already have your own customary framework and writing method, you don't have to copy the things mentioned in this article. After all, the single test is for better code, not for trouble.
However, no matter how test cases are designed or written, I always believe that the core indicator to measure the quality of test cases is branch coverage without considering the style and specification of test code. This is also a major reason why I recommend the link idea - starting from the entrance, traverse each branch of each link on the link, and Mock if there is any obstacle; Compared with the separate single test methods, the input and output parameters required by the single test link are clearer, which greatly saves the time and cost of writing test code! There are many tools for calculating branch coverage, such as local JaCoCo or various cloud testing tools. Just think, when you see that the single test perfectly covers the feature code you submitted, do you feel much relieved?
III. how to design / construct single test with link idea?
As programmers, the more familiar link concept should be full link voltage measurement.
In short, full link pressure test is a process of simulating massive user requests and data to stress test the whole business chain and continuously optimize it based on the actual production business scenario and system environment. In essence, it is also a means of performance test Through this method, the normalized and stable pressure measurement system is implemented in the production environment to realize the long-term stable performance management of IT system.
If A complete business process is regarded as A full link, it is actually A microlink as A link in the business chain, that is, A back-end service. Here, take the top-down development process as an example. For the new functional interfaces, we will habitually start from the controller, then build the service layer, dao layer and api layer, and finally add some aop. If the complex process is divided into various links based on the link idea, the function of such code is clear and the maintenance is quite convenient. I very much agree with the code access control that limits the number of lines of A single method to < = 50. For the long code "paper", no student who takes over must have A smile on his face; For this kind of code, I think the priority of clean code is higher than that of supplementary single test cases, and even the logic can't be clarified. Even if you insist on writing single test cases, the subsequent debugging and maintenance workload is unpredictable (imagine that if A student A takes over this code later, he adds xx lines in the "paper", resulting in ut failure, how can he locate the problem).
Simply draw a picture to emphasize my point. This is a functional logic diagram of "users buy pigs". Based on the link idea, the developer divides the whole process into corresponding link links, covering controller, service, dao and api layers; The whole link is clear. As long as it is matched with a perfect context log, it is easy to locate online problems.
Of course, the development based on link idea is far from enough. When supplementing single test cases, we can also use link idea to construct test cases. The requirements of test cases are very simple. They need to cover the code independently written by controller, service and so on (multi branch scenarios also need to be completely covered). Mock can be used to shield the surrounding associated systems, and the SQL of Dao layer can decide whether to mock according to the needs. Adhering to this idea, we can transform the "user buys a pig" diagram to allow the link of mock to be grayed out, so as to become the "virtual user buys a pig" diagram we need when writing unit test cases.
IV. practical cases of rapid writing
1 what are the core steps of fast writing?
The entry of fast writing method is the controller layer method, which can cover a small amount of logic code in the controller layer.
Design the input and expected output of test cases
The purpose of designing test cases is not only to run through the main process, but to run through all possible processes, that is, the so-called branch full coverage. Therefore, the input and output of design cases are particularly important. Even the incremental modification of the new branch (for example, adding a line of if else) needs to supplement the corresponding input and expected output. It is not recommended to modify the expected results according to the single test run results, which indicates that there is a problem with the original code design.
Determine all Mock points on the link
The judgment of Mock point is based on whether the link depends on the third-party service. It is strongly recommended to draw a general function flow chart (such as the "user buys a pig" chart) before design, which can greatly improve the speed and accuracy of determining Mock points.
Collect simulated return data of Mock points
After determining the mock point, we need to construct the corresponding simulation return data. Mock data needs to consider several factors:
a. Whether it matches the expected return value of the corresponding method of the api layer: the Mock data returned from the pig factory cannot be replaced with beef
b. Whether it matches the analog input data: the user needs 1 kg of pork, but cannot return the data of 5 kg of pork
c. Whether it matches all branches of the api layer: some api layers will verify the return value with response codes (2XX | 3xx | 4xx). In such scenarios, it is necessary to construct Mock data with different response codes
2 [development] real users buy pigs
The project is built based on PandoraBoot, manually upgrade the SpringBoot version to 2.5.1, and use mybatis plus components to simplify the development process of Dao layer. The following shows the important methods involved in the above figure, which only realizes a simple business process. The system framework and engineering structure can refer to the code warehouse.
Business object
PorkStorage.java - Database entity class of pork inventory /** * Database entity class of pork inventory */ @Data @NoArgsConstructor @AllArgsConstructor @Builder @TableName(value = "pork_storage", autoResultMap = true) public class PorkStorage { @TableId(value = "id", type = IdType.AUTO) private Long id; private Long cnt; }
PorkInst.java - pork instance, generated after packaging by the warehouse
/** * Pork instance, generated by the warehouse after packaging **/ @Data @NoArgsConstructor @AllArgsConstructor @Builder public class PorkInst { /** * weight */ private Long weight; /** * Attachment parameters, such as packing type, mailing address, etc */ private Map< String, Object> paramsMap; }
Business code
PorkController.java @RestController @Slf4j @RequestMapping("/pork") public class PorkController { @Autowired private PorkService porkService; @PostMapping("/buy") public ResponseEntity< PorkInst> buyPork(@RequestParam("weight") Long weight, @RequestBody Map< String,Object> params) { if (weight == null) { throw new BaseBusinessException("invalid input: weight", ExceptionTypeEnum.INVALID_REQUEST_PARAM_ERROR); } return ResponseEntity.ok(porkService.getPork(weight, params)); } }
PorkService.java
public interface PorkService { /** * Get pork packing instance * * @param weight weight * @param params Additional information * @return {@link PorkInst} - Specify the number of pork instances * @throws BaseBusinessException If the pork inventory is insufficient, an exception is returned, and the background informs the factory */ PorkInst getPork(Long weight, Map< String, Object> params); }
PorkStorageDao.java
@Mapper public interface PorkStorageDao extends BaseMapper< PorkStorage> { PorkStorage queryStore(); }
PorkStorageDao.xml
< ?xml version="1.0" encoding="UTF-8"?> < !DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> < mapper namespace="com.alibaba.ut.demo.dao.PorkStorageDao"> < sql id="columns">id, cnt< /sql> < sql id="table_name">pork_storage< /sql> < select id="queryStore" resultType="com.alibaba.ut.demo.entity.PorkStorage"> select < include refid="columns"/> from < include refid="table_name"/> where id = 1 < /select> < /mapper>
FactoryApi.java
public interface FactoryApi { void supplyPork(Long weight); }
FactoryApiImpl.java
@Service @Slf4j public class FactoryApiImpl implements FactoryApi { @Override public void supplyPork(Long weight) { log.info("call real factory to supply pork, weight: {}", weight); } }
WareHouseApi.java
public interface WareHouseApi { PorkInst packagePork(Long weight, Map< String, Object> params); }
WareHouseApiImpl.java
@Service @Slf4j public class WareHouseApiImpl implements WareHouseApi { @Override public PorkInst packagePork(Long weight, Map< String, Object> params) { log.info("call real warehouse to package, weight: {}", weight); return PorkInst.builder().weight(weight).paramsMap(params).build(); } }
3 [single test] virtual users buy pigs
Single test dependency
For PandoraBoot project, refer to Maven configuration below to introduce relevant dependencies.
For non PandoraBoot projects, only Junit and Mockito packages need to be introduced.
Note: the single test writing method mentioned in this chapter defaults to the Mock Dao layer and does not need to start the container application. If you don't want the Mock Dao layer, it is recommended to introduce an H2 memory database into the dependency, and support local startup of container applications.
Writing ideas
Before reading the following contents, it is strongly recommended to learn the basic usage and operation principle of Junit and Mockito, including but not limited to the notes that may be involved in the following writing methods:Junit native Method annotation: @ Before, @ Test, @ After
Mockito native Field annotation: @ Mock, @ InjectMocks, @ Spy
On the premise that the service link to be tested is known, the writing method can be briefly summarized into the following steps:
- Preliminary design of single test case framework. It includes three steps: setup, teststep and teardown. Setup is responsible for handling some globally necessary pre test logic (such as Mock data insertion and environment preparation). Teststep carries the main body of the single test case (it is required to end with the assertion statement similar to the Assert class). Teardown is responsible for handling some globally necessary closing logic (such as Mock data deletion and environment release)
- Declare and initialize all links involved in the use case. On the premise that the link flow is known, all links can be roughly divided into two categories according to whether it is a Mock point method (refer to the gray and white points in the "user buys a pig" diagram above).
- Non Mock point method: for non entry links in the link (usually the controller is used as the entry, and other methods are non entry), you need to mark @ Spy to declare that the object is in the listening state in the single test link, that is, you need to complete the process normally. Here, the Mock point method is further divided into two categories according to whether it is referenced in the method.
- Other Mock point methods are referenced in this method. You need to mark @ InjectMocks on the basis of @ Spy to declare that this object needs to be injected into other Mock objects in the single test link.
- Other Mock point methods are not referenced in this method, so no other operations are required.
- Mock point method: Mark @ mock to declare that the object needs to be mocked in the single test link. You can use org mockito. A series of static methods in the mockito class manually inject the mock value (EP. When (a()) thenReturn(B)).
- Write single test case body. In teststep, the method call is initiated from the controller layer, and finally the success of the use case is judged by the Assert assertion result. In addition to the normal return value verification scenario, Junit also supports @ Test(expected = xxException.class) to declare the expected exception type of the use case. Finally, it is suggested that after writing the single test, you can explain the general description of the supported scenarios and expected results of the single test in the form of notes, so that you and other students who take over can quickly understand the relevant information of the single test case in the future.
Here, we still take the "user buys pig" scenario as an example. According to the link idea, when the server receives the user's request to buy pork, we can construct the following branch scenario:
- There is A possible exit in the controller layer, i.e. weight==null. Test case A is generated accordingly, named testBuyPorkIfWeightIsNull, the actual input parameter weight==null, and the expected interface throws an exception;
- Enter the PigServiceImpl by link, and there is a possible exit, that is, hasStore() == false. Based on this, test case B is generated and named testBuyPorkIfStorageIsShortage. The weight in the actual input parameter must be greater than the inventory value (for example, the default inventory in setup in the code is 10, and the virtual user requested 20). It is expected that the interface will throw an exception;
- Continue by link and find a normal exit. Based on this, test case C is generated and named testbuyporkiffesultisok. The weight in the actual input parameter must be less than the inventory value (for example, the default inventory in setup in the code is 10, and the virtual user requested 5). It is expected that the return value of the interface is consistent with the return value matching the input parameter, that is, the pork packing instance with weight of 5 is returned normally.
Single test code
package com.alibaba.ut.demo.controller; import com.alibaba.ut.demo.PorkController; import com.alibaba.ut.demo.api.FactoryApi; import com.alibaba.ut.demo.api.WareHouseApi; import com.alibaba.ut.demo.dao.PorkStorageDao; import com.alibaba.ut.demo.entity.PorkInst; import com.alibaba.ut.demo.entity.PorkStorage; import com.alibaba.ut.demo.exception.BaseBusinessException; import com.alibaba.ut.demo.service.impl.PorkServiceImpl; import lombok.extern.slf4j.Slf4j; import org.junit.After; import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; import org.mockito.stubbing.Answer; import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import java.util.HashMap; import java.util.Map; import java.util.Optional; import static org.mockito.Matchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.when; /** * @Author Taofu.lj * @Version 1.0.0 * @Date 2021 December 2, 2014 14:15 */ @Slf4j public class PorkControllerTest { /** * controller As it is a link entry, @ Spy monitoring is not required */ @InjectMocks private PorkController porkController; /** * The link link of interface type is replaced by implementation class initialization, @ Spy needs to be initialized manually to avoid failure in initMocks * Note: each ring on the link must be declared, even if it is not explicitly called in the test case */ @InjectMocks @Spy private PorkServiceImpl porkService = new PorkServiceImpl(); /** * Link to be Mock, the same below */ @Mock private PorkStorageDao porkStorageDao; @Mock private FactoryApi factoryApi; @Mock private WareHouseApi wareHouseApi; /** * Preset data can be declared directly as class variables */ private final Map< String, Object> mockParams = new HashMap< String, Object>() {{ put("user", "system_user"); }}; @Before public void setup() { // Required: initialize the Mock and InjectMock objects declared in this class MockitoAnnotations.initMocks(this); // Mock presets data and binds related methods (applicable to methods with return values) PorkStorage mockStorage = PorkStorage.builder().id(1L).cnt(10L).build(); // Common Mock writing method 1: only try to return the value of Mock when(porkStorageDao.queryStore()).thenReturn(mockStorage); // Common Mock writing method 2: not only try to return the value of Mock, but also want to make additional logs to facilitate location when(wareHouseApi.packagePork(any(), any())) .thenAnswer(ans -> { log.info("mock log can be written here"); return PorkInst.builder() .weight(ans.getArgumentAt(0, Long.class)) .paramsMap(ans.getArgumentAt(1, Map.class)) .build(); }); // Mock action and bind related methods (applicable to methods without return value) doAnswer((Answer< Void>) invocationOnMock -> { log.info("mock factory api success!"); return null; }).when(factoryApi).supplyPork(any()); } @After public void teardown() { // TODO: Mock data cleaning or resource release can be added } /** * When the incoming parameter is null, a business exception is thrown * * @throws BaseBusinessException */ @Test(expected = BaseBusinessException.class) public void testBuyPorkIfWeightIsNull() { porkController.buyPork(null, mockParams); } /** * When the background inventory does not meet the demand, a business exception is thrown * * @throws BaseBusinessException */ @Test(expected = BaseBusinessException.class) public void testBuyPorkIfStorageIsShortage() { porkController.buyPork(20L, mockParams); } /** * Return business results during normal purchase */ @Test public void testBuyPorkIfResultIsOk() { Long expectWeight = 5L; ResponseEntity< PorkInst> res = porkController.buyPork(expectWeight, mockParams); // Check whether the return status of the interface meets the expectation for the first time Assert.assertEquals(HttpStatus.OK, res.getStatusCode()); Long actualWeight = Optional.of(res).map(HttpEntity::getBody).map(PorkInst::getWeight).orElse(-99L); // Check whether the returned value of the interface meets the expectation for the second time Assert.assertEquals(expectWeight, actualWeight); } }
Recommended reading:
What is the meaning of unit testing
https://www.zhihu.com/question/49530527
Better code, faster: 8 reasons why you should use unit testing : https://fortegrp.com/the-importance-of-unit-testing/