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
- The naming rules of test classes are generally xxtest.java;
- 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;
- 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.
-
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()); } }
-
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.
-
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:
- Using the memory database h2database, Spring Data Jpa test adopts this method by default;
- Use a real-world database.
Test with in memory database
-
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')
-
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
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:
- Simulate the object annotated with @ Mock MockitoAnnotations.initMocks(this);
- 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:
-
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.
-
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;
-
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.
-
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.
-
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.
-
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.
-
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);
-
verifyZeroInteractions and verifyNoMoreInteractions verify that all mock methods have been called.