Spring unit testing tutorial (JUnit5+Mockito)

KerryWu
中文

1. Test

If the project team has a test team, the most frequently encountered concepts are: functional testing, regression testing, smoke testing, etc., but these are all initiated by testers.

Developers often write "unit tests", but they can actually be subdivided into unit tests and integration tests.

Take the common Spring IoC as an example of the reasons for the division. Different Spring Beans depend on each other. For example, a certain API business logic will depend on the Service of different modules, and the Service method may depend on different Dao layer methods, and even call external service methods through RPC and HTTP. This makes it difficult for us to write test cases. Originally, we only wanted to test the function of a certain method, but we had to consider a series of dependencies.

1.1. Unit Testing

unit test: refers to the inspection and verification of the smallest testable unit in the software.

Usually any software will be divided into different modules and components. When testing a component individually, we call it unit testing. Unit testing is used to verify whether a related piece of code is working properly. Unit testing is not used to find application-wide bugs or regression testing bugs, but to detect each piece of code separately.

Unit testing does not verify that the application code is working properly with external dependencies. It focuses on a single component and Mock all the dependencies that interact with it. For example, to call the service of sending text messages and interact with the database in the method, we only need to mock the fake execution. After all, the focus of the test is on the current method.

Features of unit testing:

  • Does not rely on any modules.
  • Code-based testing does not need to be run in the ApplicationContext.
  • The method executes quickly, within 500ms (also related to not starting Spring).
  • The same unit test can be executed repeatedly N times, and the result is the same each time.

1.2. Integration Test

integration test: On the basis of unit testing, all modules are assembled into subsystems or systems according to design requirements for integration testing.

The integration test is mainly used to discover the problems caused by the interaction of different modules when the user requests end-to-end. The scope of integration testing can be the entire application or a single module, depending on what is to be tested.

In integration testing, we should focus on the complete request from the controller layer to the persistence layer. The application should run an embedded service (for example: Tomcat) to create the application context and all beans. Some of these beans may be covered by Mock.

Features of integration testing:

  • The purpose of integration testing is to test whether the different modules work together as expected.
  • The application should run in the ApplicationContext. Spring boot provides @SpringBootTest annotation to create a running context.
  • Use @TestConfiguration, etc. to configure the test environment.

2. Test Framework

2.1. spring-boot-starter-test

The testing framework in SpringBoot mainly comes from spring-boot-starter-test. Once spring-boot-starter-test is relied upon, the following libraries will be relied upon together:

  • JUnit : The de facto standard for java testing.
  • Spring Test & Spring Boot Test : Spring test support.
  • AssertJ : Provides a stream-style assertion method.
  • Hamcrest : Provides a rich matcher.
  • Mockito : Mock framework, you can create mock objects by type, you can specify specific responses based on method parameters, and it also supports the assertion of the mock call process.
  • JSONassert : Provides an assertion function for JSON.
  • JsonPath : Provides XPATH function for JSON.
Test Environment Custom Bean
  • @TestComponent : This annotation is another @Component, which is semantically used to specify that a Bean is specifically used for testing. This annotation is suitable when the test code and the formal are mixed together, the Bean described by this annotation is not loaded, and it is not used much.
  • @TestConfiguration : This annotation is another @TestComponent, which is used to supplement additional beans or overwrite existing beans. Make the configuration more flexible without modifying the formal code.

2.2. JUnit

The former said that JUnit is a Java language unit testing framework, but it can also be used for integration testing. The current latest version is JUnit5.

Common differences are:

  • JUnit4 requires JDK5+ version, while JUnit5 requires JDK8+ version, so it supports many Lambda methods.
  • JUnit 4 bundles everything into a single jar file. Junit 5 consists of 3 sub-projects, namely JUnit Platform, JUnit Jupiter and JUnit Vintage. The core is JUnit Jupiter, which has all the new junit annotations and TestEngine implementations to run tests written using these annotations. JUnit Vintage includes compatibility with JUnit3 and JUnit4, so the new version of spring-boot-starter-test pom will automatically exclusion it.
  • SpringBoot 2.2.0 began to introduce JUnit5 as the default library for unit testing. Before SpringBoot 2.2.0, spring-boot-starter-test included the dependency of JUnit4. After SpringBoot 2.2.0, it was replaced with Junit Jupiter.

