Learn Spring Boot: Spring Boot Junit unit test

preface

JUnit is a regression testing framework, which is used by developers to implement unit testing of applications, speed up programming and improve coding quality.

JUnit test framework has the following important features:

  • test tools
  • test suite
  • Test runner
  • Test classification

Understand Junit's basic methods

Join dependency

Add dependencies in pom.xml:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <scope>test</scope>
    <version>4.12</version>
</dependency>

Create test classes and test methods

  1. The naming rules of test classes are generally xxtest.java;
  2. The test methods in the test class can have prefixes. This depends on the unified standard, so sometimes it is found that other test methods have test prefixes;
  3. And the @ Test annotation is added to the Test method.

In IDEA, select the current class name, use the shortcut key ALT + ENTER (WIN), and select Create Test. Press enter to enter the option of generating test class. Press enter again to quickly generate test class.

After OK, you will find that the generated test class is in the src/test directory, and the package names of the test class and the source code are consistent. Result after generation (note that the generated method name does not add test):

public class HelloServiceImplTest {

    @Before
    public void setUp() throws Exception {
    }

    @After
    public void tearDown() throws Exception {
    }

    @Test
    public void say() {
    }
}

Annotations in JUnit

  • @BeforeClass: all tests are executed only once and must be static void
  • @Before: initializes the method and executes before each test method of the current test class.
  • @Test: test method, where you can test the expected exception and timeout
  • @After: release resources and execute each test method of the current test class
  • @AfterClass: all tests are executed only once and must be static void
  • @Ignore: ignored test method (only effective when testing a class, and it is invalid to execute the test method alone)
  • @RunWith: you can change the test runner. The default value is org.junit.runner.Runner

The execution order of a unit test class is:

@BeforeClass` –> `@Before` –> `@Test` –> `@After` –> `@AfterClass

The calling sequence of each test method is:

@Before` –> `@Test` –> `@After

Timeout tests

If a Test case takes more time than the specified number of milliseconds, Junit will automatically mark it as a failure. The timeout parameter is used with the @ Test annotation. Now let's look at @ test(timeout) in the activity.

    @Test(timeout = 1000)
    public void testTimeout() throws InterruptedException {
        TimeUnit.SECONDS.sleep(2);
        System.out.println("Complete");
    }

The above test will fail, and an exception org.junit.runners.model.testtimedoutexception will be thrown after one second: Test timed out after 1000 milliseconds

Abnormal test

You can Test whether the code throws the desired exception. The expected parameter is used with the @ Test annotation. Now let's look at @ Test(expected) in the activity.

    @Test(expected = NullPointerException.class)
    public void testNullException() {
        throw new NullPointerException();
    }

The above code will test successfully.

Suite test

public class TaskOneTest {
    @Test
    public void test() {
        System.out.println("Task one do.");
    }
}

public class TaskTwoTest {
    @Test
    public void test() {
        System.out.println("Task two do.");
    }
}

public class TaskThreeTest {
    @Test
    public void test() {
        System.out.println("Task Three.");
    }
}

@RunWith(Suite.class) // 1. Change the test run mode to Suite
// 2. Pass in the test class
@Suite.SuiteClasses({TaskOneTest.class, TaskTwoTest.class, TaskThreeTest.class})
public class SuitTest {
    /**
     * The entry class of the test suite only organizes the test classes to test together without any test methods,
     */
}

Parametric test

Junit 4 introduces a new functional parametric test. Parametric testing allows developers to run the same test repeatedly with different values. You will follow five steps to create parametric tests.

  • Annotate the test class with @ RunWith(Parameterized.class).
  • Create a public static method annotated by @ Parameters, which returns * * a collection of objects (array) * * as the test data collection.
  • Create a public constructor that accepts the same thing as a line of test data.
  • Create an instance variable for each column of test data.
  • Use instance variables as a source of test data to create your test cases.
//1. Change the default test runner to RunWith(Parameterized.class)
@RunWith(Parameterized.class)
public class ParameterTest {
    // 2. Declare variables to store expected values and test data
    private String firstName;
    private String lastName;

    //3. Declare a public static method with the return value of Collection and modify it with @ Parameters
    @Parameterized.Parameters //
    public static List<Object[]> param() {
        // Here I give two test cases
        return Arrays.asList(new Object[][]{{"Mike", "Black"}, {"Cilcln", "Smith"}});
    }

