
作者 | 常意
來源 |
阿裡技術公衆号前言
高德的技術大佬向老師在談論方法論時說到:“複雜的問題要簡單化,簡單的問題要深入化。”
這句話讓我感觸頗深,這何嘗不是一套編寫代碼的方法——把一個複雜邏輯拆分為許多簡單邏輯,然後把每一個簡單邏輯進行深入實作,最後把這些簡單邏輯整合為複雜邏輯,總結為八字真言即是“化繁為簡,由簡入繁”。
編寫Java單元測試用例,其實就是把“複雜的問題要簡單化”——即把一段複雜的代碼拆解成一系列簡單的單元測試用例;寫好Java單元測試用例,其實就是把“簡單的問題要深入化”——即學習一套方法、總結一套模式并應用到實踐中。這裡,作者根據日常的工作經驗,總結了一些Java單元測試技巧,以供大家交流和學習。
1. 準備環境
PowerMock是一個擴充了其它如EasyMock等mock架構的、功能更加強大的架構。PowerMock使用一個自定義類加載器和位元組碼操作來模拟靜态方法、構造方法、final類和方法、私有方法、去除靜态初始化器等等。
1.1. 引入PowerMock包
為了引入PowerMock包,需要在pom.xml檔案中加入下列maven依賴:
1.2. 內建SpringMVC項目
在SpringMVC項目中,需要在pom.xml檔案中加入JUnit的maven依賴:
1.3. 內建SpringBoot項目
在SpringBoot項目中,需要在pom.xml檔案中加入JUnit的maven依賴:
1.4. 一個簡單的測試用例
這裡,用List舉例,模拟一個不存在的清單,但是傳回的清單大小為100。
public class ListTest {
@Test
public void testSize() {
Integer expected = 100;
List list = PowerMockito.mock(List.class);
PowerMockito.when(list.size()).thenReturn(expected);
Integer actual = list.size();
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
2. mock語句
2.1. mock方法
聲明:
T PowerMockito.mock(Class clazz);
用途:
可以用于模拟指定類的對象執行個體。
當模拟非final類(接口、普通類、虛基類)的非final方法時,不必使用@RunWith和@PrepareForTest注解。當模拟final類或final方法時,必須使用@RunWith和@PrepareForTest注解。注解形如:
@RunWith(PowerMockRunner.class)
@PrepareForTest({TargetClass.class})
2.1.1. 模拟非final類普通方法
@Getter
@Setter
@ToString
public class Rectangle implements Sharp {
private double width;
private double height;
@Override
public double getArea() {
return width * height;
}
}
public class RectangleTest {
@Test
public void testGetArea() {
double expectArea = 100.0D;
Rectangle rectangle = PowerMockito.mock(Rectangle.class);
PowerMockito.when(rectangle.getArea()).thenReturn(expectArea);
double actualArea = rectangle.getArea();
Assert.assertEquals("傳回值不相等", expectArea, actualArea, 1E-6D);
}
}
2.1.2. 模拟final類或final方法
@Getter
@Setter
@ToString
public final class Circle {
private double radius;
public double getArea() {
return Math.PI * Math.pow(radius, 2);
}
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({Circle.class})
public class CircleTest {
@Test
public void testGetArea() {
double expectArea = 3.14D;
Circle circle = PowerMockito.mock(Circle.class);
PowerMockito.when(circle.getArea()).thenReturn(expectArea);
double actualArea = circle.getArea();
Assert.assertEquals("傳回值不相等", expectArea, actualArea, 1E-6D);
}
}
2.2. mockStatic方法
PowerMockito.mockStatic(Class clazz);
可以用于模拟類的靜态方法,必須使用“@RunWith”和“@PrepareForTest”注解。
@RunWith(PowerMockRunner.class)
@PrepareForTest({StringUtils.class})
public class StringUtilsTest {
@Test
public void testIsEmpty() {
String string = "abc";
boolean expected = true;
PowerMockito.mockStatic(StringUtils.class);
PowerMockito.when(StringUtils.isEmpty(string)).thenReturn(expected);
boolean actual = StringUtils.isEmpty(string);
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
3. spy語句
如果一個對象,我們隻希望模拟它的部分方法,而希望其它方法跟原來一樣,可以使用PowerMockito.spy方法代替PowerMockito.mock方法。于是,通過when語句設定過的方法,調用的是模拟方法;而沒有通過when語句設定的方法,調用的是原有方法。
3.1. spy類
PowerMockito.spy(Class clazz);
用于模拟類的部分方法。
案例:
public class StringUtils {
public static boolean isNotEmpty(final CharSequence cs) {
return !isEmpty(cs);
}
public static boolean isEmpty(final CharSequence cs) {
return cs == null || cs.length() == 0;
}
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({StringUtils.class})
public class StringUtilsTest {
@Test
public void testIsNotEmpty() {
String string = null;
boolean expected = true;
PowerMockito.spy(StringUtils.class);
PowerMockito.when(StringUtils.isEmpty(string)).thenReturn(!expected);
boolean actual = StringUtils.isNotEmpty(string);
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
3.2. spy對象
T PowerMockito.spy(T object);
用于模拟對象的部分方法。
public class UserService {
private Long superUserId;
public boolean isNotSuperUser(Long userId) {
return !isSuperUser(userId);
}
public boolean isSuperUser(Long userId) {
return Objects.equals(userId, superUserId);
}
}
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
@Test
public void testIsNotSuperUser() {
Long userId = 1L;
boolean expected = false;
UserService userService = PowerMockito.spy(new UserService());
PowerMockito.when(userService.isSuperUser(userId)).thenReturn(!expected);
boolean actual = userService.isNotSuperUser(userId);
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
4. when語句
4.1. when().thenReturn()模式
PowerMockito.when(mockObject.someMethod(someArgs)).thenReturn(expectedValue);
PowerMockito.when(mockObject.someMethod(someArgs)).thenThrow(expectedThrowable);
PowerMockito.when(mockObject.someMethod(someArgs)).thenAnswer(expectedAnswer);
PowerMockito.when(mockObject.someMethod(someArgs)).thenCallRealMethod();
用于模拟對象方法,先執行原始方法,再傳回期望的值、異常、應答,或調用真實的方法。
4.1.1. 傳回期望值
public class ListTest {
@Test
public void testGet() {
int index = 0;
Integer expected = 100;
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.when(mockList.get(index)).thenReturn(expected);
Integer actual = mockList.get(index);
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
4.1.2. 傳回期望異常
public class ListTest {
@Test(expected = IndexOutOfBoundsException.class)
public void testGet() {
int index = -1;
Integer expected = 100;
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.when(mockList.get(index)).thenThrow(new IndexOutOfBoundsException());
Integer actual = mockList.get(index);
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
4.1.3. 傳回期望應答
public class ListTest {
@Test
public void testGet() {
int index = 1;
Integer expected = 100;
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.when(mockList.get(index)).thenAnswer(invocation -> {
Integer value = invocation.getArgument(0);
return value * 100;
});
Integer actual = mockList.get(index);
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
4.1.4. 調用真實方法
public class ListTest {
@Test
public void testGet() {
int index = 0;
Integer expected = 100;
List<Integer> oldList = new ArrayList<>();
oldList.add(expected);
List<Integer> spylist = PowerMockito.spy(oldList);
PowerMockito.when(spylist.get(index)).thenCallRealMethod();
Integer actual = spylist.get(index);
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
4.2. doReturn().when()模式
PowerMockito.doReturn(expectedValue).when(mockObject).someMethod(someArgs);
PowerMockito.doThrow(expectedThrowable).when(mockObject).someMethod(someArgs);
PowerMockito.doAnswer(expectedAnswer).when(mockObject).someMethod(someArgs);
PowerMockito.doNothing().when(mockObject).someMethod(someArgs);
PowerMockito.doCallRealMethod().when(mockObject).someMethod(someArgs);
用于模拟對象方法,直接傳回期望的值、異常、應答,或調用真實的方法,無需執行原始方法。
注意:
千萬不要使用以下文法:
PowerMockito.doReturn(expectedValue).when(mockObject.someMethod(someArgs));
PowerMockito.doThrow(expectedThrowable).when(mockObject.someMethod(someArgs));
PowerMockito.doAnswer(expectedAnswer).when(mockObject.someMethod(someArgs));
PowerMockito.doNothing().when(mockObject.someMethod(someArgs));
PowerMockito.doCallRealMethod().when(mockObject.someMethod(someArgs));
雖然不會出現編譯錯誤,但是在執行時會抛出UnfinishedStubbingException異常。
4.2.1. 傳回期望值
public class ListTest {
@Test
public void testGet() {
int index = 0;
Integer expected = 100;
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.doReturn(expected).when(mockList).get(index);
Integer actual = mockList.get(index);
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
4.2.2. 傳回期望異常
public class ListTest {
@Test(expected = IndexOutOfBoundsException.class)
public void testGet() {
int index = -1;
Integer expected = 100;
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.doThrow(new IndexOutOfBoundsException()).when(mockList).get(index);
Integer actual = mockList.get(index);
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
4.2.3. 傳回期望應答
public class ListTest {
@Test
public void testGet() {
int index = 1;
Integer expected = 100;
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.doAnswer(invocation -> {
Integer value = invocation.getArgument(0);
return value * 100;
}).when(mockList).get(index);
Integer actual = mockList.get(index);
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
4.2.4. 模拟無傳回值
public class ListTest {
@Test
public void testClear() {
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.doNothing().when(mockList).clear();
mockList.clear();
Mockito.verify(mockList).clear();
}
}
4.2.5. 調用真實方法
public class ListTest {
@Test
public void testGet() {
int index = 0;
Integer expected = 100;
List<Integer> oldList = new ArrayList<>();
oldList.add(expected);
List<Integer> spylist = PowerMockito.spy(oldList);
PowerMockito.doCallRealMethod().when(spylist).get(index);
Integer actual = spylist.get(index);
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
4.3. 兩種模式的主要差別
兩種模式都用于模拟對象方法,在mock執行個體下使用時,基本上是沒有差别的。但是,在spy執行個體下使用時,when().thenReturn()模式會執行原方法,而doReturn().when()模式不會執行原方法。
測試服務類:
@Slf4j
@Service
public class UserService {
public long getUserCount() {
log.info("調用擷取使用者數量方法");
return 0L;
}
}
使用when().thenReturn()模式:
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
@Test
public void testGetUserCount() {
Long expected = 1000L;
UserService userService = PowerMockito.spy(new UserService());
PowerMockito.when(userService.getUserCount()).thenReturn(expected);
Long actual = userService.getUserCount();
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
在測試過程中,将會列印出"調用擷取使用者數量方法"日志。
使用doReturn().when()模式:
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
@Test
public void testGetUserCount() {
Long expected = 1000L;
UserService userService = PowerMockito.spy(new UserService());
PowerMockito.doReturn(expected).when(userService).getUserCount();
Long actual = userService.getUserCount();
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
在測試過程中,不會列印出"調用擷取使用者數量方法"日志。
4.4. whenNew模拟構造方法
PowerMockito.whenNew(MockClass.class).withNoArguments().thenReturn(expectedObject);
PowerMockito.whenNew(MockClass.class).withArguments(someArgs).thenReturn(expectedObject);
用于模拟構造方法。
public final class FileUtils {
public static boolean isFile(String fileName) {
return new File(fileName).isFile();
}
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({FileUtils.class})
public class FileUtilsTest {
@Test
public void testIsFile() throws Exception {
String fileName = "test.txt";
File file = PowerMockito.mock(File.class);
PowerMockito.whenNew(File.class).withArguments(fileName).thenReturn(file);
PowerMockito.when(file.isFile()).thenReturn(true);
Assert.assertTrue("傳回值為假", FileUtils.isFile(fileName));
}
}
注意:需要加上注解@PrepareForTest({FileUtils.class}),否則模拟方法不生效。
5. 參數比對器
在執行單元測試時,有時候并不關心傳入的參數的值,可以使用參數比對器。
5.1. 參數比對器(any)
Mockito提供Mockito.anyInt()、Mockito.anyString、Mockito.any(Class clazz)等來表示任意值。
public class ListTest {
@Test
public void testGet() {
int index = 1;
Integer expected = 100;
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.when(mockList.get(Mockito.anyInt())).thenReturn(expected);
Integer actual = mockList.get(index);
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
5.2. 參數比對器(eq)
當我們使用參數比對器時,所有參數都應使用比對器。 如果要為某一參數指定特定值時,就需要使用Mockito.eq()方法。
@RunWith(PowerMockRunner.class)
@PrepareForTest({StringUtils.class})
public class StringUtilsTest {
@Test
public void testStartWith() {
String string = "abc";
String prefix = "b";
boolean expected = true;
PowerMockito.spy(StringUtils.class);
PowerMockito.when(StringUtils.startsWith(Mockito.anyString(), Mockito.eq(prefix))).thenReturn(expected);
boolean actual = StringUtils.startsWith(string, prefix);
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
5.3. 附加比對器
Mockito的AdditionalMatchers類提供了一些很少使用的參數比對器,我們可以進行參數大于(gt)、小于(lt)、大于等于(geq)、小于等于(leq)等比較操作,也可以進行參數與(and)、或(or)、非(not)等邏輯計算等。
public class ListTest {
@Test
public void testGet() {
int index = 1;
Integer expected = 100;
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.when(mockList.get(AdditionalMatchers.geq(0))).thenReturn(expected);
PowerMockito.when(mockList.get(AdditionalMatchers.lt(0))).thenThrow(new IndexOutOfBoundsException());
Integer actual = mockList.get(index);
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
6. verify語句
驗證是确認在模拟過程中,被測試方法是否已按預期方式與其任何依賴方法進行了互動。
格式:
Mockito.verify(mockObject[,times(int)]).someMethod(somgArgs);
6.1. 驗證調用方法
public class ListTest {
@Test
public void testGet() {
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.doNothing().when(mockList).clear();
mockList.clear();
Mockito.verify(mockList).clear();
}
}
6.2. 驗證調用次數
public class ListTest {
@Test
public void testGet() {
List<Integer> mockList = PowerMockito.mock(List.class);
PowerMockito.doNothing().when(mockList).clear();
mockList.clear();
Mockito.verify(mockList, Mockito.times(1)).clear();
}
}
除times外,Mockito還支援atLeastOnce、atLeast、only、atMostOnce、atMost等次數驗證器。
6.3. 驗證調用順序
public class ListTest {
@Test
public void testAdd() {
List<Integer> mockedList = PowerMockito.mock(List.class);
PowerMockito.doReturn(true).when(mockedList).add(Mockito.anyInt());
mockedList.add(1);
mockedList.add(2);
mockedList.add(3);
InOrder inOrder = Mockito.inOrder(mockedList);
inOrder.verify(mockedList).add(1);
inOrder.verify(mockedList).add(2);
inOrder.verify(mockedList).add(3);
}
}
6.4. 驗證調用參數
public class ListTest {
@Test
public void testArgumentCaptor() {
Integer[] expecteds = new Integer[] {1, 2, 3};
List<Integer> mockedList = PowerMockito.mock(List.class);
PowerMockito.doReturn(true).when(mockedList).add(Mockito.anyInt());
for (Integer expected : expecteds) {
mockedList.add(expected);
}
ArgumentCaptor<Integer> argumentCaptor = ArgumentCaptor.forClass(Integer.class);
Mockito.verify(mockedList, Mockito.times(3)).add(argumentCaptor.capture());
Integer[] actuals = argumentCaptor.getAllValues().toArray(new Integer[0]);
Assert.assertArrayEquals("傳回值不相等", expecteds, actuals);
}
}
6.5. 確定驗證完畢
Mockito提供Mockito.verifyNoMoreInteractions方法,在所有驗證方法之後可以使用此方法,以確定所有調用都得到驗證。如果模拟對象上存在任何未驗證的調用,将會抛出NoInteractionsWanted異常。
public class ListTest {
@Test
public void testVerifyNoMoreInteractions() {
List<Integer> mockedList = PowerMockito.mock(List.class);
Mockito.verifyNoMoreInteractions(mockedList); // 執行正常
mockedList.isEmpty();
Mockito.verifyNoMoreInteractions(mockedList); // 抛出異常
}
}
備注:Mockito.verifyZeroInteractions方法與Mockito.verifyNoMoreInteractions方法相同,但是目前已經被廢棄。
6.6. 驗證靜态方法
Mockito沒有靜态方法的驗證方法,但是PowerMock提供這方面的支援。
@RunWith(PowerMockRunner.class)
@PrepareForTest({StringUtils.class})
public class StringUtilsTest {
@Test
public void testVerifyStatic() {
PowerMockito.mockStatic(StringUtils.class);
String expected = "abc";
StringUtils.isEmpty(expected);
PowerMockito.verifyStatic(StringUtils.class);
ArgumentCaptor<String> argumentCaptor = ArgumentCaptor.forClass(String.class);
StringUtils.isEmpty(argumentCaptor.capture());
Assert.assertEquals("參數不相等", argumentCaptor.getValue(), expected);
}
}
7. 私有屬性
7.1. ReflectionTestUtils.setField方法
在用原生JUnit進行單元測試時,我們一般采用ReflectionTestUtils.setField方法設定私有屬性值。
@Service
public class UserService {
@Value("${system.userLimit}")
private Long userLimit;
public Long getUserLimit() {
return userLimit;
}
}
public class UserServiceTest {
@Autowired
private UserService userService;
@Test
public void testGetUserLimit() {
Long expected = 1000L;
ReflectionTestUtils.setField(userService, "userLimit", expected);
Long actual = userService.getUserLimit();
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
注意:在測試類中,UserService執行個體是通過@Autowired注解加載的,如果該執行個體已經被動态代理,ReflectionTestUtils.setField方法設定的是代理執行個體,進而導緻設定不生效。
7.2. Whitebox.setInternalState方法
現在使用PowerMock進行單元測試時,可以采用Whitebox.setInternalState方法設定私有屬性值。
@Service
public class UserService {
@Value("${system.userLimit}")
private Long userLimit;
public Long getUserLimit() {
return userLimit;
}
}
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
@InjectMocks
private UserService userService;
@Test
public void testGetUserLimit() {
Long expected = 1000L;
Whitebox.setInternalState(userService, "userLimit", expected);
Long actual = userService.getUserLimit();
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
注意:需要加上注解@RunWith(PowerMockRunner.class)。
8. 私有方法
8.1. 模拟私有方法
8.1.1. 通過when實作
public class UserService {
private Long superUserId;
public boolean isNotSuperUser(Long userId) {
return !isSuperUser(userId);
}
private boolean isSuperUser(Long userId) {
return Objects.equals(userId, superUserId);
}
}
@RunWith(PowerMockRunner.class)
@PrepareForTest({UserService.class})
public class UserServiceTest {
@Test
public void testIsNotSuperUser() throws Exception {
Long userId = 1L;
boolean expected = false;
UserService userService = PowerMockito.spy(new UserService());
PowerMockito.when(userService, "isSuperUser", userId).thenReturn(!expected);
boolean actual = userService.isNotSuperUser(userId);
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
8.1.2. 通過stub實作
通過模拟方法stub(存根),也可以實作模拟私有方法。但是,隻能模拟整個方法的傳回值,而不能模拟指定參數的傳回值。
@RunWith(PowerMockRunner.class)
@PrepareForTest({UserService.class})
public class UserServiceTest {
@Test
public void testIsNotSuperUser() throws Exception {
Long userId = 1L;
boolean expected = false;
UserService userService = PowerMockito.spy(new UserService());
PowerMockito.stub(PowerMockito.method(UserService.class, "isSuperUser", Long.class)).toReturn(!expected);
boolean actual = userService.isNotSuperUser(userId);
Assert.assertEquals("傳回值不相等", expected, actual;
}
}
8.3. 測試私有方法
@RunWith(PowerMockRunner.class)
public class UserServiceTest9 {
@Test
public void testIsSuperUser() throws Exception {
Long userId = 1L;
boolean expected = false;
UserService userService = new UserService();
Method method = PowerMockito.method(UserService.class, "isSuperUser", Long.class);
Object actual = method.invoke(userService, userId);
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
8.4. 驗證私有方法
@RunWith(PowerMockRunner.class)
@PrepareForTest({UserService.class})
public class UserServiceTest10 {
@Test
public void testIsNotSuperUser() throws Exception {
Long userId = 1L;
boolean expected = false;
UserService userService = PowerMockito.spy(new UserService());
PowerMockito.when(userService, "isSuperUser", userId).thenReturn(!expected);
boolean actual = userService.isNotSuperUser(userId);
PowerMockito.verifyPrivate(userService).invoke("isSuperUser", userId);
Assert.assertEquals("傳回值不相等", expected, actual);
}
}
這裡,也可以用Method那套方法進行模拟和驗證方法。
9. 主要注解
PowerMock為了更好地支援SpringMVC/SpringBoot項目,提供了一系列的注解,大大地簡化了測試代碼。
9.1. @RunWith注解
指定JUnit 使用 PowerMock 架構中的單元測試運作器。
9.2. @PrepareForTest注解
@PrepareForTest({ TargetClass.class })
當需要模拟final類、final方法或靜态方法時,需要添加@PrepareForTest注解,并指定方法所在的類。如果需要指定多個類,在{}中添加多個類并用逗号隔開即可。
9.3. @Mock注解
@Mock注解建立了一個全部Mock的執行個體,所有屬性和方法全被置空(0或者null)。
9.4. @Spy注解
@Spy注解建立了一個沒有Mock的執行個體,所有成員方法都會按照原方法的邏輯執行,直到被Mock傳回某個具體的值為止。
注意:@Spy注解的變量需要被初始化,否則執行時會抛出異常。
9.5. @InjectMocks注解
@InjectMocks注解建立一個執行個體,這個執行個體可以調用真實代碼的方法,其餘用@Mock或@Spy注解建立的執行個體将被注入到用該執行個體中。
@Service
public class UserService {
@Autowired
private UserDAO userDAO;
public void modifyUser(UserVO userVO) {
UserDO userDO = new UserDO();
BeanUtils.copyProperties(userVO, userDO);
userDAO.modify(userDO);
}
}
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
@Mock
private UserDAO userDAO;
@InjectMocks
private UserService userService;
@Test
public void testCreateUser() {
UserVO userVO = new UserVO();
userVO.setId(1L);
userVO.setName("changyi");
userVO.setDesc("test user");
userService.modifyUser(userVO);
ArgumentCaptor<UserDO> argumentCaptor = ArgumentCaptor.forClass(UserDO.class);
Mockito.verify(userDAO).modify(argumentCaptor.capture());
UserDO userDO = argumentCaptor.getValue();
Assert.assertNotNull("使用者執行個體為空", userDO);
Assert.assertEquals("使用者辨別不相等", userVO.getId(), userDO.getId());
Assert.assertEquals("使用者名稱不相等", userVO.getName(), userDO.getName());
Assert.assertEquals("使用者描述不相等", userVO.getDesc(), userDO.getDesc());
}
}
9.6. @Captor注解
@Captor注解在字段級别建立參數捕獲器。但是,在測試方法啟動前,必須調用MockitoAnnotations.openMocks(this)進行初始化。
@Service
public class UserService {
@Autowired
private UserDAO userDAO;
public void modifyUser(UserVO userVO) {
UserDO userDO = new UserDO();
BeanUtils.copyProperties(userVO, userDO);
userDAO.modify(userDO);
}
}
@RunWith(PowerMockRunner.class)
public class UserServiceTest {
@Mock
private UserDAO userDAO;
@InjectMocks
private UserService userService;
@Captor
private ArgumentCaptor<UserDO> argumentCaptor;
@Before
public void beforeTest() {
MockitoAnnotations.openMocks(this);
}
@Test
public void testCreateUser() {
UserVO userVO = new UserVO();
userVO.setId(1L);
userVO.setName("changyi");
userVO.setDesc("test user");
userService.modifyUser(userVO);
Mockito.verify(userDAO).modify(argumentCaptor.capture());
UserDO userDO = argumentCaptor.getValue();
Assert.assertNotNull("使用者執行個體為空", userDO);
Assert.assertEquals("使用者辨別不相等", userVO.getId(), userDO.getId());
Assert.assertEquals("使用者名稱不相等", userVO.getName(), userDO.getName());
Assert.assertEquals("使用者描述不相等", userVO.getDesc(), userDO.getDesc());
}
}
9.7. @PowerMockIgnore注解
為了解決使用PowerMock後,提示ClassLoader錯誤。
10. 相關觀點
10.1. 《Java開發手冊》規範
【強制】好的單元測試必須遵守AIR原則。 說明:單元測試線上上運作時,感覺像空氣(AIR)一樣感覺不到,但在測試品質的保障上,卻是非常關鍵的。好的單元測試宏觀上來說,具有自動化、獨立性、可重複執行的特點。
A:Automatic(自動化)
I:Independent(獨立性)
R:Repeatable(可重複)
【強制】單元測試應該是全自動執行的,并且非互動式的。測試用例通常是被定期執行的,執行過程必須完全自動化才有意義。輸出結果需要人工檢查的測試不是一個好的單元測試。單元測試中不準使用System.out來進行人肉驗證,必須使用assert來驗證。
【強制】單元測試是可以重複執行的,不能受到外界環境的影響。
說明:單元測試通常會被放到持續內建中,每次有代碼check in時單元測試都會被執行。如果單測對外部環境(網絡、服務、中間件等)有依賴,容易導緻持續內建機制的不可用。
正例:為了不受外界環境影響,要求設計代碼時就把SUT的依賴改成注入,在測試時用spring 這樣的DI架構注入一個本地(記憶體)實作或者Mock實作。
【推薦】編寫單元測試代碼遵守BCDE原則,以保證被測試子產品的傳遞品質。
B:Border,邊界值測試,包括循環邊界、特殊取值、特殊時間點、資料順序等。
C:Correct,正确的輸入,并得到預期的結果。
D:Design,與設計文檔相結合,來編寫單元測試。
E:Error,強制錯誤資訊輸入(如:非法資料、異常流程、業務允許外等),并得到預期的結果。
10.2. 為什麼要使用Mock?
根據網絡相關資料,總結觀點如下:
Mock可以用來解除外部服務依賴,進而保證了測試用例的獨立性。
現在的網際網路軟體系統,通常采用了分布式部署的微服務,為了單元測試某一服務而準備其它服務,存在極大的依耐性和不可行性。
Mock可以減少全鍊路測試資料準備,進而提高了編寫測試用例的速度。
傳統的內建測試,需要準備全鍊路的測試資料,可能某些環節并不是你所熟悉的。最後,耗費了大量的時間和經曆,并不一定得到你想要的結果。現在的單元測試,隻需要模拟上遊的輸入資料,并驗證給下遊的輸出資料,編寫測試用例并進行測試的速度可以提高很多倍。
Mock可以模拟一些非正常的流程,進而保證了測試用例的代碼覆寫率。
根據單元測試的BCDE原則,需要進行邊界值測試(Border)和強制錯誤資訊輸入(Error),這樣有助于覆寫整個代碼邏輯。在實際系統中,很難去構造這些邊界值,也能難去觸發這些錯誤資訊。而Mock從根本上解決了這個問題:想要什麼樣的邊界值,隻需要進行Mock;想要什麼樣的錯誤資訊,也隻需要進行Mock。
Mock可以不用附加元件目環境配置,進而保證了測試用例的執行速度。
在進行內建測試時,我們需要附加元件目的所有環境配置,啟動項目依賴的所有服務接口。往往執行一個測試用例,需要幾分鐘乃至幾十分鐘。采用Mock實作的測試用例,不用附加元件目環境配置,也不依賴其它服務接口,執行速度往往在幾秒之内,大大地提高了單元測試的執行速度。
10.3. 單元測試與內建測試的差別
在實際工作中,不少同學用內建測試代替了單元測試,或者認為內建測試就是單元測試。這裡,總結為了單元測試與內建測試的差別:
測試對象不同
單元測試對象是實作了具體功能的程式單元,內建測試對象是概要設計規劃中的子產品及子產品間的組合。
測試方法不同
單元測試中的主要方法是基于代碼的白盒測試,內建測試中主要使用基于功能的黑盒測試。
測試時間不同
內建測試要晚于單元測試。
測試内容不同
單元測試主要是子產品内程式的邏輯、功能、參數傳遞、變量引用、出錯處理及需求和設計中具體要求方面的測試;而內建測試主要驗證各個接口、接口之間的資料傳遞關系,及子產品組合後能否達到預期效果。