Mockito 避坑指南 常见错误的预防与处理

2023年 11月 1日 44.7k 0

介绍

Mockito 是一个流行的用于测试 Java 应用程序的框架。它提供了一种强大且易于使用的方式来模拟依赖关系和编写单元测试。然而,刚接触 Mockito 的开发人员可能会犯一些错误,从而导致测试不可靠,甚至导致应用程序出现意外行为。在本文中,我们将讨论开发人员在 Spring Boot 应用程序中使用 Mockito 框架时犯的常见错误,以及代码示例和解释。

1.滥用@Mock和@InjectMocks注释

开发人员在使用 Mockito 时最常见的错误之一是滥用@Mock和@InjectMocks注释。@Mock注解用于为特定类创建模拟对象,而@InjectMocks注解用于将模拟对象注入到被测试的类中。需要注意的是,@InjectMocks 只能与类一起使用,不能与接口一起使用。

例子:

@RunWith(MockitoJUnitRunner.class)
public class MyServiceTest {
    
    @Mock
    private MyRepository myRepository;
    
    @InjectMocks
    private MyService myService;
    
    // test methods
    
}

2.不重置Mock对象

Mockito 可创建在多个测试中重用的Mock对象。如果在测试之间未重置Mock对象,则可能会导致意外行为和不可靠的测试。Mockito 提供了一个名为Mockito.reset()的方法,可用于重置所有Mock对象。

例子:

@Before
public void setUp() {
    MockitoAnnotations.initMocks(this);
}

@Test
public void test1() {
    Mockito.when(myRepository.findById(1)).thenReturn(Optional.of(new MyObject()));
    // test code
}

@Test
public void test2() {
    Mockito.when(myRepository.findById(2)).thenReturn(Optional.of(new MyObject()));
    // test code
}

@After
public void tearDown() {
    Mockito.reset(myRepository);
}

3.对Mock对象使用错误的范围

Mockito 默认创建范围为类级别。这意味着同一个Mock对象将用于类中的所有测试方法。但是,如果模拟对象需要为每个测试方法具有不同的状态或行为,则应使用方法级别的范围来创建。要创建具有正确范围的Mock对象,我们可以使用Spring Boot 提供的@MockBean注解。

@MockBean使用示例:

@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @MockBean
    private UserRepository userRepository;

    @Test
    public void testGetUserById() throws Exception {
        // arrange
        Long userId = 1L;
        User user = new User();
        user.setId(userId);
        user.setName("John Doe");
        Mockito.when(userService.getUserById(userId)).thenReturn(user);

        // act
        MvcResult result = mockMvc.perform(get("/users/{id}", userId))
                .andExpect(status().isOk())
                .andReturn();

        // assert
        String response = result.getResponse().getContentAsString();
        assertThat(response).isEqualTo("{"id":1,"name":"John Doe"}");
        Mockito.verify(userService, times(1)).getUserById(userId);
    }

    @Test
    public void testAddUser() throws Exception {
        // arrange
        User user = new User();
        user.setName("Jane Doe");
        Mockito.when(userService.addUser(user)).thenReturn(user);

        // act
        MvcResult result = mockMvc.perform(post("/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content("{"name":"Jane Doe"}"))
                .andExpect(status().isOk())
                .andReturn();

        // assert
        String response = result.getResponse().getContentAsString();
        assertThat(response).isEqualTo("{"id":null,"name":"Jane Doe"}");
        Mockito.verify(userService, times(1)).addUser(user);
    }
}

在这个例子中,我们使用@WebMvcTest注解来测试UserController类,并注入MockMvc对象来模拟HTTP请求。我们还使用@MockBean注释为UserService和UserRepository类创建模拟对象。

注意,这里不需要在测试之间重置Mock对象,因为@MockBean注解会为每个测试方法创建Mock对象的新实例。

4.不验证Mock对象

Mockito 提供了 Mockito.verify()的方法,可用于验证是否使用特定参数调用了Mock对象。如果Mock对象未经验证,可能会导致不可靠的测试和意外的行为。

Mockito.verify()使用示例:

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    public void testGetUserById() {
        // arrange
        Long userId = 1L;
        User user = new User();
        user.setId(userId);
        user.setName("John Doe");
        Mockito.when(userRepository.findById(userId)).thenReturn(Optional.of(user));

        // act
        User result = userService.getUserById(userId);

        // assert
        assertThat(result).isEqualTo(user);
        Mockito.verify(userRepository, times(1)).findById(userId);
    }

    @Test
    public void testGetUserByIdNotFound() {
        // arrange
        Long userId = 1L;
        Mockito.when(userRepository.findById(userId)).thenReturn(Optional.empty());

        // act
        UserNotFoundException exception = assertThrows(UserNotFoundException.class, () -> {
            userService.getUserById(userId);
        });

        // assert
        assertThat(exception.getMessage()).isEqualTo("User not found with ID: " + userId);
        Mockito.verify(userRepository, times(1)).findById(userId);
    }
}