    //4. Declare a public constructor with parameters for the test class and assign values to the declared variables
    public ParameterTest(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    // 5. Test and find that it will test all test cases
    @Test
    public void test() {
        String name = firstName + " " + lastName;
        assertThat("Mike Black", is(name));
    }
}

Hamcrest

JUnit 4.4 combined with Hamcrest provides a new assertion syntax - assertThat.

Syntax:

assertThat( [actual], [matcher expected] );

assertThat uses the Matcher matcher of Hamcrest. Users can use the matching criteria specified by the Matcher to accurately specify some conditions they want to meet. It has strong readability and is more flexible to use.

For some specific matching rules, you can view the source code.

Using JUnit in Spring Boot

The Spring framework provides a special test module (Spring test) for application integration testing. In Spring Boot, you can quickly start and use it through the Spring Boot starter test launcher.

Join dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

Spring Boot test

// Get the startup class, load the configuration, determine the loading method for loading the Spring program, and go back to find the main configuration startup class (annotated by @ SpringBootApplication)
@SpringBootTest
// Let JUnit run the Spring test environment and get the context support of the Spring environment
@RunWith(SpringRunner.class)
public class EmployeeServiceImplTest {
    // do 
}

Spring MVC testing

When you want to write unit test code for Spring MVC controller, you can use @ WebMvcTest annotation. It provides a self configured MockMvc, which can quickly test the MVC controller without completely starting the HTTP server.

  1. Controller to be tested:

    @RestController
    @RequestMapping(value = "/emp", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public class EmployeeController {
        private final EmployeeService employeeService;
    
        @Autowired
        public EmployeeController(EmployeeService employeeService) {
            this.employeeService = employeeService;
        }
    
        @GetMapping
        public ResponseEntity<List<EmployeeResult>> listAll() {
            return ResponseEntity.ok(employeeService.findEmployee());
        }
    }
    
  2. Write the test class of MockMvc:

    @RunWith(SpringRunner.class)
    @WebMvcTest(EmployeeController.class)
    public class EmployeeController2Test {
        @Autowired
        private MockMvc mvc;
    
        @MockBean
        private EmployeeService employeeService;
    
        public void setUp() {
            // Set the body returned by this method to be always empty
            Mockito.when(employeeService.findEmployee()).thenReturn(new ArrayList<>());
        }
    
        @Test
        public void listAll() throws Exception {
            mvc.perform(MockMvcRequestBuilders.get("/emp"))
                    .andExpect(status().isOk()) // Expecting return status code 200
                    // JsonPath expression  https://github.com/jayway/JsonPath
                    //. andExpect(jsonPath("$[1].name").exists()) / / the expected return value is an array, and the name of the second value exists, so the test fails here
                    .andDo(print()); // Print the returned http response information
        }
    }
    
    

    When @ WebMvcTest annotation is used, only some beans can be scanned. They are:

    • @Controller
    • @ControllerAdvice
    • @JsonComponent
    • Filter
    • WebMvcConfigurer
    • HandlerMethodArgumentResolver
      Other regular @ Component beans (including @ Service, @ Repository, etc.) will not be loaded into the Spring test environment context.
      So I used data piling above, Mockito in the last section of this article.
  3. We can also inject the Spring context environment into MockMvc, and write the test class of MockMvc as follows:

    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class EmployeeControllerTest {
        /**
         * Interface to provide configuration for a web application.
         */
        @Autowired
        private WebApplicationContext ctx;
    
        private MockMvc mockMvc;
    
        /**
         * Initialize MVC environment
         */
        @Before
        public void before() {
            mockMvc = MockMvcBuilders.webAppContextSetup(ctx).build();
        }
    
        @Test
        public void listAll() throws Exception {
            mockMvc
                    .perform(get("/emp") // Relative address of the test
                    .accept(MediaType.APPLICATION_JSON_UTF8) // accept response content type
                    )
                    .andExpect(status().isOk()) // Expecting return status code 200
                    // JsonPath expression  https://github.com/jayway/JsonPath
                    .andExpect(jsonPath("$[1].name").exists()) // Here, it is expected that the return value is an array, and the name of the second value exists
                    .andDo(print()); // Print the returned http response information
        }
    }
    

    It is worth noting that you need to build MockMvc using WebApplicationContext first.

Spring Boot Web test

When you want to start a complete HTTP server to write test code for Spring Boot Web applications, you can use the @ SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT) annotation to open a random available port. Spring Boot provides a TestRestTemplate template for testing REST calls, which can resolve the relative address of the linked server.

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class EmployeeController1Test {
    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    public void listAll() {
        ResponseEntity<List> result = restTemplate.getForEntity("/emp", List.class);
        Assert.assertThat(result.getBody(), Matchers.notNullValue());
    }
}

In fact, the previous test results are not very correct. Only one List can be received, which adds a lot of trouble to the test code. Fortunately, a solution is finally found:

    @Test
    public void listAll() {
        // Because I returned a List type, I couldn't think of a solution. The solution was given on the Internet, using the exchange function instead
        //public <T> ResponseEntity<T> exchange(String url, HttpMethod method,
        //			HttpEntity<?> requestEntity, ParameterizedTypeReference<T> responseType,
        //			Object... urlVariables) throws RestClientException {
        ParameterizedTypeReference<List<EmployeeResult>> type = new ParameterizedTypeReference<List<EmployeeResult>>() {};
        ResponseEntity<List<EmployeeResult>> result = restTemplate.exchange("/emp", HttpMethod.GET, null, type);
        Assert.assertThat(result.getBody().get(0).getName(), Matchers.notNullValue());
    }

Spring Data JPA test

We can use the @ DataJpaTest annotation to indicate that only JPA is tested@ DataJpaTest annotation only scans @ entitybeans and assembles the Spring Data JPA Repository. Other regular @ Component (including @ Service, @ Repository, etc.) beans will not be loaded into the Spring test environment context.

@DataJpaTest also provides two test methods:

  1. Using the memory database h2database, Spring Data Jpa test adopts this method by default;
  2. Use a real-world database.
Test with in memory database
  1. By default, @ DataJpaTest uses an in memory database for testing. You do not need to configure and enable a real database. You only need to declare the following dependencies in the pom.xml configuration file:

    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
    </dependency>
    

    gradle file:

    testCompile('com.h2database:h2')
    
  2. Write test method:

    @RunWith(SpringRunner.class)
    @DataJpaTest
    public class EmployeeDaoTest {
    
        @Autowired
        private EmployeeDao employeeDao;
    
        @Test
        public void testSave() {
            Employee employee = new Employee();
            EmployeeDetail detail = new EmployeeDetail();
            detail.setName("kronchan");
            detail.setAge(24);
            employee.setDetail(detail);
            assertThat(detail.getName(), Matchers.is(employeeDao.save(employee).getDetail().getName()));;
        }
    }
    
Test with real database

If you need to use the database in the real environment for testing, you need to replace the default rule and use @ AutoConfigureTestDatabase(replace = Replace.NONE) annotation:

@RunWith(SpringRunner.class)
@DataJpaTest
// Add AutoConfigureTestDatabase annotation
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class EmployeeDaoTest {

    @Autowired
    private EmployeeDao employeeDao;

    @Test
    public void testSave() {
        Employee employee = new Employee();
        EmployeeDetail detail = new EmployeeDetail();
        detail.setName("kronchan");
        detail.setAge(24);
        employee.setDetail(detail);
        assertThat(detail.getName(), Matchers.is(employeeDao.save(employee).getDetail().getName()));;
    }
}

Transaction control

Execute the above test of new data, and it is found that the test passes, but the database does not add new data. By default, transactions are rolled back at the end of each JPA test. This can prevent the test data from polluting the database to a certain extent.

If you do not want the transaction to be rolled back, you can use the @ Rollback(false) annotation, which can be marked at the class level for global control, or at a specific method level that does not need to perform transaction rollback.

You can also explicitly use the annotation @ Transactional to set the transaction and transaction control level to enlarge the scope of the transaction.

Mockito

This part refers to Unit testing using Mockito and spring test

JUnit and spring test can basically meet most unit tests. However, due to the increasing complexity of the current system, there are more and more dependencies on each other. Especially in the system after microservicing, the code of one module often depends on several other modules. Therefore, when doing unit testing, it is often difficult to construct the required dependencies. For a unit test, we only care about a small function, but in order to run this small function, we may need to rely on a bunch of other things, which makes the unit test impossible. Therefore, we need to introduce Mock test in the retest process.

The so-called Mock test is to use a virtual object (Mock object) to simulate some objects that are not easy to construct or have nothing to do with the unit test but depend on the context, so that the unit test can be carried out.

For example, the dependency of a piece of code is:

When we want to conduct unit testing, we need to inject B and C into A, but C depends on D and D depends on E. As A result, A's unit test is difficult to carry out.
However, when we use Mock to simulate objects, we can decouple this dependency, only care about the test of A itself, all the B and C it depends on, use the objects from Mock, and specify A clear behavior for MockB and MockC. Like this:

Therefore, when we use Mock, those objects that are difficult to build become simulation objects. We only need to do Stubbing in advance. The so-called pile data is to tell the Mock object what behavior process to perform when interacting with it. For example, when calling the b() method of B object, we expect to return a true, which is the expectation of setting pile data.

Basics

Mockito concise tutorial

Used in Spring Boot

Mockito is also used in the above Spring MVC test,

Spring boot starter test comes with mockito core.

Basic business
@Entity
@Data
@NoArgsConstructor
public class User implements Serializable {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(unique = true, nullable = false, length = 50)
    private String username;

