1. Test
If the project team has a testing team, the most common 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.
The reason for the division is to take the common Spring IoC as an example. Different Spring beans depend on each other. For example, the business logic of an API depends on the Service of different modules, and the Service method may depend on different Dao layer methods, and even calls 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 testing: refers to checking and verifying the smallest testable unit in software.
Usually any software is divided into different modules and components. When testing a component in isolation, we call it unit testing. Unit tests are used to verify that a related small piece of code works correctly. Unit testing is not used to find application-wide bugs, or regression testing bugs, but to detect each piece of code individually.
Unit tests do not verify that application code works correctly with external dependencies. It focuses on a single component and mocks all dependencies that interact with it. For example, to call the texting service in the method, and to interact with the database, we only need mock execution, after all, the focus of the test is on the current method.
Features of unit testing:
- Does not depend on any modules.
- Code-based tests do not need to run in an ApplicationContext.
- The method executes quickly, within 500ms (also related to not starting Spring).
- The same unit test can be repeated N times with the same result each time.
1.2. Integration tests
Integration testing: On the basis of unit testing, all modules are assembled into subsystems or systems according to design requirements for integration testing.
Integration testing is mainly used to find problems caused by the interaction of different modules when users request end-to-end. The integration test scope can be the entire application or a single module, depending on what is being tested.
In integration tests, we should focus on the complete request from the controller layer to the persistence layer. The application should run an embedded service (eg: Tomcat) to create the application context and all beans. Some of these beans may be overridden by Mock.
Features of integration tests:
- The purpose of integration testing is to test whether different modules work together as expected.
- Applications should run in ApplicationContext. Spring boot provides @SpringBootTest annotation to create running context.
- Configure the test environment using @TestConfiguration etc.
2. Test framework
2.1. spring-boot-starter-test
The framework for testing in SpringBoot mainly comes from spring-boot-starter-test. Once you depend on spring-boot-starter-test, the following libraries will be depended on together:
- JUnit : The de facto standard for java testing.
- Spring Test & Spring Boot Test : Spring's testing support.
- AssertJ : Provides a streaming assertion method.
- Hamcrest : Provides a rich matcher.
- Mockito : Mock framework, which can create mock objects by type, specify specific responses based on method parameters, and also support assertions for the mock calling process.
- JSONassert : Provides assertion capabilities for JSON.
- JsonPath : Provides XPATH functionality for JSON.
Test Environment Custom Bean
@TestComponent
: This annotation is another type of @Component that is used semantically to specify that a bean is dedicated to testing. This annotation is suitable for testing code and formal mixing together, do not load the Bean described by this annotation, not much used.@TestConfiguration
: This annotation is another type of @TestComponent, which is used to supplement additional beans or override existing ones. Make the configuration more flexible without modifying the official code.
2.2. JUnit
The former said that JUnit is a unit testing framework for the Java language, but it can also be used for integration testing. The current latest version is JUnit5.
Common differences are:
- JUnit4 requires JDK5+ version, and 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 a TestEngine implementation to run tests written with these annotations. JUnit Vintage includes compatibility with JUnit3 and JUnit4, so it is often automatically excluded in the new version of spring-boot-starter-test pom.
- 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 dependencies of JUnit4. After SpringBoot 2.2.0, it was replaced by Junit Jupiter.
The difference between JUnit5 and JUnit4 in annotations is:
Function | JUnit4 | JUnit5 |
---|---|---|
declare a test method | @Test | @Test |
Executed before all test methods in the current class | @BeforeClass | @BeforeAll |
Executed 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 methods/classes | @Ignore | @Disabled |
Test factory for dynamic testing | NA | @TestFactory |
Nested tests | NA | @Nested |
Tag and filter | @Category | @Tag |
Register a custom extension | NA | @ExtendWith |
RunWith and ExtendWith
In the JUnit4 version, when adding the @SpringBootTest
annotation to the test class, it also takes effect by adding @RunWith(SpringRunner.class)
, namely:
@SpringBootTest
@RunWith(SpringRunner.class)
class HrServiceTest {
...
}
But in JUnit5, the official website informs that the functions of @RunWith
are replaced by @ExtendWith
, that is, the original @RunWith(SpringRunner.class)
is replaced by @ExtendWith(SpringExtension.class)
with the same function. But @ExtendWith(SpringExtension.class) is included by default in the @SpringBootTest annotation in JUnit5.
Therefore, in JUnit5, you only need to use the @SpringBootTest
annotation alone. Others that need custom extension use @ExtendWith
instead of @RunWith
.
2.3. Mockito
Test-driven development (TDD) requires us to write unit tests first, and then write the implementation code. In the process of writing unit tests, we often encounter that the class to be tested has many dependencies, and these dependent classes/objects/resources have other dependencies, thus forming a large dependency tree. The purpose and function of Mock technology is to simulate some objects that are not easy to construct or more complex in the application, so as to isolate the test from the objects outside the test boundary.
There are many Mock frameworks, in addition to the traditional EasyMock, Mockito, there are PowerMock, JMock, JMockit and so on. Mockito is chosen here because Mockito is more popular in the community and is the default integration framework of SpringBoot.
The two core concepts in the Mockito framework are Mock and Stub . When testing, it does not actually operate external resources, but simulates operations through custom code. We can mock any dependency so that the behavior of the test requires no preparation or side effects.
When we are testing, if we only care about whether an operation has been performed, but not the specific behavior of this operation, this technique is called mocking. For example, the code we test will perform the operation of sending an email, and we will mock this operation; when testing, we only care whether the operation of sending an email is called, but not whether the email is actually sent.
In another case, when we care about the specific behavior of the operation, or the return result of the operation, we replace the target operation by executing the preset operation, or return the preset result as the return result of the target operation. This simulated behavior of operations is called stubbing. For example, if we test whether the exception handling mechanism of the code is normal, we can stub the code somewhere and let it throw an exception. For another example, the code we test needs to insert a piece of data into the database. We can stub the code that inserts data so that it always returns 1, indicating that the data is inserted successfully.
Recommend a Mockito Chinese document .
The difference between mock and
Both the mock method and the spy method can mock objects. But the former takes over all the methods of the object, while the latter just mocks the stubbing calls, and the rest of the methods are still actual calls.
The following example, because only the List.size() method is mocked. If the mockList is generated by mocking, other methods such as add and get of List will be invalid, and the returned data will be null. But if it's generated via spy, the validation works fine.
In the usual development process, we usually only need some methods of the mock class, just use 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());
}
2.4. Plugin Squaretest
This plug-in that automatically generates unit test code is strongly recommended. Like the framework mentioned in this article, the default template is JUnit5Mockito.java.ft
.
After right-clicking on the class Generate -> Generate Test
, not only can test classes and methods be generated, but even Mockito data, methods and Assertions are written, just need to change it by yourself.
3. Example
3.1. Unit Test Example
Because JUnit5 and Mockito are all the default dependencies of spring-boot-starter-test, there is no need to introduce other special dependencies into the 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 creates test class
Next, create a test class for the Service class. The development tool we use is IDEA. Click into the current class, Right-click->Go To->Test->Create New Test, select Junit5 in the Testing library, and 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, the dependency is simple, and the running speed is faster.
It can be clearly seen that the code written by the unit test is several times the length of the tested code? In fact, the length of the unit test code is relatively fixed, all of which are data creation and piling, but if you write unit tests for the code with more complex logic, it is still more cost-effective.
3.2. Integration Test Example
Still that method, if you use the Spring context, the real call method depends, you can directly use 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 replace the corresponding bean in the Spring context with @MockBean
, @SpyBean
:
@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 issues
When adding @SpyBean to the dao layer of spring data jpa (inheriting the interface of JpaRepository), the container cannot be started, and an error org.springframework.beans.factory.BeanCreationException: Error creating bean with name
. Spring data, including mongo, will have this problem, which is not officially supported by spring boot. You can check Issues-7033 , which has been fixed in spring boot version 2.5.3
.
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。