请注意,我们使用该Mockito.verify()方法来验证两个测试方法是否使用正确的 ID 并仅调用了该类的findById()方法一次。使用times(1)参数来指定该方法应该被调用一次,并传入正确的 ID 作为参数。如果未使用正确的 ID 调用该方法,或者多次调用该方法,则测试将失败。

5.不指定Mock对象的行为

Mockito 默认创建Mock对象,默认行为是“不执行任何操作”。这意味着,如果在Mock对象上调用方法并且未指定任何行为,则该方法将仅返回 null 或其返回类型的默认值。指定Mock对象的行为来确保它们在测试中按预期运行非常重要。下面是使用Mockito.when()方法指定Mock对象的行为的示例:

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    public void testGetAllUsers() {
        // arrange
        List users = Arrays.asList(
                new User(1L, "John Doe"),
                new User(2L, "Jane Doe")
        );
        Mockito.when(userRepository.findAll()).thenReturn(users);

        // act
        List result = userService.getAllUsers();

        // assert
        assertThat(result).isEqualTo(users);
    }

    @Test
    public void testGetAllUsersEmpty() {
        // arrange
        List users = Collections.emptyList();
        Mockito.when(userRepository.findAll()).thenReturn(users);

        // act
        List result = userService.getAllUsers();

        // assert
        assertThat(result).isEqualTo(users);
    }
}

6.使用错误的方法验证模拟对象

Mockito 提供了几种方法来验证是否使用特定参数调用了Mock对象,例如Mockito.verify()、Mockito.verifyZeroInteractions () 和Mockito.verifyNoMoreInteractions () 。使用正确的方法进行所需的验证非常重要,因为使用错误的方法可能会导致不可靠的测试和意外的行为。Mockito.verify()方法使用示例:

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    public void testGetAllUsers() {
        // arrange
        List users = Arrays.asList(
                new User(1L, "John Doe"),
                new User(2L, "Jane Doe")
        );
        Mockito.when(userRepository.findAll()).thenReturn(users);

        // act
        List result = userService.getAllUsers();

        // assert
        assertThat(result).isEqualTo(users);
        Mockito.verify(userRepository).findAll();
        Mockito.verifyNoMoreInteractions(userRepository);
    }

    @Test
    public void testEmptyUserList() {
        // arrange
        List users = Collections.emptyList();
        Mockito.when(userRepository.findAll()).thenReturn(users);

        // act
        List result = userService.getAllUsers();

        // assert
        assertThat(result).isEqualTo(users);
        Mockito.verify(userRepository).findAll();
        Mockito.verifyNoMoreInteractions(userRepository);
        Mockito.verifyZeroInteractions(userRepository);
    }
}

在第二个测试用例中,我们使用Mockito.verifyZeroInteractions()方法来验证测试期间没有与Mock对象发生交互。这确保只测试我们想要测试的行为,并且代码中不会发生意外的交互。

7.不处理异常

