UT單元測試總結實踐篇

在實際進行單元測試的過程中,我們會發現被測代碼通常會調用一些外部依賴或者尚未實現的方法,導致編寫單元測試代碼相當困難。針對這種情況,我們就需要對這些依賴的對象進行僞造注入,使得被測代碼能夠順利運行,並能夠對運行結果進行驗證。

Java開發中常用的Mock框架包括PowerMock, JMockit, Mockito, EasyMock, JMock等等,其中PowerMock是在Mockito和EasyMock的基礎上封裝實現的,API簡單易用,功能相比其他框架也更強大。這裏主要介紹如何使用PowerMock和Mockito框架來應對單元測試中常見的Mock場景。

基礎篇中有提到,在一個單元測試用例中通常包括三個階段,首先構造測試條件,對存在的依賴項創建Stub測試樁,設置好when.then,或者創建Mock模擬對象,用於最後的結果驗證,然後第二步對被測代碼執行真正的調用,最後一步在用例中添加斷言,驗證代碼運行結果是否與期望的一致。

1. 依賴包

我們首先將測試需要的依賴包引入。在使用Maven進行構建的項目中引入的依賴包如下:

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-module-junit4</artifactId>
    <version>1.6.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.powermock</groupId>
    <artifactId>powermock-api-mockito</artifactId>
    <version>1.6.3</version>
    <scope>test</scope>
</dependency>

2. Mock階段

PowerMock作爲非受限的Mock框架,針對各種場景都提供了方法。

2.1 普通Mock

被測代碼如下:

public class Book {
    public int getResult() {
        throw new UnsupportedOperationException();
    }
}

測試用例如下:

public class BookTest {
    @Test
    public void test() {
        Book stub = PowerMockito.mock(Book.class);
        PowerMockito.when(stub.getResult()).thenReturn(5);
        ......
    }
}

當Mock的方法中沒有返回值或者需要拋出異常時,方法如下:

PowerMockito.doNothing().when(stub).getResult();
PowerMockito.doThrow(new Exception()).when(stub).thenReturn();

2.2 模擬靜態方法或者final類/方法

被測代碼如下:

public final class Book {
    public static int getResult() {
        throw new UnsupportedOperationException();
    }
}

測試用例如下:

@RunWith(PowerMockRunner.class)
public class BookTest {
    @Test
    @PrepareForTest(Book.class)
    public void test() {
        PowerMockito.mockStatic(Book.class);
        PowerMockito.when(Book.getResult()).thenReturn(5);
        ......
    }
}

不是普通Mock的情況都需要同時添加RunWith和PrepareForTest註解,這裏的PrepareForTest註解也可以移到類的頭上,當需要Mock多個靜態類時,PrepareForTest的參數需要用大括號括起來。

2.3 模擬私有方法

被測代碼如下:

public class Book {
    private int getResult() {
        throw new UnsupportedOperationException();
    }
}

測試用例如下:

@RunWith(PowerMockRunner.class)
public class BookTest {
    @Test
    @PrepareForTest(Book.class)
    public void test() throws Exception {
        Book stub = PowerMockito.mock(Book.class);
        PowerMockito.when(stub, "getResult").thenReturn(5);
        ......
    }
}

2.4 模擬構造方法

被測代碼如下:

public class Book {
    public Book() {
        throw new UnsupportedOperationException();
    }
}

測試用例如下:

@RunWith(PowerMockRunner.class)
public class BookTest {
    @Test
    @PrepareForTest(Book.class)
    public void test() throws Exception {
        Book stub = PowerMockito.mock(Book.class);
        PowerMockito.whenNew(Book.class).withNoArguments().thenReturn(stub);
        ......
    }
}

如果這裏的構造函數需要傳入參數,那麼whenNew之後可以調用withArguments()方法。

2.5 使用spy進行部分模擬

