Mockito 避坑指南 - 常見錯誤的預(yù)防與處理
介紹
Mockito 是一個流行的用于測試 Java 應(yīng)用程序的框架。它提供了一種強大且易于使用的方式來模擬依賴關(guān)系和編寫單元測試。然而,剛接觸 Mockito 的開發(fā)人員可能會犯一些錯誤,從而導(dǎo)致測試不可靠,甚至導(dǎo)致應(yīng)用程序出現(xiàn)意外行為。在本文中,我們將討論開發(fā)人員在 Spring Boot 應(yīng)用程序中使用 Mockito 框架時犯的常見錯誤,以及代碼示例和解釋。
1.濫用@Mock和@InjectMocks注釋
開發(fā)人員在使用 Mockito 時最常見的錯誤之一是濫用@Mock和@InjectMocks注釋。@Mock注解用于為特定類創(chuàng)建模擬對象,而@InjectMocks注解用于將模擬對象注入到被測試的類中。需要注意的是,@InjectMocks 只能與類一起使用,不能與接口一起使用。
例子:
@RunWith(MockitoJUnitRunner.class)
public class MyServiceTest {
@Mock
private MyRepository myRepository;
@InjectMocks
private MyService myService;
// test methods
}
2.不重置Mock對象
Mockito 可創(chuàng)建在多個測試中重用的Mock對象。如果在測試之間未重置Mock對象,則可能會導(dǎo)致意外行為和不可靠的測試。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 默認(rèn)創(chuàng)建范圍為類級別。這意味著同一個Mock對象將用于類中的所有測試方法。但是,如果模擬對象需要為每個測試方法具有不同的狀態(tài)或行為,則應(yīng)使用方法級別的范圍來創(chuàng)建。要創(chuàng)建具有正確范圍的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類創(chuàng)建模擬對象。
注意,這里不需要在測試之間重置Mock對象,因為@MockBean注解會為每個測試方法創(chuàng)建Mock對象的新實例。
4.不驗證Mock對象
Mockito 提供了 Mockito.verify()的方法,可用于驗證是否使用特定參數(shù)調(diào)用了Mock對象。如果Mock對象未經(jīng)驗證,可能會導(dǎo)致不可靠的測試和意外的行為。
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 并僅調(diào)用了該類的findById()方法一次。使用times(1)參數(shù)來指定該方法應(yīng)該被調(diào)用一次,并傳入正確的 ID 作為參數(shù)。如果未使用正確的 ID 調(diào)用該方法,或者多次調(diào)用該方法,則測試將失敗。
5.不指定Mock對象的行為
Mockito 默認(rèn)創(chuàng)建Mock對象,默認(rèn)行為是“不執(zhí)行任何操作”。這意味著,如果在Mock對象上調(diào)用方法并且未指定任何行為,則該方法將僅返回 null 或其返回類型的默認(rèn)值。指定Mock對象的行為來確保它們在測試中按預(yù)期運行非常重要。下面是使用Mockito.when()方法指定Mock對象的行為的示例:
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
public void testGetAllUsers() {
// arrange
List<User> users = Arrays.asList(
new User(1L, "John Doe"),
new User(2L, "Jane Doe")
);
Mockito.when(userRepository.findAll()).thenReturn(users);
// act
List<User> result = userService.getAllUsers();
// assert
assertThat(result).isEqualTo(users);
}
@Test
public void testGetAllUsersEmpty() {
// arrange
List<User> users = Collections.emptyList();
Mockito.when(userRepository.findAll()).thenReturn(users);
// act
List<User> result = userService.getAllUsers();
// assert
assertThat(result).isEqualTo(users);
}
}
6.使用錯誤的方法驗證模擬對象
Mockito 提供了幾種方法來驗證是否使用特定參數(shù)調(diào)用了Mock對象,例如Mockito.verify()、Mockito.verifyZeroInteractions () 和Mockito.verifyNoMoreInteractions () 。使用正確的方法進(jìn)行所需的驗證非常重要,因為使用錯誤的方法可能會導(dǎo)致不可靠的測試和意外的行為。Mockito.verify()方法使用示例:
@RunWith(MockitoJUnitRunner.class)
public class UserServiceTest {
@Mock
private UserRepository userRepository;
@InjectMocks
private UserService userService;
@Test
public void testGetAllUsers() {
// arrange
List<User> users = Arrays.asList(
new User(1L, "John Doe"),
new User(2L, "Jane Doe")
);
Mockito.when(userRepository.findAll()).thenReturn(users);
// act
List<User> result = userService.getAllUsers();
// assert
assertThat(result).isEqualTo(users);
Mockito.verify(userRepository).findAll();
Mockito.verifyNoMoreInteractions(userRepository);
}
@Test
public void testEmptyUserList() {
// arrange
List<User> users = Collections.emptyList();
Mockito.when(userRepository.findAll()).thenReturn(users);
// act
List<User> result = userService.getAllUsers();
// assert
assertThat(result).isEqualTo(users);
Mockito.verify(userRepository).findAll();
Mockito.verifyNoMoreInteractions(userRepository);
Mockito.verifyZeroInteractions(userRepository);
}
}
在第二個測試用例中,我們使用Mockito.verifyZeroInteractions()方法來驗證測試期間沒有與Mock對象發(fā)生交互。這確保只測試我們想要測試的行為,并且代碼中不會發(fā)生意外的交互。
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 調(diào)用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<User> 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()方法的參數(shù)值。我們還使用Mockito.eq()方法來指定方法調(diào)用的參數(shù)值,使用user.getName()和user.getAge()方法來獲取正確的值。這有助于確保向方法傳遞正確的參數(shù),并避免在測試中出現(xiàn)意外的行為。
下面是另一個示例:
@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()方法調(diào)用的參數(shù)值。這確保了正確的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<User> users = Arrays.asList(
new User(1L, "John Doe"),
new User(2L, "Jane Doe")
);
Mockito.when(userRepository.findAll()).thenReturn(users);
// act
List<User> result = userService.getAllUsers();
// assert
assertThat(result).isEqualTo(users);
}
}
使用@RunWith和@SpringBootTest注解來配置單元測試的Spring測試框架。通過使用這些注解,我們可以確保應(yīng)用程序上下文被加載并且依賴項被正確地注入。
10.未使用正確的測試配置
我們希望使用正確的配置,以確保正確加載應(yīng)用程序上下文并按預(yù)期注入依賴項。以下是使用@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<User> users = Arrays.asList(
new User(1L, "John Doe"),
new User(2L, "Jane Doe")
);
Mockito.when(userRepository.findAll()).thenReturn(users);
// act
List<User> result = userService.getAllUsers();
// assert
assertThat(result).isEqualTo(users);
}
}
使用@ContextConfiguration注解來指定測試的配置。我們將一個類數(shù)組傳遞給它,其中包括UserService和UserRepository類,這樣可以確保它們被加載到應(yīng)用程序上下文中。
11.沒有使用正確的方法來創(chuàng)建Mock對象
使用正確的方法來創(chuàng)建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<User> users = Arrays.asList(
new User(1L, "John Doe"),
new User(2L, "Jane Doe")
);
Mockito.when(userRepository.findAll()).thenReturn(users);
// act
List<User> result = userService.getAllUsers();
// assert
assertThat(result).isEqualTo(users);
}
}
使用了Mockito.when()方法來指定Mock對象的行為,即當(dāng)findAll()方法被調(diào)用時,返回一個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<User> users = Arrays.asList(
new User(1L, "John Doe"),
new User(2L, "Jane Doe")
);
Mockito.when(userRepository.findAll()).thenReturn(users);
// act
List<User> result = userService.getAllUsers();
// assert
assertThat(result).isEqualTo(users);
}
}
通過使用Mockito提供的when().thenReturn()方法,我們可以指定模擬對象的行為并確保在測試中控制依賴項。
13.沒有使用正確的方法來驗證Mock對象的交互
Mockito 提供了幾種驗證 Mock對象交互的方法,例如Mockito.verify()、Mockito.verifyZeroInteractions()和Mockito.verifyNoMoreInteractions()。使用正確的方法來實現(xiàn)所需的行為非常重要,因為使用錯誤的方法可能會導(dǎo)致不可靠的測試和意外的行為。
@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());
}
結(jié)論
Mockito 是一個強大的測試框架。但是,剛接觸 Mockito 的開發(fā)人員可能會犯錯誤,從而導(dǎo)致應(yīng)用程序中的測試不可靠和出現(xiàn)意外行為。