Talk About Unit Testing

Talk about unit testing Introduction:

Talk about unit testing. The current state of unit testing, and best practices for building automated unit test generation.

Write in front

For us developers, unit testing must not be unfamiliar, but it will be ignored for various reasons, especially in the projects I have come into contact with, when various problems are found in the testing phase, I think it is necessary to talk about Let's unit test. Whether there is a need for unit testing, in the end, is not the point I want to talk about here. If you are interested, you can find out:
[Unit Testing and TDD]
[What exactly is unit testing]
Unit tests written for the sake of writing are of little value, but the benefits of a good unit test are very objective. The question is how to write good unit tests? How to drive and write unit tests?

Talk about unit testing Our current situation


Status 1 : Multiple projects have no unit tests at all
Status 2: Developers do not have the habit of writing unit tests, or do not have time to write due to rushing business records
Status 3: Unit tests are written as integration tests, such as containers and databases, which lead to long running time of unit tests and lose their meaning
Status 4: Too much reliance on integration tests

Above is the test situation of the two projects I found in aone . It is basically useless to merge and release without considering unit testing.
From the perspective of development, the reasons for the above problems are probably as follows:

1.Development costs
In the early stage of the system, it may take a lot of time to write new business, and the old system is too large to start.
2.maintenance cost
Every time we modify the relevant class or refactor the code, we have to modify the corresponding unit test.

3.ROI
Is the input-output positive return? Maybe both managers and ourselves are questioning this question, so sometimes there is no strong motivation
Second, how to solve
After all, it's all about cost, so how do we solve the cost?
So, let's start from the very beginning: the cost of development
The traditional way of writing a unit test includes the following aspects:
1.Test data (data under test, and dependent objects)
2.testing method
3.return value assertion

@Test
public void testAddGroup() {
// database
BuyerGroupDTO groupDTO = new BuyerGroupDTO();
groupDTO.setGmtCreate(new Date());
groupDTO.setGmtModified(new Date());
groupDTO.setName("China");
groupDTO.setCustomerId(customerId);
// Method
Result result = customerBuyerDomainService.addBuyerGroup ( groupDTO );
// return value assertion
Assert.assertTrue ( result.isSuccess ());
Assert.assertNotNull ( result.getData ());
}
A simple test is fine, but if the logic is complex and the input data is complex , it is actually quite a headache to write. How to free the hands of our programmers?
"If you want to do good work, you must first sharpen your tools"
We try our best to reduce our development costs, which involves the choice of our testing frameworks and tools


Talk about unit testing.Test framework selection

.
First of all, the first question is the choice of junit4 and junit5. [From junit4 to junit5] I think one of the most convenient benefits is that we can parameterize the test, and based on the parameterized test, we can configure our parameters more flexibly
The effect is as follows

@ParameterizedTest _
@ValueSource ( strings = { "racecar", "radar", "able was I ere I saw elba " })
void palindromes( String candidate) {
assertTrue ( StringUtils.isPalindrome (candidate));
}
Even better, junit5 provides extensions, such as our commonly used json format. Here we use json file as input
@ParameterizedTest _
@JsonFileSource ( resources = {"/ com / cq /common/KMPAlgorithm/test.json " } )
public void test2 Test( JSONObject arg ) {
Animal animal = JSONObject.parseObject ( arg.getString ("Animal"), Animal.class );
List stringList = JSONObject.parseArray(arg.getString("List"),String.class);
when(testService.testOther(any(Student.class))).thenReturn(stringArg);
when(testService.testMuti(any(List.class),any(Integer.class))).thenReturn(stringList);
when(testService.getAnimal(any(Integer.class))).thenReturn(animal);
String result = kMPAlgorithm.test2();
//todo verify the result
}

Talk about unit testing mock frame:

Mockito : The syntax is particularly elegant, suitable for mocking container classes, and it also provides better assertions for function calls with empty return values. The disadvantage is that static methods cannot be simulated (supported in versions 3.4.x and above)
EasyMock : Similar usage, but stricter
PowerMock : It can be used as a supplement to Mockito, such as to test static methods, but does not support junit5
Spock : Unit testing framework based on Groovy language

Talk about unit testing.database layer

Here we mainly introduce the H2 database, which is based on memory as a simulation of a relational database, and is automatically released after running to achieve the purpose of isolation.
Main configuration: ddl file path, dml file path. Not detailed here.
However, it is difficult to define whether or not to integrate the database. Its function is mainly used to verify the sql syntax, but it is relatively heavy. It is recommended to be used for lightweight integration testing.

Talk about unit testing.Junit5 and Mockito

The framework used for automatic generation mentioned later and the most used in the industry are MocKito , so I will focus on it here, including the problems encountered when using it.

Talk about unit testing Instructions

1.Introduce dependencies separately, it is recommended to introduce the latest version


< groupId > org.junit.jupiter
junit-jupiter
5.7.2
test



org.mockito
mockito-core
3.9.0
test



