[JAVA] Since the unit test of the PJ I was in charge of was a hell picture, I will publish my own guide (?)

Introduction

Hello, I'm Negero.

I was assigned to a Java development project in the first half of 2020, but the UT code created by the members, including myself, was __chaos here.

For example -Test with mixed JUnit4 and JUnit5 code -A test that just calls a method without asserting anything · Non-reproducible tests that may or may not succeed ・ Normal system test using a mock that succeeds regardless of whether the status code is 200, 404, or 500. etc···.

This PJ will continue development in the second half to implement additional functions, but I think it is not good as it is. I decided to make a unit test guide (?) After consulting with senior employees.

In this article, we will publish the guide.

However, please be aware that it is a guide for __chaotic sites __ as described above.

Also, the author himself is not familiar with unit tests. If you have any opinions such as "I should do this more!" Or "It's wrong!", Please write them in the comments!

I hope this article helps those who are wondering "How do I proceed with unit testing?"!

__ The following is a description assuming JUnit5. __

Basic

test case

--Simplify without including too many tests in one test case ――The analysis when it becomes NG becomes troublesome --Test in 4 phases in the test case -Write the test in 4 phases of [Preparation] → [Execution] → [Verification] → [Post-processing]

Add.java


//Tested class
public class Calc {

    public int add(int a, int b) {
        return a + b;
    }

}

CalcTest.java


import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;

//Test class
class CalcTest {

    @Test
Passing 2 and 3 to the void add method returns 5() {
        //【Advance preparation】
        Calc sut = new Calc();
        int expected = 5;

        //[Execution]
        int actual = sut.add(2, 3);

        //[Verification]
        assertEquals(expected, actual);

        //[Post-processing]
        /*
         *Instance destruction, file close, etc.
         *Write processing if necessary
         */
    }

}

--In the [Execution] phase, __ Call only one method or constructor to be evaluated __ ――Except for cross-sectional evaluation, unit tests do not do much cross-sectional testing in the first place. -[Verification] In the phase, evaluation is performed using the syntax assertEquals (expected value, inspection target) `` `. --Other assertions include assertTrue / False, assertNull / NotNull, assertSame / NotSame, so use them properly as needed. --In JUnit4, assertThat (inspection target, matcher function (expected value)) `` ` However, the asserThat and Matcher functions have been abolished in JUnit5. --It is possible to use it by loading the corresponding library. --Use assertThrows to test that exceptions are being thrown

Bookshelf.java


//Tested class
public class Bookshelf {

    String[] books = new String[3];
    public String addBook(int i, String title) {
        books[i] = title;
        return title;
    }

    public List<String> readBook(String str) throws IOException {
        Path path = Paths.get(str);
        List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
        return lines;
    }

}

BookshelfTest.java


import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;
  
import java.io.IOException;

//Test class
class BookshelfTest {

    @Test
When the addBook method is called with 3 or more specified for the void index, ArrayIndexOutOfBoundsException is thrown.() {
        //【Advance preparation】
        Bookshelf sut = new Bookshelf();

        //[Execution]
        assertThrows(ArrayIndexOutOfBoundsException.class, () -> sut.addBook(3, "JavaTextBook"));
    }

    @Test
void Throws IOException when calling readBook method with non-existent file specified() {
        //【Advance preparation】
        Bookshelf sut = new Bookshelf();

        //[Execution]
        assertThrows(IOException.class, () -> sut.readBook("hoge.txt"));
    }

}

About naming conventions

--The test class name is named by adding "Test" to the end of the class name to be tested. --For example, the test class name of the Config class is "ConfigTest". --Test method (= test case) __ Write the content of the test concisely in Japanese __ --Example) If you pass 2 and 3 to the add method, 5 is returned () --Avoid test names such as Addition (), Addition_Case 1 (), Addition_Case 2 (), ... that you do not know what you are evaluating. --In JUnit5, you can set the display name of the test with the `@ DisplayName``` annotation. --Since there are restrictions such as "you cannot start with a numerical value" in the method name, it may be convenient to add this annotation. --The variable name of the object of the evaluation target class is __sut__ (System Under Test). --The variable name of the execution result is __actual__ or __actualXXX__, the variable name of the expected value is __expected__ or __expectedXXX__, etc., so that you can see the execution result and the expected value, respectively. --You don't have to put it in a variable -- Immediate value is good if the immediate value is easier to understand, such as ```assertEquals (0, actual); `` `.

Recommended features of JUnit 5

Test structuring

--By structuring tests, each test case can be grouped by preprocessing and purpose. --Achieve layering by annotating the inner class with `@ Nested``` --You can also write pre-processing and post-processing before executing each test by annotating @ BeforeEach``` and `` @ AfterEach```. --It is also possible to select and run the test for each hierarchy

BookshelfTest.java


import static org.junit.jupiter.api.Assertions.*;
  
import java.io.IOException;
  
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

class BookshelfTest {

    @Nested
Up to 3 class books can be stored on the bookshelf{
        private Bookshelf sut;
        private String expected1 = "JavaText";
        private String expected2 = "PythonText";
        private String expected3 = "RubyText";
      
        @BeforeEach
        void setUp() {
            sut = new Bookshelf();
        }

        @Test
You can add a book to the void addBook method() {
            //【Advance preparation】

            //[Execution]
            String actual = sut.addBook(0, expected1);
        
            //[Verification]
            assertEquals(expected1, actual);
        }

        @Test
Two books can be added to the void addBook method() {
            //【Advance preparation】

            //[Execution]
            String actual1 = sut.addBook(0, expected1);
            String actual2 = sut.addBook(1, expected2);

            //[Verification]
            assertEquals(expected1, actual1);
            assertEquals(expected2, actual2);
        }

        @Test
You can add 3 books to the void addBook method() {
            //【Advance preparation】

            //[Execution]
            String actual1 = sut.addBook(0, expected1);
            String actual2 = sut.addBook(1, expected2);
            String actual3 = sut.addBook(1, expected3);

            //[Verification]
            assertEquals(expected1, actual1);
            assertEquals(expected2, actual2);
            assertEquals(expected3, actual3);
        }
    }

