Spring Boot 中使用Junit 5 和 Mockito 编写单元测试-Spring专区论坛-技术-SpringForAll社区

Spring Boot 中使用Junit 5 和 Mockito 编写单元测试

Spring Boot 提供了不同的测试方法,例如单元测试、集成测试和端到端测试。本文我们将学习如何使用 Junit 5 和 Mockito 为 Spring Boot 应用程序编写单元测试。

什么是单元测试

单元测试是一种软件测试技术,其中对软件应用程序的各个单元或组件进行单独测试,以验证它们是否按预期运行。

Spring Boot Starter 测试模块

Spring Boot Starter Test 依赖项提供了用于测试 Spring Boot 应用程序的基本库和实用程序,包括 JUnit、Hamcrest、Mockito 和 AssertJ。它简化了 Spring Boot 应用程序的编写和执行测试的过程。

什么是 JUnit 和 AssertJ

JUnit 是一个流行的 Java 开源测试框架。它提供注释来定义测试方法、测试类和测试套件,以及用于验证预期结果的断言。 JUnit使得编写和执行单元测试变得容易,保证了Java代码的正确性。

AssertJ 是一个流行的 Java 库,它提供流畅、富有表现力的断言,用于编写更清晰、更具可读性的单元测试。它还提供了有用的错误消息。

动手试试

使用 spring 初始化工具创建一个项目:

d2b5ca33bd20240814142610

 

项目结构:

d2b5ca33bd20240814142540

 

application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/unit_test_db?useSSL=false
spring.datasource.username=root
spring.datasource.password=Online12@
spring.jpa.hibernate.ddl-auto = update

Employee(员工)实体:

import jakarta.persistence.*;
import lombok.*;

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    @Builder
    @Entity
    @Table(name = "employees")
    public class Employee {

        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private long id;

        @Column(nullable = false)
        private String firstName;

        @Column(nullable = false)
        private String lastName;

        @Column(nullable = false)
        private String email;

}

Employee Repository

import org.springframework.data.jpa.repository.JpaRepository;
import test.example.springboot.test.demo.Model.Employee;
import java.util.Optional;

public interface EmployeeRepository extends JpaRepository<Employee, Long> {
    Optional<Employee> findByEmail(String email);

}

Employee Controller

@RestController
@RequestMapping("/api/employees")
public class EmployeeController {

    
    private EmployeeService employeeService;

    public EmployeeController(EmployeeService employeeService) {
        this.employeeService = employeeService;
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Employee createEmployee(@RequestBody Employee employee){
        return employeeService.saveEmployee(employee);
    }

    @GetMapping
    public List<Employee> getAllEmployees(){
        return employeeService.getAllEmployees();
    }

    @GetMapping("{id}")
    public ResponseEntity<Optional<Employee>> getEmployeeById(@PathVariable("id") long id){
        return new ResponseEntity<Optional<Employee>>(employeeService.getEmployeeById(id),HttpStatus.OK);
    }

    @PutMapping("{id}")
    public ResponseEntity<Employee> updateEmployee(@PathVariable("id") long id,
                                                   @RequestBody Employee employee){
        return new ResponseEntity<Employee>(employeeService.updateEmployee(employee,id),HttpStatus.OK);
    }

    @DeleteMapping("{id}")
    public ResponseEntity<String> deleteEmployee(@PathVariable("id") long id){
        employeeService.deleteEmployee(id);
        return new ResponseEntity<String>("Employee deleted successfully!.", HttpStatus.OK);

    }
}

Employee Service 接口和实现

public interface EmployeeService {

    Employee saveEmployee(Employee employee);
    List<Employee> getAllEmployees();
    Optional<Employee> getEmployeeById(long id);
    Employee updateEmployee(Employee employee,long id);
    void deleteEmployee(long id);

}



@Service
public class EmployeeServiceImpl implements EmployeeService {

    
    private EmployeeRepository employeeRepository;

    public EmployeeServiceImpl(EmployeeRepository employeeRepository) {
        this.employeeRepository = employeeRepository;
    }

    @Override
    public Employee saveEmployee(Employee employee) {

        Optional<Employee> savedEmployee = employeeRepository.findByEmail(employee.getEmail());
        if(savedEmployee.isPresent()){
            throw new RuntimeException("Employee already exist with given email:" + employee.getEmail());
        }
        return employeeRepository.save(employee);
    }

    @Override
    public List<Employee> getAllEmployees() {
        return employeeRepository.findAll();
    }

    @Override
    public Optional<Employee> getEmployeeById(long id) {
        return employeeRepository.findById(id);
    }

    @Override
    public Employee updateEmployee(Employee employee,long id) {
        Employee existingEmployee = employeeRepository.findById(id)
                .orElseThrow(()->new RuntimeException());

        existingEmployee.setFirstName(employee.getFirstName());
        existingEmployee.setLastName(employee.getLastName());
        existingEmployee.setEmail(employee.getEmail());

        employeeRepository.save(existingEmployee);
        return existingEmployee;
    }