< groupId > org.mockito
< artifactId > mockito-junit-jupiter
3.9.0
test

2. Use spring-test family bucket

< groupId > org.springframework.boot
< artifactId >spring-boot-starter-test
test
2.5.0

The use of junit5 will not be introduced here, mainly to talk about the ArgumentsProvider interface. By implementing it, you can customize the parameterized class, similar to the built-in ValueSource , EnumSource , etc.

Talk about unit testing.Mockito main annotation introduction

First ask why, why do you need Mockito
Because: the current java project is almost inseparable from the spring framework, and its most famous is the IOC, all beans are managed by the container, so this brings us a problem with unit testing. If we want to do unit testing on beans, we need to Start the container, then the time overhead will be very large. So Mockito brought me a series of solutions that allow us to easily test beans.
@Component
public class A {
@Autowired _
private B b ; // fully mock
@Autowired _
private C c ; // need to execute method
@ Autowired D d ; // Need to execute real method
public void func ( ){
}
}
@Component
class C {
@Autowired _
private B b ;
public void needExec ( ){
}
}
@Component
public class B {
}
Suppose we want to unit test the A.func () above.

@InjectMocks annotation _
Indicates the class that needs to be injected into the bean, there are two types
1.The class under test is easy to understand. We test this class, and of course we need to inject beans into it. such as the above A
2.In the tested class, its real method needs to be executed, but it also needs the main bean, which is C above. We need to test the neeExec method, but we don't care about the specific details of B. In reality, such as things, concurrent locks, etc. This class requires the form of Mockito.spy (new C()), otherwise an error will be reported

@Mock
Indicates the data to be mocked, that is, the content of the method is not actually executed, and only executed according to our rules, or returned, such as using when(). thenReturn () syntax.
Of course, it is also possible to execute the real method, you need the when(). thenCallRealMethod () method.