當一個類只有少量的方法未實現,或者我們希望只對少量的幾個方法進行Mock,其他方法依然調用原來的邏輯時,我們就可以使用spy來進行部分模擬。

被測代碼如下:

public class Book {
    public int getPrice() {
        throw new UnsupportedOperationException();
    }

    public int getResult() {
        return this.getPrice() + 2;
    }
}

測試用例如下:

@RunWith(PowerMockRunner.class)
public class BookTest {
    @Test
    @PrepareForTest(Book.class)
    public void test() throws Exception {
        Book spy = PowerMockito.spy(new Book());
        PowerMockito.doReturn(5).when(spy).getPrice();

        int result = spy.getResult();
        Assert.assertEquals(7, result);
    }
}

這裏有兩個注意點,一是如果這裏使用PowerMockito.when(spy.getPrice()).thenReturn(5); 進行模擬,getPrice()方法依然會被調用;二是在調用被測代碼時需要使用spy對象進行,如果使用原來的實例,則這裏的Mock操作就不能生效。

2.6 訪問對象的屬性

我們可以通過Whitebox的setInternalState和getInternalState設置和訪問對象的私有屬性。例如假設Book對象有一個price私有屬性,我們可以通過Whitebox.getInternalState(book, "num") 訪問該屬性。

2.7 禁用非預期行爲

當我們的被測方法中調用了某些尚未實現或者不希望被調用的方法時,我們在測試時可以暫時將其禁用。

被測代碼如下:

public class Book {
    public void doOtherThings() {
        throw new UnsupportedOperationException();
    }

    public int getResult() {
        this.doOtherThings();
        return 2;
    }
}

測試用例如下:

@RunWith(PowerMockRunner.class)
public class BookTest {
    @Test
    @PrepareForTest(Book.class)
    public void test() throws Exception {
        Book book = new Book();
        PowerMockito.suppress(PowerMockito.method(Book.class, "doOtherThings"));
        Assert.assertEquals(2, book.getResult());
    }
}

2.8 參數匹配

被測代碼如下:

public class Book {
    public int getResult(int count) {
        throw new UnsupportedOperationException();
    }
}

測試用例如下:

@RunWith(PowerMockRunner.class)
public class BookTest {
    @Test
    @PrepareForTest(Book.class)
    public void test() throws Exception {
        Book stub = PowerMockito.mock(Book.class);
        PowerMockito.when(stub.getResult(Mockito.anyInt())).thenReturn(5);
    }
}

我們直接使用Mockito的api接口,這裏還支持Mockito的如下參數匹配規則:eq, matches, any(anyBoolean, anyByte, anyShort, anyChar, anyInt, anyLong, anyFloat, anyDouble, anyList, anyCollection, anyMap, anySet等), isNull, isNotNull, endsWith, isA等。

另外,當調用的方法中包含多個參數時,如果使用了參數匹配器,那麼每個參數都需要使用,否則都不使用。

2.9 doAnswer支持複雜邏輯

當對方法進行Mock時,如果響應中希望包含比較複雜的邏輯,例如我們需要根據方法參數計算返回值,這種情況我們可以使用doAnswer來實現。

被測代碼如下:

public class Book {
    public int getResult(int count) {
        throw new UnsupportedOperationException();
    }
}

測試用例如下:

@RunWith(PowerMockRunner.class)
public class BookTest {
    @Test
    @PrepareForTest(Book.class)
    public void test() throws Exception {
        Book stub = PowerMockito.mock(Book.class);
        PowerMockito.when(stub.getResult(Mockito.anyInt())).thenAnswer(invocation -> {
            int count = (int) invocation.getArguments()[0];
            return 3 * count;
        });
        ......
    }
}

2.10 使用註解進行模擬

當被測對象包含多個需要被Mock的屬性時,我們可以通過註解的方式非常簡單地 進行Mock注入。

被測代碼如下:

public class Book {
    @Autowired
    private ComputeDao computeDao;
    
