天天看點

Java單元測試技巧之PowerMock

Java單元測試技巧之PowerMock

作者 | 常意

來源 |

阿裡技術公衆号

前言

高德的技術大佬向老師在談論方法論時說到:“複雜的問題要簡單化,簡單的問題要深入化。”

這句話讓我感觸頗深,這何嘗不是一套編寫代碼的方法——把一個複雜邏輯拆分為許多簡單邏輯,然後把每一個簡單邏輯進行深入實作,最後把這些簡單邏輯整合為複雜邏輯,總結為八字真言即是“化繁為簡,由簡入繁”。

編寫Java單元測試用例,其實就是把“複雜的問題要簡單化”——即把一段複雜的代碼拆解成一系列簡單的單元測試用例;寫好Java單元測試用例,其實就是把“簡單的問題要深入化”——即學習一套方法、總結一套模式并應用到實踐中。這裡,作者根據日常的工作經驗,總結了一些Java單元測試技巧,以供大家交流和學習。

1. 準備環境

PowerMock是一個擴充了其它如EasyMock等mock架構的、功能更加強大的架構。PowerMock使用一個自定義類加載器和位元組碼操作來模拟靜态方法、構造方法、final類和方法、私有方法、去除靜态初始化器等等。

1.1. 引入PowerMock包

為了引入PowerMock包,需要在pom.xml檔案中加入下列maven依賴:

Java單元測試技巧之PowerMock

1.2. 內建SpringMVC項目

在SpringMVC項目中,需要在pom.xml檔案中加入JUnit的maven依賴:

Java單元測試技巧之PowerMock

1.3. 內建SpringBoot項目

在SpringBoot項目中,需要在pom.xml檔案中加入JUnit的maven依賴:

Java單元測試技巧之PowerMock

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. 單元測試與內建測試的差別

在實際工作中,不少同學用內建測試代替了單元測試,或者認為內建測試就是單元測試。這裡,總結為了單元測試與內建測試的差別:

測試對象不同

單元測試對象是實作了具體功能的程式單元,內建測試對象是概要設計規劃中的子產品及子產品間的組合。

測試方法不同

單元測試中的主要方法是基于代碼的白盒測試,內建測試中主要使用基于功能的黑盒測試。

測試時間不同

內建測試要晚于單元測試。

測試内容不同

單元測試主要是子產品内程式的邏輯、功能、參數傳遞、變量引用、出錯處理及需求和設計中具體要求方面的測試;而內建測試主要驗證各個接口、接口之間的資料傳遞關系,及子產品組合後能否達到預期效果。