By Zhang Cunxi (Yefeng)
Unit Testing means testing a unit. The unit usually refers to a function or class rather than modules and systems in integration testing. The test process of integration testing involves calls across system modules, which is an end-to-end test. Unit testing focuses on the fine granularity of objects to ensure a class or function is executed correctly as expected.
As one of the effective means to ensure code quality, the company is actively promoting unit testing. Combined with the practice of single tests, the following benefits of unit testing are summarized below.
The figure illustrates two issues:
Unit testing is the lowest type of testing in all testing sessions. It is the first and most important. Most defects are introduced during the coding phase, and the cost of fixing them continues to rise as the software lifecycle progresses. In daily research and development, we write the main process, various boundaries, and abnormal unit testing for functional units before delivery testing, which can help us find defects in the code. Compared with the cost of troubleshooting, locating, fixing, and releasing from testing students or online abnormal feedback, unit testing is cost-effective. Unit testing can ensure code quality, bring us a quality reputation, and reduce the time invested by others and ourselves in fixing low-level bugs. We can allocate energy to other, more meaningful things.
In the face of the old code left over in the project, we all have the urge to overthrow it and start over. However, it has been tested for stability for a long time, and we are worried about problems after refactoring. This is a situation we often encounter. When we want to refactor the long-used code we are unfamiliar with, and there is no sufficient test resource guarantee, the risk of refactoring introduced defects is high.
How do we ensure that refactoring doesn't go wrong? In Refactoring: Improving the Design of Existing Code, Martin Fowler says:
“Refactoring is a valuable tool, but refactoring alone is not enough. Refactoring correctly requires a solid set of tests to help me spot unavoidable omissions. Even if there are tools that can help me automate some refactorings, many refactoring techniques still need to be guaranteed by test collections.”
“In addition to the need to have a sufficient understanding of the workflow and master many design ideas and patterns, unit testing is an effective means to ensure error-free refactoring. When the refactoring is complete and the new code still passes the unit testing, it means that the original correct logic of the code has not been broken, and the original externally visible behavior has not changed. Unit testing gives us the confidence to refactor.”
Unit testing and CR are two effective means to ensure code quality. The timing of our submission of CR lags in the process of R&D delivery. The time point of review students pointing out places to be optimized or repaired is also late. Thus, the risk and cost of repair have increased.
The process of coding unit testing is a self-CodeReview process. In this process, we test the main process, boundaries, and exceptions of the functional unit and examine the specification, logic, and design of the code. It improves the quality and review efficiency of subsequent submission of CR and exposes the problems in advance.
When there are multiple collaborators in the project, we only need to mock the data of the dependencies according to the agreement. We do not need to wait for the development and deployment of all the dependent application interfaces before debugging, which improves the efficiency and quality of our collaboration. We disassemble the functional requirements. After developing each small function point, we can write and verify unit testing. This habit enables us to get quick verification feedback on the code. At the same time, when developing a complete function, we need to run through all the single test cases of the project, which makes us perceive whether the change of the whole functional requirements will affect the existing business case.
If we can ensure that each class and function can be executed according to the expected business logic through unit testing, the probability of problems in the integrated functional module or system can be reduced. In this sense, unit testing provides strong support for integration testing and system testing.
When designing and coding, it is difficult to think about all the problems. One of the important criteria for judging code quality is the testability of the code. We can perform a single test of a piece of code. If it is found to be difficult to write, there are a lot of cases to be written, or the current test framework cannot mock dependent objects and needs to rely on other test frameworks with advanced features, we need to look back at the code to see if the coding design is unreasonable, resulting in low testability of the code. This is a positive feedback process, which helps us redesign and refactor in a targeted way.
JUnit is the most widely used unit testing framework in the Java language for writing and running repeatable automated tests. It includes the following features:
Most Java development environments have integrated JUnit as a unit testing tool, and open-source frameworks have corresponding support for JUnit.
Dependencies in projects are complex. The unit testing Mock framework simulates the dependencies of the tested class and provides the expected behavior and state, so our single test can focus on the tested class without being affected by the complexity of the dependencies.
Here, we discuss the commonly used Mockito and PowerMock, both of which are used as unit testing simulation frameworks to simulate complex dependency objects in applications. Mockito is implemented based on dynamic proxies, and PowerMock adds a class loader and bytecode tampering technology to Mockito, making it possible to Mock private/static/final methods.
For example, the company uses JaCoCo to detect unit coverage. When we use the Mock tool that supports bytecode tampering, this may cause:
We recommend using Mockito as our unit testing Mock framework for two reasons:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>4.7.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-inline</artifactId>
<version>4.7.0</version>
<scope>test</scope>
</dependency>
xxx/src/test/java
).ContentService -> ContentServiceTest
.
A good unit testing method name allows us to know the test scenario, intent, and validation expectations quickly. We recommend using should_{expected result}_when_{tested method}_give_{given scenario}
.
Example:
@Test
public void should_returnFalse_when_deleteContent_given_invokeFailed() {
...
}
Counterexample:
@Test
public void testDeleteContent() {
...
}
If the implementation of the single-test method is layered clearly, it can make the code easy to understand. It can also improve the efficiency of subsequent CR.
Here, we recommend a given-when-then
three-segment structure.
Example:
@Test
public void should_returnFalse_when_deleteContent_given_invokeFailed() {
// given
Result<Boolean> deleteDocResult = new Result<>();
deleteDocResult.setEntity(Boolean.FALSE);
when(docManageService.deleteContentDoc(anyLong())).thenReturn(deleteDocResult);
when(docManageService.queryContentDoc(anyLong())).thenReturn(new DocEntity());
// when
Long contentId = 123L;
Boolean result = contentService.deleteContent(contentId);
// then
verify(docManageService, times(1)).queryContentDoc(contentId);
verify(docManageService, times(1)).deleteContentDoc(contentId);
Assert.assertFalse(result);
}
public class SnsFeedsShareServiceImpl {
private SnsFeedsShareHandler snsFeedsShareHandler;
@Autowired
public void setSnsFeedsShareHandler(SnsFeedsShareHandler snsFeedsShareHandler) {
this.snsFeedsShareHandler = snsFeedsShareHandler;
}
public Result<Boolean> shareFeeds(Long feedsId, String platform, List<String> snsAccountList) {
if (!validateParams(feedsId, platform, snsAccountList)) {
return ResponseBuilder.paramError();
}
try {
Result<Boolean> snsResult = snsFeedsShareHandler.batchShareFeeds(feedsId, platform, snsAccountList);
if (Objects.isNull(snsResult) || !snsResult.isSuccess() || Objects.isNull(snsResult.getModel())) {
return ResponseBuilder.buildError(ResponseEnum.SNS_SHARE_SERVICE_ERROR);
}
return ResponseBuilder.successResult(snsResult.getModel());
} catch (Exception e) {
LOGGER.error("shareFeeds error, feedsId:{}, platform:{}, snsAccountList:{}",
feedsId, platform, JSON.toJSONString(snsAccountList), e);
return ResponseBuilder.systemError();
}
}
// Omit code...
}
@RunWith(MockitoJUnitRunner.class)
public class SnsFeedsShareServiceImplTest {
@Mock
SnsFeedsShareHandler snsFeedsShareHandler;
@InjectMocks
SnsFeedsShareServiceImpl snsFeedsShareServiceImpl;
@Test
public void should_returnServiceError_when_shareFeeds_given_invokeFailed() {
// given
Result<Boolean> invokeResult = new Result<>();
invokeResult.setSuccess(Boolean.FALSE);
invokeResult.setModel(Boolean.FALSE);
when(snsFeedsShareHandler.batchShareFeeds(anyLong(), anyString(), anyList())).thenReturn(invokeResult);
// when
Long feedsId = 123L;
String platform = "TEST_SNS_PLATFORM";
List<String> snsAccountList = Collections.singletonList("TEST_SNS_ACCOUNT");
Result<List<String>> result = snsFeedsShareServiceImpl.shareFeeds(feedsId, platform, snsAccountList);
// then
verify(snsFeedsShareHandler, times(1)).batchShareFeeds(feedsId, platform, snsAccountList);
Assert.assertNotNull(result);
Assert.assertEquals(result.getResponseCode(), ResponseEnum.SNS_SHARE_SERVICE_ERROR.getResponseCode());
}
}
@RunWith(MockitoJUnitRunner.class)
public class ContentServiceTest {
@Mock
DocManageService docManageService;
@InjectMocks
ContentService contentService;
...
}
MockitoJUnitRunner
makes Mockito's annotations effective or uses the initialization method MockitoAnnotations.initMocks(this)
.@Mock
to simulate various dependent objects@InjectMocks
to inject the mocked dependency object into the target test object. Take the preceding code as an example. In a single test, docManageService
is injected into the contentService
.We can also use direct initialization or @Spy
to simulate the object and then use the Setter method to simulate the injection of the object. Here is a simpler way.
doNothing().when(contentService.deleteContent(anyLong()));
// given
Result<Boolean> deleteResult = new Result<>(Boolean.FALSE);
when(contentService.deleteContent(anyLong())).thenReturn(deleteResult);
when(contentService.deleteContent(anyLong())).thenCallRealMethod();
when(contentService.deleteContent(anyLong())).thenThrow(NullPointerException.class);
// Verify the input parameter of the calling method. Specify "testTagId".
verify(tagOrmService).queryByValue("testTagId");
// Verify that the queryByValue method is called twice.
verify(tagOrmService, times(2)).queryByValue(anyString());
Validate the return value or exception of the validation method.
// then
Assert.assertNotNull(result);
Assert.assertEquals(result.getResponseCode(), 200);
// Other common assertion functions
Assert.assertTrue(...);
Assert.assertFalse(...);
Assert.assertSame(...);
Assert.assertEquals(...);
Assert.assertArrayEquals(...);
MockedStatic<TagHandler> tagHandlerMockedStatic = Mockito.mockStatic(TagHandler.class);
tagHandlerMockedStatic.when(() -> TagHandler.getSingleCommonTag(anyString())).thenReturn("tag");
When executing mvn test
, if multiple test methods mock the Mockito.mockStatic(TagHandler.class)
, an error will be reported because static methods are class-level and will be registered multiple times. You can refer to the following two solutions:
1. Use @BeforeClass
and @AfterClass
@BeforeClass
Annotated Methods: It is only executed once, the first method executed when running the junit test class.
@AfterClass
Annotated Methods: It is only executed once, the last method executed when running the junit test class.
Example:
@RunWith(MockitoJUnitRunner.class)
public class ContentServiceTest {
@Mock
DocManageService docManageService;
@InjectMocks
ContentService contentService;
private static MockedStatic<TagHandler> tagHandlerMockedStatic = null;
@BeforeClass
public static void beforeTest() {
tagHandlerMockedStatic = Mockito.mockStatic(TagHandler.class);
tagHandlerMockedStatic.when(() -> TagHandler.getSingleCommonTag(anyString())).thenReturn("testTag");
}
// Omit the test method.
@AfterClass
public static void afterTest() {
tagHandlerMockedStatic.close();
}
}
2. Defining Simulation in try-with-resources
Construction
@RunWith(MockitoJUnitRunner.class)
public class ContentServiceTest {
@Mock
DocManageService docManageService;
@InjectMocks
ContentService contentService;
@Test
public void should_returnEmptyList_when_queryContentTags_given_invokeParams() throws Exception {
try (MockedStatic<TagHandler> tagHandlerMockedStatic = Mockito.mockStatic(TagHandler.class)) {
tagHandlerMockedStatic.when(() -> TagHandler.getSingleCommonTag(anyString())).thenReturn("testTag");
// The specific implementation of the single-test method is omitted.
...
}
}
}
public T select(QueryCondition queryCondition) throws Exception {
LindormQueryParam params = queryCondition.generateQueryParams();
if (Objects.isNull(params)) {
LOGGER.error("Invalid query condition:{}", queryCondition.toString());
return null;
}
Select select = tableService.select()
.from(params.getTableName())
.where(params.getCondition())
.limit(1);
QueryResults results = select.execute();
return convert(results.next());
}
Mockito provides the solution, such as tableService.select().from(params.getTableName()).where(params.getCondition()).limit(1)
.
Chain Call Solution: Add parameter RETURNS_DEEP_STUBS
when mocking objects
@Test
public void should_returnNull_when_select_given_invalidQueryCondition() throws Exception {
// when
TableService tableService = mock(TableService.class, RETURNS_DEEP_STUBS);
when(tableService.select().from(anyString()).where(any()).limit(anyInt())).thenReturn(null);
Object result = lindormClient.select(new QueryCondition());
// then
Assert.isNull(result);
}
IDEA has two easy-to-use single-test automatic generated plug-ins: TestMe[1] and Diffblue[2]. TestMe is mainly introduced here. If you have a better plug-in, please recommend it.
1. Installation: Search for TestMe in the Plguins in the IDEA settings and download and install it
2. Use: Find the entry in the code button or directly use the shortcut key option + shift + Q
:
3. The generated code is listed below:
The automatic generation of plug-ins facilitates the initialization of some code, which can improve the efficiency of single-test writing, but it also has limitations. Single-test name specifications, specific implementation, etc. still need to be improved and supplemented before normal use.
Whether it is projects within the company or open-source projects outside the network, few projects have perfect and high-quality unit testing. The reasons for writing a single test were mentioned before, so they will not be repeated here. In the short term, a single test will undoubtedly bring an increase in development workload and duration, but we have to look at the advantages of a single test from the entire iteration cycle. In the end, sticking to unit testing will reduce the number of defects in iterations and shorten the delivery cycle of requirements.
In the past, our CR only focused on the core business code. In most cases, we can point out obvious defects or unreasonable code design in the review, but various conditions of cases, boundaries, and abnormal situations are difficult to review with the naked eye. If the submitted CR contains perfect and high-quality unit testing, the confidence of both the submitting and reviewing parties will be enhanced.
After we commit the code, CI can be set up to run unit testing for that branch. In the release process, add single-test-related controls, such as unit testing pass rate and incremental coverage.
There is no fixed standard for evaluating the workload of unit testing, depending on the complexity of the business logic. In general, if you have not written unit testing before, in the familiar phase, it can be correspondingly increased by 20%-30% based on the requirements of the workload. After being proficient, increasing the requirements of the workload by 10% is sufficient. When business requirements involve a large number of cases, and a single test needs to cover these necessary processes, we can increase some time to ensure high-quality single tests when evaluating the workload.
Unit testing is easy to understand but difficult to practice. It is easy to change the way of working. The difficult thing is to establish a consistent consensus and recognize the value of unit testing. This is the only way we can effectively land on the ground.
[1] https://plugins.jetbrains.com/plugin/9471-testme
[2] https://plugins.jetbrains.com/plugin/14946-diffblue-cover--create-complete-junit-tests-with-ai
1,042 posts | 256 followers
FollowAlibaba Cloud Community - March 24, 2023
Alibaba Cloud Community - February 8, 2022
gangz - December 10, 2020
Changyi - September 2, 2021
gangz - December 10, 2020
Alibaba Cloud Native - October 27, 2021
1,042 posts | 256 followers
FollowProvides comprehensive quality assurance for the release of your apps.
Learn MoreExplore Web Hosting solutions that can power your personal website or empower your online business.
Learn MorePlan and optimize your storage budget with flexible storage services
Learn MoreExplore how our Web Hosting solutions help small and medium sized companies power their websites and online businesses.
Learn MoreMore Posts by Alibaba Cloud Community