In the team I belong to, if there is a problem, I write a test code, reproduce it (check the red bar), and then fix it. The other day, there was a problem that transaction control was missing, so I tried to write a test code, but since I was confused, I will organize it and post it. The test code is listed on Github (https://github.com/shimi58/transactiontest), so please refer to it.
The environment built on Eclipse is as follows. I have created a Gradle project and passed it through the path.
environment | version |
---|---|
Java | 1.8 |
SpringBoot | 2.2.6 |
MyBatis | 2.1.0 |
H2 | PostgresqlMode |
Junit | 5(Jupiter) |
Register employee information (name, phone number, email address) and update the number in the employee table to the status table. It is a sample source such as.
/**
* Employee service
*/
@Service
public class EmployeeService {
@Autowired
EmployeeRepository employeeRepository;
/**
* Employee registration
*/
@Transactional // ← This was leaking
public EmployeeNumber register(Employee employee) {
employeeRepository.registerEmployee(employee);
Employees employees = employeeRepository.findEmployees();
EmployeeNumber employeeNumber = employees.number();
employeeRepository.registerNumber(employeeNumber);
return employeeNumber;
}
}
In this sample source, it is updated as employee table → status table, but if the transaction is not effective and the update of the status table fails, it is retained in the number and status table held in the employee table. The number of cases is inconsistent.
By the way, the Controller class will be mentioned later when explaining the test code.
/**
* Employee Controller
*/
@RestController
@RequestMapping("/employees")
public class EmployeesController {
@Autowired
EmployeeService employeeService;
/**
* Employee registration
*/
@RequestMapping(value = "/register", method = {RequestMethod.POST})
public String regist(@RequestBody Employee employee) {
EmployeeNumber employeeNumber = new EmployeeNumber(0);
try {
employeeNumber = employeeService.register(employee);
} catch (Exception e) {
System.out.println ("processing error");
}
return employeeNumber.toString();
}
}
Use ** @SpyBean **.
SpyBean is a Spring Boot function, and it is a mock object that only partially processes the class you want to test. In the case of this sample source, Mock the EmployeeRepository with SpyBean and
--registerEmployee: Employee registration process → Normal implementation --findEmployees: Employee table count processing → Normal execution --registerNumber: Status table update process → Error occurred
I want to create a situation like that.
The test is executed by calling the Controller class as SpringBootTest. The Controller class is calling the above service class.
@SpringBootTest
public class EmployeesControllerTransactionTest {
MockMvc mockMvc;
@Autowired
private ObjectMapper mapper;
@Autowired
private EmployeesController employeesController;
// Mock target
@SpyBean
private EmployeeRepository employeeRepository;
/**
* Transaction test
*
* When an error occurs when updating the number of employees after registering employee information in the Emploee table, <br>
* Make sure you are rolling back employee information registration
*
* @throws Exception
*/
@ParameterizedTest
@CsvSource ({"Takao Hibi, 123-4345-2352, [email protected], 3"})
public void testTransaction(String name, String phone, String mail, int expected)
throws Exception {
Employee employee = new Employee(name, phone, mail);
// Convert request to Json format
String json = mapper.writeValueAsString(employee);
// Error occurrence setting
doThrow(new RuntimeException()).when(employeeRepository)
.registerNumber(Mockito.any(EmployeeNumber.class));
this.mockMvc = MockMvcBuilders.standaloneSetup(employeesController).build();
// Issue request
MvcResult result = mockMvc.perform(
post("/employees/register").contentType(MediaType.APPLICATION_JSON).content(json))
.andExpect(status().isOk()).andReturn();
String response = result.getResponse().getContentAsString();
Employees employees = employeeRepository.findEmployees();
System.out.println(response);
System.out.println(employees.toString());
EmployeeNumber actual = employees.number();
// Because it will be rolled back, check that it is in the state of 3 before registration
assertEquals(expected, actual.getValue().intValue());
}
@SpringBootTest
public class EmployeesControllerTransactionTest {
It is indispensable because it will not DI unless SpringBootTest is attached.
@SpyBean
private EmployeeRepository employeeRepository;
Declare the repository to be Mocked.
@ParameterizedTest
@CsvSource ({"Takao Hibi, 123-4345-2352, [email protected], 3"})
Although it is not related to this article, by using CsvSource, you can perform variation test with one method. I had a long history of Junit4, so I was impressed when I realized that I could do this. By the way, the name was borrowed from Amazing Name Generator. It has become a convenient world. (More irrelevant.)
// Error occurrence setting
doThrow(new RuntimeException()).when(employeeRepository)
.registerNumber(Mockito.any(EmployeeNumber.class));
Here, it is declared that a RuntimeException will be generated when processing registerNumber.
this.mockMvc = MockMvcBuilders.standaloneSetup(employeesController).build();
Dohamari point ①. If you don't set it up standalone, mockMvc will drop with a nullpo.
MvcResult result = mockMvc.perform(
post("/employees/register").contentType(MediaType.APPLICATION_JSON).content(json))
.andExpect(status().isOk()).andReturn();
Dohamari point ②. If it is not status (). isOk (), it will fly away (debug will not be returned). I was worried for an hour when the URL of the post was wrong and 404 was returned, and why the debug response did not occur.
When I ran the test code, it turned out to be a nice green bar!
By the way, if you remove @Transactional in the service class,
Yeah, it's a red bar. If the number of items in the employee table is different by one, an assertion error has occurred and the transaction can be confirmed.
I will look back alone.
--H2's in-memory DB is very convenient This is the first time I've touched it, and it's perfect for checking the operation of these sample codes. It would be even more perfect if you could connect with A5SQL. --Gradle is very convenient Just write the definition and it will pass through the path, so it will make a lot of progress. As an aside, I'm going to drop a good number of libraries by tracing the dependency, so checking the license makes me cry (laugh) --SpyBean is very convenient I think there are some things that can't be tested with Mock. ――It is difficult to write the process from 1 I should have written it in business such as how to receive Gradle and post, test code, but it took a long time to write from scratch. .. (But implementation is fun) --H2 Postgres Mode cannot Upsert It looks like H2 Database Postgres Mode Upsert. Therefore, I made sure to register the record in the status table with the initial build data so that only update is required. (Not cool ...)
--Read and organize test-driven design to master the test path --Make various variations of test code work as a sample source so that other Mock processing works.
Recommended Posts