    private String password;

    @CreationTimestamp
    private Date createDate;

    public User(Long id, String username) {
        this.id = id;
        this.username = username;
    }
}

public interface IUserRepository extends JpaRepository<User, Long> {
    boolean updateUser(User user);
}

@Service
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class UserServiceImpl implements IUserService {
    private final IUserRepository userRepository;

    @Override
    public User findOne(Long id) {
        return userRepository.getOne(id);
    }

    @Override
    public boolean updateUsername(Long id, String username) {
        User user = findOne(id);
        if (user == null) {
            return false;
        }
        user.setUsername(username);
        return userRepository.updateUser(user);
    }
}
Test class
public class IUserServiceTest {
    private IUserService userService;

    //@Mock
    private IUserRepository userRepository;

    @Before
    public void setUp() throws Exception {
        // Simulate all objects annotated with @ Mock
        // MockitoAnnotations.initMocks(this);
        // You can mock a single object without annotations
        userRepository = Mockito.mock(IUserRepository.class);
        // Construct the tested object
        userService = new UserServiceImpl(userRepository);
        // When the execution parameter of getOne function of userRepository is 1, set the returned result User
        Mockito.when(userRepository.getOne(1L)).thenReturn(new User(1L, "kronchan"));
        // When the execution parameter of getOne function of userRepository is 2, set the returned result null
        Mockito.when(userRepository.getOne(2L)).thenReturn(null);
        // In addition, when the execution parameter of getOne function of userRepository is 3, the setting result throws an exception
        Mockito.when(userRepository.getOne(3L)).thenThrow(new IllegalArgumentException("The id is not support"));
        // In addition, when userRepository.updateUser executes any User type parameter, the returned result is true
        Mockito.when(userRepository.updateUser(Mockito.any(User.class))).thenReturn(true);
    }

    @Test
    public void testUpdateUsernameSuccess() {
        long userId = 1L;
        String newUsername = "new kronchan";
        // Method for testing a service
        boolean updated = userService.updateUsername(userId, newUsername);
        // Inspection results
        Assert.assertThat(updated, Matchers.is(true));
        // Verifies certain behavior <b>happened once</b>.
        // Once a mock object is created, it will automatically record its interaction behavior. Verify whether the method is called through the verify(mock).someMethod() method.
        // Verify whether userRepository.getOne(1L) has been called after calling the above service method,
        Mockito.verify(userRepository).getOne(userId);
        // Methods that have not been called can be tested conditionally:
        //   Mockito.verify(userRepository).deleteById(userId);
        //   The test fails:
        //    Wanted but not invoked:
        //      userRepository.deleteById(1L);
        //    However, there were exactly 2 interactions with this mock:
        //      userRepository.getOne(1L);
        //      userRepository.updateUser(
        //         User(id=1, username=new kronchan, password=null, createDate=null)
        //      );

        //  In the updateUsername function, we have called other functions that have been built. Now let's verify the parameters entered into other functions
        //Construct a parameter catcher to capture method parameters for verification
        ArgumentCaptor<User> userCaptor = ArgumentCaptor.forClass(User.class);
        // Verify that the updateUser method has been called and that the parameters are captured
        Mockito.verify(userRepository).updateUser(userCaptor.capture());
        // Get parameter updatedUser
        User updatedUser = userCaptor.getValue();
        // Verify that the input parameter is expected
        Assert.assertThat(updatedUser.getUsername(), Matchers.is(newUsername));
        //Ensure that the relevant methods of all Mock objects in this test case have been verified
        Mockito.verifyNoMoreInteractions(userRepository);
        // If there is an interaction, but we do not verify, an error will be reported,
        //      org.mockito.exceptions.verification.NoInteractionsWanted:
        //      No interactions wanted here:
        //      -> at com.wuwii.service.IUserServiceTest.testUpdateUsernameSuccess(IUserServiceTest.java:74)
        //      But found this interaction on mock 'iUserRepository':
        //      -> at com.wuwii.service.impl.UserServiceImpl.findOne(UserServiceImpl.java:21)
        //      ***
    }