The differences between JUnit5 and JUnit4 in annotations are:

FunctionJUnit4JUnit5
Declare a test method@Test@Test
Execute before all test methods in the current class@BeforeClass@BeforeAll
Execute after all test methods in the current class@AfterClass@AfterAll
Execute before each test method@Before@BeforeEach
Execute after each test method@After@AfterEach
Disable test method/class@Ignore@Disabled
Test factory for dynamic testingNA@TestFactory
Nested testNA@Nested
Marking and filtering@Category@Tag
Register a custom extensionNA@ExtendWith
RunWith and ExtendWith

In the JUnit4 version, when adding @SpringBootTest annotations to the test class, the same must be added @RunWith(SpringRunner.class) to take effect, namely:

@SpringBootTest
@RunWith(SpringRunner.class)
class HrServiceTest {
...
}

However, in the JUnit5, the official website to inform @RunWith functions are @ExtendWith substituting the original @RunWith(SpringRunner.class) the same function @ExtendWith(SpringExtension.class) instead. But @SpringBootTest annotation in JUnit5 already includes @ExtendWith(SpringExtension.class) by default.

Therefore, you only need to use the @SpringBootTest annotation alone in JUnit5. @ExtendWith other items that need to be customized. Do not use @RunWith anymore.

2.3. Mockito

Test-driven development (TDD) requires us to write unit tests first, and then write implementation code. In the process of writing unit tests, we often encounter many dependencies on the classes to be tested, and these dependent classes/objects/resources have other dependencies, thus forming a large dependency tree. The purpose and function of the Mock technology is to simulate some objects that are not easy to construct or more complex in the application, thereby isolating the test from the objects outside the test boundary.

There are many Mock frameworks, in addition to the traditional EasyMock and Mockito, there are PowerMock, JMock, JMockit, etc. Mockito is chosen here because Mockito is popular in the community and is the framework integrated by SpringBoot by default.

The two core concepts in the Mockito framework are Mock and Stub . The test is not a real operation of external resources, but a simulation operation through custom code. We can simulate any dependency, so that the behavior of the test does not require any preparation work or does not have any side effects.

When we are testing, if we only care about whether a certain operation has been executed, and do not care about the specific behavior of this operation, this technique is called mock. For example, the code we tested will perform the operation of sending emails. We mock this operation; when testing, we only care about whether the sending email operation is called, not whether the email is actually sent.

In another case, when we are concerned about the specific behavior of the operation, or the return result of the operation, we perform a preset operation instead of the target operation, or return the preset result as the return result of the target operation. This kind of simulated behavior of operation is called stub (stubbing). For example, when we test whether the code's exception handling mechanism is normal, we can stub the code somewhere and let it throw an exception. For another example, the code we tested needs to insert a piece of data into the database. We can stub the code that inserts the data so that it always returns 1, indicating that the data is inserted successfully.

Recommend a Mockito Chinese document .

The difference between mock and spy

Both mock method and spy method can mock objects. But the former takes over all the methods of the object, while the latter just mocks the calls with stubbing, and the other methods are still actual calls.

In the following example, because only the List.size() method is mocked. If the mockList is generated by mock, the add, get and other methods of List will all fail, and the returned data will be null. But if it is generated by spy, the verification is normal.

In the usual development process, we usually only need certain methods of the mock class, using spy.

@Test
    void mockAndSpy() {
        List<String> mockList = Mockito.mock(List.class);
        // List<String> mockList = Mockito.spy(new ArrayList<>());
        Mockito.when(mockList.size())
                .thenReturn(100);

        mockList.add("A");
        mockList.add("B");
        Assertions.assertEquals("A", mockList.get(0));
        Assertions.assertEquals(100, mockList.size());
    }

3. Examples

3.1. Unit test example

Because JUnit5 and Mockito are all dependencies of spring-boot-starter-test by default, there is no need to introduce other special dependencies in pom. First write a simple Service layer method to query data through two tables.
HrService.java

@AllArgsConstructor
@Service
public class HrService {
    private final OrmDepartmentDao ormDepartmentDao;
    private final OrmUserDao ormUserDao;

    List<OrmUserPO> findUserByDeptName(String deptName) {
        return ormDepartmentDao.findOneByDepartmentName(deptName)
                .map(OrmDepartmentPO::getId)
                .map(ormUserDao::findByDepartmentId)
                .orElse(Collections.emptyList());
    }
}
IDEA Create test class

