"The Art of Unit Testing" Reading Notes - Best Practices for Testing Code

  • Test hierarchy and organization

        1. Run unit tests and integration tests in automated daily builds, such as using continuous integration tools to automate builds;

        2. Based on speed and type layout test:

        It's easy to differentiate between integration and unit tests based on the time it takes to run the tests, place integration and unit tests in separate directories, and specify how often unit and integration tests are run.

        3. Ensure that part of the source code management during testing is jointly managed by the version manager.

        4. Map the test class to the code under test

        When creating test classes, how should they be organized and placed? We want to find all relevant tests for a project, all relevant tests for a class, and all relevant tests for a method. We can do it the following way:

        (1) The test class and the tested class are placed in the same project;

        (2) The test class and the tested class should try to keep the same or similar package level;

        (3) For the naming of multiple test methods for the same tested method, such as userLoginTest_Success, userLoginTest_Fail, etc. can be used.

       5. Build test APIs, such as using test class inheritance mode, creating test tool classes, etc.;

 

  • Pillars of Good Unit Testing

        1. Write reliable tests

      (1) Decide when to delete or modify tests

        When will unit tests fail?

        Product defects, do not need to modify the test, just fix the product defects;

        Test defects, need to fix the test;

        Product semantics or API changes, usage changes, and tests need to be modified;

        Rename unclear tests, refactor unreadable tests.

        Remove duplicate tests.

        (2) Avoid logic in tests

        If the unit test contains switch, if, else, foreach, for, while and other statements, it means that your test contains unnecessary logic.

        If you need complex and large tests, such as multi-threaded tests, you should write such tests in a package marked as integration tests.

        The following test code also contains unnecessary logic, inadvertently repeating the logic user + greeting of the product code,        

1 public void addString(){
2         String user = "USER";
3         String greeting = "GREETING";
4         String actual = MessageBuilder.Build(user, greeting);
5 
6         assertEqual(user + greeting, actual);
7 }

 

        Change to the following code to eliminate the import logic:

1 public void addString(){
2         String user = "USER";
3         String greeting = "GREETING";
4         String actual = MessageBuilder.Build(user, greeting);
5 
6         assertEqual("USER GREETING", actual);
7 }

 

        (3) Test only one concern

        Keeping a single assertion in a test method makes it easier to diagnose what's wrong.

        (4) Separate unit tests and integration tests

        Unit tests are easy to run, integration tests are more likely to fail, and if they are not stable enough, developers skip all tests and fail to function as unit tests.

        (5) Use code review to ensure code coverage

        Without code review, the conclusion of code coverage statistics is not convincing. Since the developer may not write an assertion in the test method, the test will always pass.

        Code reviews help improve the skill level of your team, as well as create readable, high-quality code that will last for years and give you confidence.

 

        2. Write maintainable tests

        (1) Test private or protected methods

        make the method public;

        Extract methods to new classes;

        Make the method static.

 

        (2) Remove duplicate code

        Extraction helper methods to remove duplicate code.

        Use @Before or @After to remove duplicate code;

 

        (3) Maintainable methods use @Before

        limitation:

        The @Before method is only used when initialization work is required;

        The @Before method should only contain code that applies to all tests in the current test class, otherwise the method will be harder to read and understand.

        Try not to use the @Before method, but encapsulate the auxiliary initialization method, and call each test method manually. This increases code readability.

 

        (4) Implement test isolation

        Definition: A test should always run independently and not depend on any other tests.

        Test the stink of isolation:

        Forced test order: tests need to be executed in a specific order, or information from other test results;

        Hidden test calls: tests call other tests;

        Shared state corruption: testing the state in shared memory without rolling back the state;

        External shared state corruption: Integration tests share resources without rolling back them;

 

        (5) Avoid multiple assertions on different concerns       

1 @Test
2 public void CheckVariousUsmResult(){
3         assertEqual(3, sum(1001, 1, 2));
4         assertEqual(3, sum(1, 1001, 2));
5         assertEqual(3, sum(1, 2, 1001));
6 }

 

        The above unit test uses three simple assertions, runs three different sub-function tests, and hopefully saves some time. What's wrong with doing this? If the assertion fails, an exception will be thrown, and subsequent assertions will not be executed, that is, subsequent functions will not be tested. But in this case, even if one assertion fails, you still want to know the result of the other assertions.

        You can implement this test in other ways:

        Create a separate test for each assertion;

        Use parameterized tests (.Net support, Java does not seem to support currently);

        Put the assertion in a try-catch block.

 

        (6) When comparing multiple states of an object, there are two ways:

        Method 1. Multiple assertion method        

 1 @Test
 2 public void compare(){
 3         String userName = "zhangf";
 4         String realName = "张飞";
 5         String id = "1001";
 6         User user = new User(id, userName, realName);
 7  
 8         assertEqual(id, user.getId());
 9         assertEqual(userName, user.getUserName());
10         assertEqual(realName, user.getRealName());
11 }

 

         Method 2, single assertion method, toString() comparison

1 @Test
2 public void compare(){
3         String userName = "zhangf";
4         String realName = "张飞";
5         String id = "1001";
6         User user = new User(id, userName, realName);
7           assertEqual("id:"+id+",userName:"+userName+",realName:"+realName, user.toString());
8 }

 

         The first way makes it seem that testing multiple functions is less readable, and the second way is highly readable. The second method is recommended.

 

        (7) Avoid over-specifying

        Overspecifying is making assumptions about how the unit under test implements its internal behavior, rather than just checking the correctness of its final behavior.

        There are mainly the following situations:

        A test asserts the internal state of an object under test;

        The test uses multiple mock objects;

        Tests use mock objects when stubbing is required;

        The test specifies an order or uses an exact match when it is not necessary. For example, to make an exact match assertion on the returned string, but actually only need to make an assertion on a part of the string, we can use String.contains() instead of String.equal().

 

        3. Write readable unit tests

        (1) Unit test naming

        The test method name consists of three parts: the name of the method to be tested, the test scenario, and the expected behavior.

        For example, to test user login, the scenario is that the verification code is required after multiple logins, and the expected behavior is that the password fails and fails, which can be named void userLogin_requirePictureNum_fail(){...}

 

        (2) Variable naming

        Reasonably named variables will ensure that people reading the test can easily understand what you are trying to verify. Because unit tests not only play the role of testing, but also serve as a kind of documentation for the API.

        Bad naming is like a magic number, assertEqual(-100, result), you can't see what -100 means, assign -100 to a variable named expressively, such as "COULD_NOT_READ_FILE = -100;", and then use If the variable is equal, it is easier to understand the purpose of the assertion.

 

        (3) Meaningful assertions

          Try not to write your own custom assertion information, if you must, name it clearly.

 

        (4) Assertion and operation separation

         Counterexample:

         assertEqual(COULD_NOT_READ_FILE, log.GetLineCount("aaa.txt"))   

         Normal example:

        int result = log.GetLineCount("aaa.txt"); 

        assertEqual(COULD_NOT_READ_FILE, result);

 

        (5)@Before和@After

         These two ways are so often abused that the method is completely unreadable.

        A case of abuse: preparing stubs and mock objects in @Before so that the person reading the test doesn't realize the mock object is used in the test and doesn't know what the expected value of the object is.

        Test readability would be better if the mock object was initialized directly by the test method itself, setting all expected values.

 

        Takeaway: Tests grow and change with the system under test.

 

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325847313&siteId=291194637