Talk about unit testing

Author|Dust
Source|Ali Technical Public Number

Write before

Unit testing is certainly familiar to our developers, but it can be overlooked for a variety of reasons, especially in projects I come across where a wide variety of issues are found during the quiz phase, and I feel it is necessary to talk about unit testing.

Unit tests written for writing are of little value, but the benefits of a good unit test are very objective. The question is how do I write unit tests? How do I drive unit tests to be written?

1 Our present situation

Status 1: There are no unit tests for multiple projects at all.

Status two: Developers don't have the habit of writing unit tests or don't have time to write because they are rushing to get business records.

Status 3: Unit testing has been written as integrated testing, such as containers and databases, which results in long running time and meaningless unit testing.

Status 4: Too much dependence on integration testing.


The above is a test case of the two projects I looked for in aone, which merges and releases without considering unit tests.

From the development point of view, there are probably the following reasons for the above problems:

1. Development Cost

For the initial stage of the system, it may take a lot of time to write new business, for the old system is too large to start.

2. Maintenance Cost

Each time you modify a related class or refactor your code, you modify the corresponding unit tests.

3,ROI

Is input-output a positive return? Perhaps both managers and developers questioned this question, so sometimes there was no strong motivation.

Second, how to solve

It's all about cost, so how do we solve it?

So let's start with: the cost of development

The traditional writing of a unit test consists of the following:

  1. Test data (measured data, and dependent objects)
  2. test method
  3. Return Value Assertion
  @Test
  public void testAddGroup() {
    // data
    BuyerGroupDTO groupDTO = new BuyerGroupDTO();
    groupDTO.setGmtCreate(new Date());
    groupDTO.setGmtModified(new Date());
    groupDTO.setName("China");
    groupDTO.setCustomerId(customerId);
    // Method
    Result<Long> result = customerBuyerDomainService.addBuyerGroup(groupDTO);
    // Return Value Assertion
    Assert.assertTrue(result.isSuccess());
    Assert.assertNotNull(result.getData());
  }

A simple test is okay, but it can be a headache when the logic is complex and the input data is complex. How can we free our programmers'hands?

"Everything that works well must first be beneficial"

We are doing our best to reduce our development costs, which involves choosing our test framework and tools

1 Test Framework Selection

The first one is the choice of junit4 and junit5, [from junit4 to junit5]. I think one of the most convenient benefits is that we can parameterize tests, and we can configure our parameters more flexibly based on parameterized tests.

The results are as follows:

@ParameterizedTest
@ValueSource(strings = { "racecar", "radar", "able was I ere I saw elba" })
void palindromes(String candidate) {
    assertTrue(StringUtils.isPalindrome(candidate));
}

Better yet, junit5 provides extensions, such as the json format we commonly use. Here we use the json file as input:

  @ParameterizedTest
  @JsonFileSource(resources = {"/com/cq/common/KMPAlgorithm/test.json"})
   public void test2Test(JSONObject arg) {
    Animal animal = JSONObject.parseObject(arg.getString("Animal"),Animal.class); 
    List<String> stringList = JSONObject.parseArray(arg.getString("List<String>"),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
  }

2 mock framework

Then there's the framework for the other mock classes

Mockito: The syntax is particularly elegant, suitable for simulating container classes, and provides better assertions for function calls with empty return values. The disadvantage is that static methods cannot be simulated (supported in versions above 3.4.x)

EasyMock: Similar but stricter

PowerMock: Can be a complement to Mockito, such as testing static methods, but junit5 is not supported

Spock: Groovy-based unit testing framework

3 Database Layer

This paper mainly introduces H2 database, which is based on memory as a simulation of relational database, and runs to achieve the purpose of isolation.

Main configurations: ddl file path, dml file path. This is not detailed here.

However, it is difficult to define if you want to integrate the database. It is mainly used to validate sql syntax issues, but it is relatively heavy and is recommended for lightweight integration testing.

Three Junit5 and Mackito

MocKito is the framework and industry most commonly used for auto-generation, so here's a highlight, including problems encountered when using it.

1 Usage

Introduce dependencies separately, recommending the latest version

  1. Use spring-test family bucket

The usage of junit5 is not discussed here, but rather the ArgumentsProvider interface, which allows you to customize parameterized classes, such as its own ValueSource, EnumSource, and so on.

2 Introduction to Mockito main notes

First ask why, why Mockito is needed

Because: the spring framework is almost indispensable to java projects today, and its most famous one is IOC, where all beans are managed by containers, this brings us a problem with unit testing. If you want to do unit testing on beans, you need to start the containers, which will cause a lot of time overhead. So Mockito brings us a series of solutions that make it easy to test beans.

Suppose we want to unit test A.func() above.

@InjectMocks comment

There are two classes that represent classes that need to be injected into bean s

  1. The class under test, which is easy to understand, is what we test and, of course, inject bean s into it. Like A above
  2. In the class being tested, its real method needs to be executed, but it also needs the main bean, C above. We need to test the neeExec method, but we don't care about the details of B. In reality things like things, concurrent locks, etc. This category requires Mockito.spy(new C()) form, otherwise error will occur

@Mock

Represents the data to be mock ed, i.e. does not actually execute its method content, only follows our rules, or returns, such as when (). The nReturn() syntax.

Of course, when () is required to implement the real method. The nCallRealMethod () method.

@Spy

Represents that all methods follow the real way, such as some tool classes, conversion classes, and that we have written as bean s (strictly speaking, this needs to be written as static tool classes).

@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
  }

}