    @Test
    public void testUpdateUsernameFailed() {
        long userId = 2L;
        String newUsername = "new kronchan";
        // The updateUser method without mock returns false
        boolean updated = userService.updateUsername(userId, newUsername);
        Assert.assertThat(updated, Matchers.not(true));
        //Verify whether the getOne(2L) method of userRepository has been called (this method has been tested, and this step passes)
        Mockito.verify(userRepository).getOne(2L);
        // Verify whether the updateUser(null) method of userRepository has been called (this method has not been tested, and this step does not pass)
        //Mockito.verify(userRepository).updateUser(null);
        Mockito.verifyNoMoreInteractions(userRepository);
    }
}
analysis
Create objects for MOCK

If I need to measure the userService, I need to simulate the userRepository object.

In the setUp() method, I simulate the object and run it.

There are two ways to simulate objects:

  1. Simulate the object annotated with @ Mock MockitoAnnotations.initMocks(this);
  2. Manually mock a single object: userRepository = Mockito.mock(IUserRepository.class);

In addition to the above methods used in my code, there are many methods for data piling, which can be seen when using. They are mainly divided into the following:

  1. The most basic usage is to call the when and thenReturn methods. Their function is to specify what value is returned when we call a method and parameter of the proxy object.

  2. Provide parameter matcher to flexibly match parameters. any(), any(Class type), anyBoolean(), anyByte(), anyChar(), anyInt(), anyLong(), etc. it supports complex filtering. You can use regular Mockito.matches(".*User $"). At the beginning and end, you can verify endsWith(String suffix), startsWith(String prefix), null verification isNotNull() isNull()
    You can also use argThat(ArgumentMatcher matcher). For example, ArgumentMatcher has only one method, boolean matches(T argument); Pass in the input parameter and return a boolean indicating whether it matches.

    Mockito.argThat(argument -> argument.getUsername.length() > 6;

  3. Mockito also provides two methods to represent behavior: thenAnswer(Answer answer) thenCallRealMethod();, It represents the behavior after the user-defined processing call and calling the real method. These two methods are useful in some test cases.

  4. For the same method, Mockito can be concerned with order and number. In other words, the same method can be implemented. The first call returns a value, the second call returns a value, and even the third call throws an exception. Just call thenXXXX continuously.

  5. If you set the pile data for a method that returns Void. The above methods all represent methods with return values. Since a method has no return value, we cannot call the when method (not allowed by the compiler). Therefore, for methods with no return value, Mockito provides doXXXXX methods for some columns, such as doanswer (answer), doNothing(), doreturn (object to be returned), dothrow (class to be thrown), and doCallRealMethod(). Their usage method is actually the same as thenXXXX above, but the when method passes in Mock's object:

    /*Set simulation for void method*/
    Mockito.doAnswer(invocationOnMock -> {
        System.out.println("Entered Mock");
        return null;
    }).when(fileRecordDao).insert(Mockito.any());
    

    When Mockito monitors a real object, we can also simulate the method of the object to return the expected value we set,

    List spy = spy(new LinkedList());  
    List spy = spy(new LinkedList());  
    // IndexOutOfBoundsException (the list is yet empty)  
    when(spy.get(0)).thenReturn("foo");  
    // You have to use doReturn() for stubbing  
    doReturn("foo").when(spy).get(0);  
    

    In the when method parameter, spy.get(0) calls get(0) of the real list object, which will generate an IndexOutOfBoundsException exception. Therefore, you need to use the doReturn method to set the return value.

Verify the results of the test method

Use assertion statements to check the results.

Call to validate MOCK object

In fact, it is very simple if we only verify the correctness of the method result here. However, in the complex method call stack, the result may be correct but the process may be incorrect. For example, there are two possibilities for the updateUserName method to return false: one is that the user is not found, and the other is that userRepository.updateUser(userPO) returns false. Therefore, if we just use Assert.assertFalse(updated); To verify the results, some errors may be ignored.

  1. Therefore, in the above test, I also need to verify the specified method userRepository).getOne(userId); Whether it has been run, and I also use the parameter catcher to grab the intermediate method parameters for verification.

  2. The verify (t mock, VerificationMode) method is provided. VerificationMode has many functions,

      // Verify that the specified method get(3) is not called  
        verify(mock, never()).get(3);  
    
  3. verifyZeroInteractions and verifyNoMoreInteractions verify that all mock methods have been called.

The main code address of this article

Tags: Java Spring Boot Back-end

Posted on Thu, 18 Nov 2021 11:08:42 -0500 by madmindz