    public int getResult(int count) {
        throw new UnsupportedOperationException();
    }
}

測試代碼如下:

@RunWith(PowerMockRunner.class)
public class BookTest {
    
    @InjectMocks
    private Book book;
    
    @Mock
    private ComputeDao computeDao;
    
    @Test
    public void test() throws Exception {
        ......
    }
}

這裏會創建一個Book 實例,這個實例是真實對象,以及一個Mock的ComputeDao對象,然後將這個Mock的computeDao自動注入到了book實例中。

2.11 按照調用順序進行模擬

當一個方法被調用多次,我們需要爲每次調用設置不同的返回值時,我們可以使用如下方法。

@RunWith(PowerMockRunner.class)
public class BookTest {

    @Test
    @PrepareForTest(Book.class)
    public void test() throws Exception {
        Book stub = PowerMockito.mock(Book.class);
        PowerMockito.when(stub.getResult(Mockito.anyInt())).thenReturn(3).thenReturn(5);

        Assert.assertEquals(3, stub.getResult(0));
        Assert.assertEquals(5, stub.getResult(0));
    }
}

這裏設置了兩個返回值,前兩次調用會分別返回3和5,之後再調用時則始終按照最後一個返回值進行返回。

3. Call階段

在調用被測方法時,通常我們是直接通過obj.invokeMethod(…)的方式調用即可,但是當我們的被測方法是私有方法時,通過前面的方式就調用不了了,這時我們可以使用PowerMock的Whitebox實現私有方法調用,如下:

Whitebox.invokeMethod(instance, "privateMethod", arg0, ...);
Book book = Whitebox.invokeConstructor(Book.class, arg0, ...);

當類的構造函數也爲私有的時候,我們也可以通過這種方法創建對象。

4. Verify階段

在每個測試用例的最後階段,我們需要驗證程序的運行結果是否與我們期望的一致。這裏通常使用Mockito提供的方法進行驗證,針對靜態方法的驗證,PowerMock也提供了相應的驗證方法。

4.1 數值驗證

對於數值驗證,等待運行結果出來後,直接通過org.junit.Assert.assertxxx這一系列方法即可進行驗證。例如:

Assert.assertEquals(3, book.getPrice());

4.2 方法調用驗證

我們還可以對某個具體方法是否被調用了和未被調用進行驗證,代碼示例如下:

Mockito.verify(mock).getResult(Mockito.anyInt());
Mockito.verify(mock, Mockito.never()).getResult(Mockito.anyInt());

爲了調用方便,我們可以將Mockito的這一系列方法統一引入,import static org.mockito.Mockito.*;

這裏verify方法的第二個參數支持傳入各種驗證條件,例如:

  • time(n): 驗證方法是否調用了n次
  • atLeastOnce(): 至少調用了一次
  • atLeast(n): 至少調用了n次
  • atMost(n): 至多調用了n次
  • never(): 從未被調用過
    如果不傳入參數,則驗證方法是否剛好被調用了一次。

4.3 方法調用順序驗證

需要驗證多個方法的調用順序的時候,使用InOrder。

InOrder inOrder = Mockito.inOrder(mock);
inOrder.verify(mock).invokeMethod1();
inOrder.verify(mock).invokeMethod2();

這裏驗證invokeMethod1和invokeMethod2方法是否按照先後順序進行調用。

4.4 靜態方法調用驗證

對靜態方法調用的驗證如下:

PowerMockito.verifyStatic(times(2));
Book.getDefault();

這裏驗證Book.getDefault()方法是否被調用了2次。

4.5 參數捕捉

有時候我們不光是需要驗證某個方法是否被調用了,還需要對傳入的參數進行驗證。

ArgumentCaptor<Integer> argCaptor = ArgumentCaptor.forClass(Integer.class);
PowerMockito.verifyStatic();
Book.getDefault(argCaptor.capture());
Assert.assertEquals(4, (int) argCaptor.getValue());
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章