以下是使用 Mockito 时如何处理异常的示例:

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    public void testGetUserById() {
        // arrange
        Long userId = 1L;
        User user = new User();
        user.setId(userId);
        user.setName("John Doe");
        Mockito.when(userRepository.findById(userId)).thenReturn(Optional.of(user));

        // act
        User result = userService.getUserById(userId);

        // assert
        assertThat(result).isEqualTo(user);
    }

    @Test
    public void testGetUserByIdNotFound() {
        // arrange
        Long userId = 1L;
        Mockito.when(userRepository.findById(userId)).thenReturn(Optional.empty());

        // act and assert
        UserNotFoundException exception = assertThrows(UserNotFoundException.class, () -> {
            userService.getUserById(userId);
        });

        assertThat(exception.getMessage()).isEqualTo("User not found with ID: " + userId);
    }
}

在testGetUserByIdNotFound()方法中,我们Mock UserRepository 类的 findById() 方法以返回一个空的可选值。然后,我们使用特定 ID 调用UserService类的getUserById()方法,并且期望该方法抛出UserNotFoundException. 然后使用assertThrows()方法来验证是否抛出了正确的异常,并且我们还使用getMessage()异常的方法来验证是否返回了正确的消息。

8.不使用正确的匹配器

以下是使用 Mockito 时如何使用正确匹配器的示例:

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    public void testAddUser() {
        // arrange
        User user = new User();
        user.setName("John Doe");
        user.setAge(30);

        // act
        userService.addUser(user);

        // assert
        ArgumentCaptor captor = ArgumentCaptor.forClass(User.class);
        Mockito.verify(userRepository).save(captor.capture());
        assertThat(captor.getValue().getName()).isEqualTo("John Doe");
        assertThat(captor.getValue().getAge()).isEqualTo(30);
    }
}

使用ArgumentCaptor类来捕获传递给UserRepository类的save()方法的参数值。我们还使用Mockito.eq()方法来指定方法调用的参数值,使用user.getName()和user.getAge()方法来获取正确的值。这有助于确保向方法传递正确的参数,并避免在测试中出现意外的行为。

下面是另一个示例:

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    public void testDeleteUserById() {
        // arrange
        Long userId = 1L;

        // act
        userService.deleteUserById(userId);

        // assert
        Mockito.verify(userRepository, Mockito.times(1)).deleteById(Mockito.eq(userId));
    }
}

使用Mockito.eq()方法来指定deleteById()方法调用的参数值。这确保了正确的ID被传递给该方法,并避免了测试中的意外行为。

9.没有对Mock对象使用正确的注解

以下是使用@MockBean 和 @RunWith 注解示例:

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTest {

    @Autowired
    private UserService userService;

    @MockBean
    private UserRepository userRepository;

    @Test
    public void testGetAllUsers() {
        // arrange
        List users = Arrays.asList(
                new User(1L, "John Doe"),
                new User(2L, "Jane Doe")
        );
        Mockito.when(userRepository.findAll()).thenReturn(users);

        // act
        List result = userService.getAllUsers();

        // assert
        assertThat(result).isEqualTo(users);
    }
}

使用@RunWith和@SpringBootTest注解来配置单元测试的Spring测试框架。通过使用这些注解,我们可以确保应用程序上下文被加载并且依赖项被正确地注入。

10.未使用正确的测试配置

我们希望使用正确的配置,以确保正确加载应用程序上下文并按预期注入依赖项。以下是使用@ContextConfiguration 的示例:

@RunWith(MockitoJUnitRunner.class)
@ContextConfiguration(classes = {UserService.class, UserRepository.class})
public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    public void testGetAllUsers() {
        // arrange
        List users = Arrays.asList(
                new User(1L, "John Doe"),
                new User(2L, "Jane Doe")
        );
        Mockito.when(userRepository.findAll()).thenReturn(users);

        // act
        List result = userService.getAllUsers();

        // assert
        assertThat(result).isEqualTo(users);
    }
}