    @Override
    public void deleteEmployee(long id) {
        employeeRepository.deleteById(id);
    }
}

用 JUnit 编写存储库层的单元测试

@DataJpaTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class EmployeeRepositoryUnitTests {

    @Autowired
    private EmployeeRepository employeeRepository;

    @Test
    @DisplayName("Test 1:Save Employee Test")
    @Order(1)
    @Rollback(value = false)
    public void saveEmployeeTest(){

        //Action
        Employee employee = Employee.builder()
                .firstName("Sam")
                .lastName("Curran")
                .email("sam@gmail.com")
                .build();

        employeeRepository.save(employee);

        //Verify
        System.out.println(employee);
        Assertions.assertThat(employee.getId()).isGreaterThan(0);
    }

    @Test
    @Order(2)
    public void getEmployeeTest(){

        //Action
        Employee employee = employeeRepository.findById(1L).get();
        //Verify
        System.out.println(employee);
        Assertions.assertThat(employee.getId()).isEqualTo(1L);
    }

    @Test
    @Order(3)
    public void getListOfEmployeesTest(){
        //Action
        List<Employee> employees = employeeRepository.findAll();
        //Verify
        System.out.println(employees);
        Assertions.assertThat(employees.size()).isGreaterThan(0);

    }

    @Test
    @Order(4)
    @Rollback(value = false)
    public void updateEmployeeTest(){

        //Action
        Employee employee = employeeRepository.findById(1L).get();
        employee.setEmail("samcurran@gmail.com");
        Employee employeeUpdated =  employeeRepository.save(employee);

        //Verify
        System.out.println(employeeUpdated);
        Assertions.assertThat(employeeUpdated.getEmail()).isEqualTo("samcurran@gmail.com");

    }

    @Test
    @Order(5)
    @Rollback(value = false)
    public void deleteEmployeeTest(){
        //Action
        employeeRepository.deleteById(1L);
        Optional<Employee> employeeOptional = employeeRepository.findById(1L);

        //Verify
        Assertions.assertThat(employeeOptional).isEmpty();
    }

}

@TestMethodOrder(MethodOrderer.OrderAnnotation.class) 根据@Order 注解指定的顺序指定测试执行的顺序。

您可以为您的项目使用 MySQL 或任何其他数据库来保存实际数据。如果你使用同一个数据库进行测试,将会影响你的实际数据。因此,您可以使用内存H2数据库进行测试。这是常见的方式。

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

更改 application.properties 文件:

#MysQL Database Configuration for Project
spring.datasource.url=jdbc:mysql://localhost:3306/unit_test_db?useSSL=false
spring.datasource.username=root
spring.datasource.password=Online12@
spring.jpa.hibernate.ddl-auto = update

#H2 Database Configuration for testing
spring.datasource.test.url=jdbc:h2:mem/unit_test_db
spring.datasource.test.diver-class-name=org.h2.Driver
spring.datasource.test.username=sa
spring.datasource.test.password=password
spring.jpa.test.hibernate.ddl-auto = create-drop

运行EmployeeRepositoryUnitTest.java类:

d2b5ca33bd20240814143119

使用Mockito

Mockito 是一个流行的 Java 框架,用于在单元测试中模拟对象。 Mockito 可以在单元测试中使用来模拟依赖关系并隔离正在测试的代码。Controller通常依赖Service层组件来执行业务逻辑或与应用程序的数据层(例如存储库)交互。使用 Mockito 模拟服务层组件,将正在测试的控制器与实际服务层实现隔离。

MockMvc 不是 Mockito 的一部分。 MockMvc是Spring Test框架提供的专门用于测试Spring MVC控制器的类。

EmployeeControllerUnitTests类:

@WebMvcTest(EmployeeController.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class EmployeeControllerUnitTests {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private EmployeeService employeeService;

    @Autowired
    private ObjectMapper objectMapper;

    Employee employee;

    @BeforeEach
    public void setup(){

         employee = Employee.builder()
                .id(1L)
                .firstName("John")
                .lastName("Cena")
                .email("john@gmail.com")
                .build();

    }

    //Post Controller
    @Test
    @Order(1)
    public void saveEmployeeTest() throws Exception{
        // precondition
        given(employeeService.saveEmployee(any(Employee.class))).willReturn(employee);

        // action
        ResultActions response = mockMvc.perform(post("/api/employees")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(employee)));

        // verify
        response.andDo(print()).
                andExpect(status().isCreated())
                .andExpect(jsonPath("$.firstName",
                        is(employee.getFirstName())))
                .andExpect(jsonPath("$.lastName",
                        is(employee.getLastName())))
                .andExpect(jsonPath("$.email",
                        is(employee.getEmail())));
    }

    //Get Controller
    @Test
    @Order(2)
    public void getEmployeeTest() throws Exception{
        // precondition
        List<Employee> employeesList = new ArrayList<>();
        employeesList.add(employee);
        employeesList.add(Employee.builder().id(2L).firstName("Sam").lastName("Curran").email("sam@gmail.com").build());
        given(employeeService.getAllEmployees()).willReturn(employeesList);

        // action
        ResultActions response = mockMvc.perform(get("/api/employees"));

        // verify the output
        response.andExpect(status().isOk())
                .andDo(print())
                .andExpect(jsonPath("$.size()",
                        is(employeesList.size())));

    }

    //get by Id controller
    @Test
    @Order(3)
    public void getByIdEmployeeTest() throws Exception{
        // precondition
        given(employeeService.getEmployeeById(employee.getId())).willReturn(Optional.of(employee));

        // action
        ResultActions response = mockMvc.perform(get("/api/employees/{id}", employee.getId()));

        // verify
        response.andExpect(status().isOk())
                .andDo(print())
                .andExpect(jsonPath("$.firstName", is(employee.getFirstName())))
                .andExpect(jsonPath("$.lastName", is(employee.getLastName())))
                .andExpect(jsonPath("$.email", is(employee.getEmail())));

    }


    //Update employee
    @Test
    @Order(4)
    public void updateEmployeeTest() throws Exception{
        // precondition
        given(employeeService.getEmployeeById(employee.getId())).willReturn(Optional.of(employee));
        employee.setFirstName("Max");
        employee.setEmail("max@gmail.com");
        given(employeeService.updateEmployee(employee,employee.getId())).willReturn(employee);

        // action
        ResultActions response = mockMvc.perform(put("/api/employees/{id}", employee.getId())
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(employee)));

        // verify
        response.andExpect(status().isOk())
                .andDo(print())
                .andExpect(jsonPath("$.firstName", is(employee.getFirstName())))
                .andExpect(jsonPath("$.email", is(employee.getEmail())));
    }