Next, create a test class for the Service class. The development tool we use is IDEA. into the current class, >Go To->Test->Create New Test, select Junit5 in the Testing library, and then generate test classes and methods in the corresponding directory.

HrServiceTest.java

@ExtendWith(MockitoExtension.class)
class HrServiceTest {
    @Mock
    private OrmDepartmentDao ormDepartmentDao;
    @Mock
    private OrmUserDao ormUserDao;
    @InjectMocks
    private HrService hrService;

    @DisplayName("根据部门名称,查询用户")
    @Test
    void findUserByDeptName() {
        Long deptId = 100L;
        String deptName = "行政部";
        OrmDepartmentPO ormDepartmentPO = new OrmDepartmentPO();
        ormDepartmentPO.setId(deptId);
        ormDepartmentPO.setDepartmentName(deptName);
        OrmUserPO user1 = new OrmUserPO();
        user1.setId(1L);
        user1.setUsername("001");
        user1.setDepartmentId(deptId);
        OrmUserPO user2 = new OrmUserPO();
        user2.setId(2L);
        user2.setUsername("002");
        user2.setDepartmentId(deptId);
        List<OrmUserPO> userList = new ArrayList<>();
        userList.add(user1);
        userList.add(user2);

        Mockito.when(ormDepartmentDao.findOneByDepartmentName(deptName))
                .thenReturn(
                        Optional.ofNullable(ormDepartmentPO)
                                .filter(dept -> deptName.equals(dept.getDepartmentName()))
                );
        Mockito.doReturn(
                userList.stream()
                        .filter(user -> deptId.equals(user.getDepartmentId()))
                        .collect(Collectors.toList())
        ).when(ormUserDao).findByDepartmentId(deptId);

        List<OrmUserPO> result1 = hrService.findUserByDeptName(deptName);
        List<OrmUserPO> result2 = hrService.findUserByDeptName(deptName + "error");

        Assertions.assertEquals(userList, result1);
        Assertions.assertEquals(Collections.emptyList(), result2);
    }

Because the unit test does not need to start the Spring container, there is no need to add @SpringBootTest, because to use Mockito, you only need to customize the extension MockitoExtension.class, which is simple to rely on and runs faster.

It can be clearly seen how the code written by the unit test is several times the length of the code under test? In fact, the code length of the unit test is relatively fixed, it is data creation and piling, but if you write unit tests for the more complex logic code, the more cost-effective it is.

3.2. Integration test example

It is the same method. If you use the Spring context, the actual calling method depends on the following methods:

@SpringBootTest
class HrServiceTest {
    @Autowired
    private HrService hrService;

    @DisplayName("根据部门名称,查询用户")
    @Test
    void findUserByDeptName() {
        List<OrmUserPO> userList = hrService.findUserByDeptName("行政部");
        Assertions.assertTrue(userList.size() > 0);
    }  
}

You can also use @MockBean , @SpyBean replace the corresponding Bean in the Spring context:

@SpringBootTest
class HrServiceTest {
    @Autowired
    private HrService hrService;
    @SpyBean
    private OrmDepartmentDao ormDepartmentDao;

    @DisplayName("根据部门名称,查询用户")
    @Test
    void findUserByDeptName() {
        String deptName="行政部";
        OrmDepartmentPO ormDepartmentPO = new OrmDepartmentPO();
        ormDepartmentPO.setDepartmentName(deptName);
        Mockito.when(ormDepartmentDao.findOneByDepartmentName(ArgumentMatchers.anyString()))
                .thenReturn(Optional.of(ormDepartmentPO));
        List<OrmUserPO> userList = hrService.findUserByDeptName(deptName);
        Assertions.assertTrue(userList.size() > 0);
    }
}
Tips: @SpyBean and spring boot data problems

When using @SpyBean to add to the dao layer of spring data jpa (inheriting the interface of JpaRepository), the container cannot be started, and error org.springframework.beans.factory.BeanCreationException: Error creating bean with name . Spring data including mongo will have this problem. It is not officially supported by spring boot. You can check Issues-7033 , which has been fixed 2.5.3

阅读 566

保持饥饿

336 声望
109 粉丝
0 条评论
你知道吗?

保持饥饿

336 声望
109 粉丝
文章目录
宣传栏