使用@ContextConfiguration注解来指定测试的配置。我们将一个类数组传递给它,其中包括UserService和UserRepository类,这样可以确保它们被加载到应用程序上下文中。

11.没有使用正确的方法来创建Mock对象

使用正确的方法来创建Mock对象,以确保依赖项的行为是可控的并且测试是可靠的。以下是使用Mockito.mock()的示例:

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {

    private UserService userService;

    private UserRepository userRepository;

    @Before
    public void setUp() {
        userRepository = Mockito.mock(UserRepository.class);
        userService = new UserService(userRepository);
    }

    @Test
    public void testGetAllUsers() {
        // arrange
        List users = Arrays.asList(
                new User(1L, "John Doe"),
                new User(2L, "Jane Doe")
        );
        Mockito.when(userRepository.findAll()).thenReturn(users);

        // act
        List result = userService.getAllUsers();

        // assert
        assertThat(result).isEqualTo(users);
    }
}

使用了Mockito.when()方法来指定Mock对象的行为,即当findAll()方法被调用时,返回一个User对象的列表。

12.没有使用正确的方法来存根Mock对象

使用正确的方法来存根Mock对象,以确保依赖项的行为可以控制并且测试是可靠的。以下是使用when().thenReturn()的示例:

@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {

    @Mock
    private UserRepository userRepository;

    @InjectMocks
    private UserService userService;

    @Test
    public void testGetAllUsers() {
        // arrange
        List users = Arrays.asList(
                new User(1L, "John Doe"),
                new User(2L, "Jane Doe")
        );
        Mockito.when(userRepository.findAll()).thenReturn(users);

        // act
        List result = userService.getAllUsers();

        // assert
        assertThat(result).isEqualTo(users);
    }
}

通过使用Mockito提供的when().thenReturn()方法,我们可以指定模拟对象的行为并确保在测试中控制依赖项。

13.没有使用正确的方法来验证Mock对象的交互

Mockito 提供了几种验证 Mock对象交互的方法,例如Mockito.verify()、Mockito.verifyZeroInteractions()和Mockito.verifyNoMoreInteractions()。使用正确的方法来实现所需的行为非常重要,因为使用错误的方法可能会导致不可靠的测试和意外的行为。

@Test
public void test() {
    MyObject myObject = new MyObject();
    myObject.setName("Name");
    Mockito.when(myRepository.findById(1)).thenReturn(Optional.of(myObject));
    
    MyObject result = myService.findById(1);
    
    Mockito.verify(myRepository).findById(1);
    Mockito.verifyNoMoreInteractions(myRepository);
    Assert.assertEquals("Name", result.getName());
}

14.没有使用正确的方法来验证Mock对象的交互顺序

Mockito 提供了一个名为Mockito.inOrder()的方法,可用于验证与模拟对象交互的顺序。在验证交互顺序时使用此方法非常重要。

@Test
public void test() {
    MyObject myObject1 = new MyObject();
    myObject1.setName("Name 1");
    MyObject myObject2 = new MyObject();
    myObject2.setName("Name 2");
    InOrder inOrder = Mockito.inOrder(myRepository);
    
    Mockito.when(myRepository.findById(1)).thenReturn(Optional.of(myObject1));
    Mockito.when(myRepository.findById(2)).thenReturn(Optional.of(myObject2));
    
    MyObject result1 = myService.findById(1);
    MyObject result2 = myService.findById(2);
    
    inOrder.verify(myRepository).findById(1);
    inOrder.verify(myRepository).findById(2);
    Assert.assertEquals("Name 1", result1.getName());
    Assert.assertEquals("Name 2", result2.getName());
}

结论

Mockito 是一个强大的测试框架。但是,刚接触 Mockito 的开发人员可能会犯错误,从而导致应用程序中的测试不可靠和出现意外行为。

相关文章

JavaScript2024新功能:Object.groupBy、正则表达式v标志
PHP trim 函数对多字节字符的使用和限制
新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
为React 19做准备:WordPress 6.6用户指南
如何删除WordPress中的所有评论

发布评论