@Spy
Indicates that all methods follow the real way, such as some tool classes and conversion classes, we also write them in the form of beans ( strictly speaking, this needs to be written as a static tool class ).
@ExtendWith ( MockitoExtension.class ) _
public class ATest {
@InjectMocks _
private A a=new A( );
@Mock
private B b ;
@Spy
private D d ;
@InjectMocks _
private C c= Mockito.spy(new C());;
@BeforeEach
public void setUp() throws Exception {
MockitoAnnotations.openMocks(this);
}
@ParameterizedTest
@ValueSource(strings = {"/com/alibaba/cq/springtest/jcode5test/needMockService/A/func.json"})
public void funcTest(String str) {
JSONObject arg = TestUtils.getTestArg (str);
a.func ();
// todo verify the result
}

Talk about unit testing.Mockito and junit5 FAQ


1. Mock static methods
Mockito 3.4 and later support, the previous version can be assisted by PowerMock

2. Mockito version and java version compatibility issue

The error is as follows
Mockito cannot mock this class: xxx
Mockito can only mock non-private & non-final classes.
reason is that 2.17.0 and earlier versions are compatible with java8
But after 2.18, you need to use java11, in order to use Mockito in java8, you need to introduce another package

< groupId > net.bytebuddy
< artifactId >byte-buddy
1.12.6


3. Jupiter- api version compatibility issue
Process finished with exit code 255
java.lang.NoSuchMethodError : org.junit.jupiter.api.extension.ExtensionContext.getRequiredTestInstances() Lorg /junit/jupiter/api/extension/TestInstance
The first problem is caused by inconsistent versions of api , engine, and params in junit5.
The second problem is because the version of jupiter-api is too low, and it is only supported in versions after 5.7.0

Fourth, the test code is automatically generated
After choosing the framework, we still haven't solved our problem, "how to save development costs?" We will talk about this issue in this section, which is what I mainly want to express.
Writing unit tests has always been a headache. To assemble all kinds of data, you may be annoyed by a bunch of " xxxx cannot be null" errors before it runs successfully . Therefore, we have reason to imagine whether there is a way to solve this problem.

Fourth, the test code is automatically generated
After choosing the framework, we still haven't solved our problem, "how to save development costs?" We will talk about this issue in this section, which is what I mainly want to express.
Writing unit tests has always been a headache. To assemble all kinds of data, you may be annoyed by a bunch of " xxxx cannot be null" errors before it runs successfully . Therefore, we have reason to imagine whether there is a way to solve this problem.

1. TestMe
@Test
void testTestExtend ( ) {
when( testService.getStr ( anyInt ())). thenReturn (" getStrResponse ");
when( testService.testMuti (any(), anyInt ())). thenReturn (Arrays. asList ("String"));
when(testService.testOther(any())).thenReturn("testOtherResponse");
jCode5.testExtend(Integer.valueOf(0));
}
@Test
void testTestGeneric() {
when(testService.getStr(anyInt())).thenReturn("getStrResponse");
when( testService.getLong ( anyString (), anyString ())). thenReturn ( Long.valueOf (1));
when( testService.testMuti (any(), anyInt ())). thenReturn (Arrays. asList ("String"));
jCode5.testGeneric(new Person( ));
}
1. The generated code is basically logical, including the logic of beans that require mocking.
2. But it omits the most important part, that is, the data , and simply uses the form of the constructor. This is obviously not suitable for our DDD model.
3. In addition, he did not use some features of junit5, such as parameterized testing
4. For the method of testExtend , it only recognizes 3 methods. Calls that do not recognize the parent class
2. JunitGenerate
Only the basic framework code can be generated, and the logic and test methods that I want to mock are not generated, which is not very useful.
@Test
public void testTestExtend ( ) throws Exception {
//TODO: Test goes here...
}
Talk about unit testing.Squaretest

The generation methods are very rich, and a very powerful point is that it can generate multiple branches. For example, if there is an if condition in the code logic, it can generate two tests, so that the branch that cannot be passed.
However, the biggest disadvantage is "paid software, not open source", which determines that we cannot use it unless it is specially needed. In addition , some other problems were found in the process of testing , such as inheritance, overloading and other problems, it did not solve the problem very well, and often could not identify the method that needs to be called.
Although it can't be used, it can still be used for reference.

Talk about unit testing.Create the best solution for automatic code generation

Since the plug-ins on the market are not particularly suitable, I decided to write a plug-in suitable for my own project (temporarily named JCode5). If you are interested, you can try it yourself.

Plugin installation
1.Download the idea plugin market, search for JCode5

plugin usage
The plugin has three functions

1.Generate test code, that is, generate unit tests.
2.Generate json data, usually used to generate test data, such as model. Used to parameterize tests.
3.Add test methods. With business development, classes may add functional methods. At this time, test methods can be added accordingly.
Locate the class to be tested, and locate the generator by shortcut key or menu , as follows, select JCode5 .

1. Generate test class
Three options are currently supported, which will be gradually improved in the future

The other two functions are similar, just try to use them directly.
2. Generated results---class + json data

@ParameterizedTest _
@ValueSource ( strings = {"/com/ cq /common/JCode5/testExtend.json " } )
public void testExtendTest ( String str) {
JSONObject arg = TestUtils.getTestArg (str);
Integer i = arg.getInteger ("Integer");
// Identify generic or collection classes
List stringList = JSONObject.parseArray( arg.getString ("List"),String.class);
String stringArg = arg.getString ("String");
String stringArg1 = arg.getString ("String");
String stringArg0 = arg.getString ("String");
// Identify four methods, including parent class calls and other method calls
when( testService.testBase(any(Integer.class))).thenReturn(stringArg);
when( testService.testMuti(any(List.class),any(Integer.class))).thenReturn(stringList);
when( testService.getStr(any(Integer.class))).thenReturn(stringArg0);
when( testService.testOther(any(Student.class))).thenReturn(stringArg1);
jCode5.testExtend( i );
// todo verify the result
}
As above, in addition to generating the basic code, it will also generate test data, which will generate all the test data required by the method in a json file , fully realized
"Separation of Data and Code "
Such as testExtend.json :
{
"Integer":1,
" String":"test ",
"List ":[
"test"
]
}
3. Supplementary judgment statement
This piece of pre-consideration has different verifications for different methods, so the current thinking is that developers should write the verification code themselves.
Precautions
After the code is automatically generated, although it can be run, as we mentioned earlier, the unit tests written for the purpose of writing unit tests are of little value. Our ultimate goal is to write a good test. The code is automatically generated, but it has limited capabilities after all, so we still need to verify it ourselves, such as
1.The code generated by this plugin needs the support of junit5 and mockito , and related dependencies need to be introduced when using it
2.Add assert verification logic to see if it is the desired result. Currently, the plugin does not automatically generate assertEquals and other assertion codes.
3.Use parametric testing capabilities, copy a generated json file and modify the input data, multiple sets of tests
Introduction to plugin implementation
The main implementation idea refers to the source code of dubbo 's SPI, that is, the part that automatically implements the adaptive SPI. Simply put, it is to obtain the code logic by reflection, and then generate the test code.

Later planning

1.Mock data can be customized, the current idea is
a.Fixed values such as the current String: test, Integer and boolean : 0 , 1
b.Testers use configuration templates, such as txt files containing keyValue pairs
c.Use Faker to automatically generate features for data with specific tendencies such as name, email, and phone
2.Automatic branch testing, the idea of which is currently mainly aimed at if, takes a certain amount of time.
3.other

Talk about unit testing. Write at the end

For automatic code generation, there are still many things that can be done, but some problems have yet to be solved. I hope to do our best to free our hands and improve the quality of our unit tests.
This pattern has been used in our project to increase 135 test cases (70% of single modules without mocks): the speed is one level higher than that of integration tests (pandora, spring, etc.). Code coverage is relatively impressive

Related Articles

Explore More Special Offers

  1. Short Message Service(SMS) & Mail Service

    50,000 email package starts as low as USD 1.99, 120 short messages start at only USD 1.00