    @Nested
class Abnormal test{
        private Bookshelf sut;

        @BeforeEach
        void setUp() {
            sut = new Bookshelf();
        }

        @Test
When the addBook method is called with 3 or more specified for the void index, ArrayIndexOutOfBoundsException is thrown.() {
            //【Advance preparation】
            Bookshelf sut = new Bookshelf();

            //[Execution]
            assertThrows(ArrayIndexOutOfBoundsException.class, () -> sut.addBook(3, "JavaTextBook"));
        }

        @Test
void Throws IOException when calling readBook method with non-existent file specified() {
            //【Advance preparation】
            Bookshelf sut = new Bookshelf();

            //[Execution]
            assertThrows(IOException.class, () -> sut.readBook("hoge.txt"));
        }
    }

}

Parameterization test

--Parameterization tests allow you to run multiple times with different arguments --In the parameterized test, annotate @ ParameterizedTest instead of @ Test --Specify the source of the argument by adding an annotation --There are several types, but here we exemplify the `` `@ CsvSource annotation. --For other types, refer to JUnit5 User Guide. --It is also possible to read an external csv file and use it as the source of the argument (`` `@ CsvFileSource```)

BookshelfTest.java


import static org.junit.jupiter.api.Assertions.*;
  
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
  
class BookshelfTest {

    @ParameterizedTest
    @CsvSource({
        "0, JavaText",
        "1, PythonText",
        "2, RubyText"
    })
3 bookshelves can be stored in void Bookshelf(int index, String title) {
        Bookshelf sut = new Bookshelf();
        assertEquals(title, sut.addBook(index, title));
    }

}

Tagging

--It is possible to tag each test code by adding the @ tag annotation. --You can execute only the test associated with the specified tag or exclude it from execution.

BookshelfTest.java


import static org.junit.jupiter.api.Assertions.*;
  
import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

class BookshelfTest {

    @Test
You can add a book to the void addBook method() {
        //【Advance preparation】
        Bookshelf sut = new Bookshelf();
        String expected1 = "JavaText";

        //[Execution]
        String actual = sut.addBook(0, expected1);

        //[Verification]
        assertEquals(expected1, actual);
    }

    @Tag("Abnormal system")
    @Test
When the addBook method is called with 3 or more specified for the void index, ArrayIndexOutOfBoundsException is thrown.() {
        //【Advance preparation】
        Bookshelf sut = new Bookshelf();

        //[Execution]
        assertThrows(ArrayIndexOutOfBoundsException.class, () -> sut.addBook(3, "JavaTextBook"));
    }

}

Through development in the first half of 2020

--The pros and cons of testing private methods are controversial. -Do you write tests for private methods? ――It is better to set guidelines as a team before development --For example, the following are examples of support for testing private methods: --Test via public method --Cut out to another class and make it a public method --(Slightly) increase the visibility of the test target --Access with reflection and write tests --It is preferable to write tests for methods automatically generated using Lombok (mainly setters and getters). --If you switch to an OSS other than Lombok, the minimum unit for verifying compatibility is that test. --However, this PJ does not include it in the coverage total. --Don't write tests just to improve coverage --_ Increase coverage __ and evaluate __ features __ Write test code with both in mind --For example, if almost every test just calls a method to increase coverage, it's not testing the functionality of that class. --Evaluate correctly in the test code --For example, when testing a method that adds some element to a collection, not only does the number of elements in the collection match the number of added elements, but each element of the collection also matches the added element. It can be said that it was tested correctly only after checking up to --Write with the test code as a document in mind --The first manual that explains how to use the method is the test code. --Create a test that always succeeds in your environment (__ reproducible test __)

References

-JUnit5 User Guide -JUnit5 usage memo -T-wada's blog -[Introduction to JUnit Practice ~ Systematic Unit Test Techniques](https://www.amazon.co.jp/JUnit%E5%AE%9F%E8%B7%B5%E5%85%A5%E9% 96% 80-% E4% BD% 93% E7% B3% BB% E7% 9A% 84% E3% 81% AB% E5% AD% A6% E3% 81% B6% E3% 83% A6% E3% 83 % 8B% E3% 83% 83% E3% 83% 88% E3% 83% 86% E3% 82% B9% E3% 83% 88% E3% 81% AE% E6% 8A% 80% E6% B3% 95 -WEB-PRESS-plus / dp / 477415377X)

Recommended Posts

Since the unit test of the PJ I was in charge of was a hell picture, I will publish my own guide (?)
How a liberal arts engineer passed Java Silver in half a year after joining the company
I was confused because there was a split in the Array
The daily report table of my parents' company was made into a web application. (For smartphones)
Approximately three and a half months after joining the company, an output article to feed on the reviews
Since the unit test of the PJ I was in charge of was a hell picture, I will publish my own guide (?)
[Past POST] I will publish a part of the answer memo when I was a mentor of TECH :: CAMP
A super beginner has completed the Spring introductory book, so I will summarize it in my own way
I was confused because there was a split in the Array
I checked the automatic unit test creation tool (end of 2019 version)
I passed the Java test level 2 so I will leave a note