    // delete employee
    @Test
    public void deleteEmployeeTest() throws Exception{
        // precondition
        willDoNothing().given(employeeService).deleteEmployee(employee.getId());

        // action
        ResultActions response = mockMvc.perform(delete("/api/employees/{id}", employee.getId()));

        // then - verify the output
        response.andExpect(status().isOk())
                .andDo(print());
    }
}

运行测试类:

d2b5ca33bd20240814143242

 

使用 JUnit + Mockito 编写服务层单元测试

EmployeeServiceUnitTests类:

@ExtendWith(MockitoExtension.class)
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class EmployeeServiceUnitTests {

    @Mock
    private EmployeeRepository employeeRepository;

    @InjectMocks
    private EmployeeServiceImpl employeeService;

    private Employee employee;


    @BeforeEach
    public void setup(){

        employee = Employee.builder()
                .id(1L)
                .firstName("John")
                .lastName("Cena")
                .email("john@gmail.com")
                .build();

    }

    @Test
    @Order(1)
    public void saveEmployeeTest(){
        // precondition
        given(employeeRepository.save(employee)).willReturn(employee);

        //action
        Employee savedEmployee = employeeService.saveEmployee(employee);

        // verify the output
        System.out.println(savedEmployee);
        assertThat(savedEmployee).isNotNull();
    }

    @Test
    @Order(2)
    public void getEmployeeById(){
        // precondition
        given(employeeRepository.findById(1L)).willReturn(Optional.of(employee));

        // action
        Employee existingEmployee = employeeService.getEmployeeById(employee.getId()).get();

        // verify
        System.out.println(existingEmployee);
        assertThat(existingEmployee).isNotNull();

    }


    @Test
    @Order(3)
    public void getAllEmployee(){
        Employee employee1 = Employee.builder()
                .id(2L)
                .firstName("Sam")
                .lastName("Curran")
                .email("sam@gmail.com")
                .build();

        // precondition
        given(employeeRepository.findAll()).willReturn(List.of(employee,employee1));

        // action
        List<Employee> employeeList = employeeService.getAllEmployees();

        // verify
        System.out.println(employeeList);
        assertThat(employeeList).isNotNull();
        assertThat(employeeList.size()).isGreaterThan(1);
    }

    @Test
    @Order(4)
    public void updateEmployee(){

        // precondition
        given(employeeRepository.findById(employee.getId())).willReturn(Optional.of(employee));
        employee.setEmail("max@gmail.com");
        employee.setFirstName("Max");
        given(employeeRepository.save(employee)).willReturn(employee);

        // action
        Employee updatedEmployee = employeeService.updateEmployee(employee,employee.getId());

        // verify
        System.out.println(updatedEmployee);
        assertThat(updatedEmployee.getEmail()).isEqualTo("max@gmail.com");
        assertThat(updatedEmployee.getFirstName()).isEqualTo("Max");
    }

    @Test
    @Order(5)
    public void deleteEmployee(){

        // precondition
        willDoNothing().given(employeeRepository).deleteById(employee.getId());

        // action
        employeeService.deleteEmployee(employee.getId());

        // verify
        verify(employeeRepository, times(1)).deleteById(employee.getId());
    }


}

运行测试类:

d2b5ca33bd20240814143502

小结

在本教程中,我们使用 JUnit 5 和 Mockito 为存储库、控制器和服务编写了 Spring Boot 单元测试。许多经验丰富的程序员和团队都会优先考虑单元测试和集成测试,以确保应用程序健壮、可靠和可维护。

请登录后发表评论

    没有回复内容