3 Mockito and junit5 FAQs

mock static method

mockito3.4 Support will start later, previous versions can use PowerMock as an auxiliary

Compatibility issues between Mockito and java versions

The error is as follows

Mockito cannot mock this class: xxx
Mockito can only mock non-private & non-final classes.

The reason is that versions 2.17.0 and earlier are compatible with Java 8

However, Java 11 is required after 2.18, and another package needs to be introduced in order to use Mockito in Java 8

Jupiter-api Version Compatibility Issues

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 due to inconsistent versions of api, engine, and params in junit5.

The second problem is that the jupiter-api version is too low for later versions than 5.7.0.

Four test code generation

With the framework selected, we still haven't solved our problem, "How can we save development costs?" Let's talk about this in this section, which is what I want to say.

Writing unit tests has always been a headache. Assembling all kinds of data can be frustrating without a lot of "xxxx can't be null" reports. So we have reason to imagine if there is a way to solve this.

1 Industry and Group Program Research

Before you do this, you must investigate whether there is a ready-made framework. The answer is yes, but unfortunately, I can't find the exact results I want. Let's take a look at these plug-ins:

public class BaseTest {
    protected TestService testService;
    public String baseTest() {
        return testService.testBase(1); // 4
    }
}
public class JCode5 extends BaseTest {
     public void testExtend(){
        String s = testService.testOther(new Student()); //1 
        // Call another method
        System.out.println(testBean());
        // Call base class methods
        baseTest();
    }
    // Using testService
    public String testBean() {
        testService.testMuti(new ArrayList<Integer>() {{add(1);}}, 2); //2
        return testService.getStr(12); //3
    }
    /**
     * Test Paradigm Class
     */
    public void testGeneric(Person person) {
        //test
        list.stream().forEach(a -> {
            System.out.println(a);
        });
        for (int i = 0; i < 2; i++) {
            Long aLong = testService.getLong("1213"
                , "12323");
            System.out.println(aLong);
        }
        System.out.println(testBean());
    }
}
public class TestService {
    public String testBase(Integer integer) {
        return "TestBase";
    }
    public List<String> testMuti(List<Integer> a, Integer c) {
        List<String> res = new ArrayList<>();
        res.add(a.toString() + c + "test muti");
        return res;
    }
     public String getStr(Integer integer) {
        return "TestService" + getInt();
    }
     public String testOther(Student student) {
        return student.getAge() + "age";
    }

}

As mentioned above, testExtend calls four methods of testService, and we compare the code generated by each plug-in.

TestMe

    @Test
    void testTestExtend() {
        when(testService.getStr(anyInt())).thenReturn("getStrResponse");
        when(testService.testMuti(any(), anyInt())).thenReturn(Arrays.<String>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.<String>asList("String"));

        jCode5.testGeneric(new Person());
    }

1. The generated code is basically logical, including the logic of bean s that require mock s.

2. But it omits the most important element, that is, the data, and simply uses the form of a constructor. This is obviously not appropriate for our DDD model.

3. In addition, he does not use some features of junit5, such as parameterized testing.

4. For the testExtend method, it only recognizes three methods. There is no call that identifies the parent class.

JunitGenerate

Only basic framework code can be generated, which is of little use to the logic and test methods that I think mock does not generate.

@Test
public void testTestExtend() throws Exception { 
//TODO: Test goes here... 
}

Squaretest

The generation method is very rich, and one of the most powerful points is that it can generate multiple branches, such as if conditions in code logic, and it can generate two tests that do not work.

However, the biggest disadvantage is "Charge software, no source", which means we can't use it unless it is specially needed. In addition, some other problems were found during the test process, such as inheritance, overload, and so on, which did not solve well and often did not recognize the methods that needed to be invoked.

Although not available, it can still be used for reference.

Five Create Code Automatically Generate Best Solutions

Since none of the plug-ins in the market are particularly suitable, I decided to write a plug-in that fits my project (temporarily named JCode5). If you are interested, you can try it yourself.

1 Plugin Installation

idea Plugin Market Download, Search JCode5

2 Plug-in Use

Plugins have three functions

  1. Generate test code, that is, generate unit tests.
  2. Generate json data, often used to generate test data, such as a model. Used to parameterize tests.
  3. Increase test methods, and as business development progresses, classes may add functional methods, at which point you can add test methods accordingly

Locate the class you want to test, shortcut keys or menus to generater, as follows, choose JCode5.

Generate Test Class

Three options are currently supported and will be improved in the future

The other two functions are similar, just try it once.

Generated result - 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 Living Collection Classes
    List<String> stringList = JSONObject.parseArray(arg.getString("List<String>"),String.class); 
    String stringArg = arg.getString("String"); 
    String stringArg1 = arg.getString("String"); 
    String stringArg0 = arg.getString("String"); 
    // Identify four methods, including parent calls, 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
  }

In addition to generating basic code, test data is generated, which generates all the test data required for this method in a json file and is fully implemented

"Separation of data and code"

Examples include testExtend.json:

{
  "Integer":1,
  "String":"test",
  "List<String>":[
    "test"
  ]
}

Supplementary Decision Statement

This piece of pre-consideration has different validations for different methods, so it's up to the developers to write their own validation code.

Matters needing attention

Once code has been automatically generated, it can be run, but as we mentioned earlier, unit tests written to write unit tests are of little value, and our ultimate goal is to write a good test. Code is generated automatically, but ultimately it has limited capabilities, so we need to verify it ourselves, for example

  1. The code generated by this plug-in needs support from junit5 and mockito, and dependent dependencies need to be introduced when using it
  2. Add assert checking logic to see if you want the result. Currently, the plug-in does not automatically generate assertEquals and other assertion code.
  3. Using parameterized testing capabilities, copy a generated json file and modify the input data, multiple sets of tests

3 Plugin implementation introduction

The main idea is to refer to the source code of dubbo's SPI, that is, to automatically implement the adaptive SPI. Simply put, to reflect and get the code logic, then generate the test code.

4 Post-planning

1. mock data can be customized, the current idea is

  • Fixed values such as current String: test, Integer, and boolean: 0, 1
  • Testers use configuration templates, such as txt files that contain keyValue pairs
  • Use Faker to feature auto-generate data for specific tendencies such as name, email, phone

2. Automatic Branch Testing, which is currently mainly for if, takes a while.

3. Other

Six at the end

There is still a lot to do with code auto-generation, but there are still some issues to be resolved. I hope to do my best to free our hands and improve the quality of our unit tests.

This pattern has been used in our project to add 135 test cases (70% for single modules except mock s): one level faster than integrated tests (pandora, spring, etc.). Code coverage is relatively impressive.

Elasticsearch Battalion

click here To view details.

Keywords: Java Database JSON Spring SQL

Added by BZero on Mon, 14 Feb 2022 19:51:11 +0200