很多情況下,代碼需要與外部依賴打交道,如一個REST地址,數據庫鏈接、外部IO等;這些依賴有些速度過慢、有些不夠穩定,不符合單元測試要求的快速、可重復等原則性要求,因此引入了Mock對象這一概念。與Mock相關的還有Stub這個單詞。
- stub 樁,它針對指定的輸入緩存了行為
- mock 模擬對象,增加了對輸入條件校驗、注入等功能,簡單來說,它保證在收到預期參數時表現出預定義的行為,常用的有兩個框架
- mockito 較為易用
- powermock 功能更加強大,能夠對靜態方法和私有函數進行Mock
一般來說,在編寫stub之后,需要將其注入依賴對象中,也即依賴注入(DI),框架上有Spring DI和Google Guice等。
修改代碼結構使其更具可測性
為了使得測試更加容易,有時需要修改代碼,如將依賴以成員變量的形式傳入被測類中,如:
public class AddressRetriever{ private Http http; //將外部依賴以構造函數的方式引入,對單元測試更加友好 public AddressRetriever(Http http){ this.http = http; } public Address retrieve(double latitude, double longitude) throws IOException, ParseException{ String params = String.format("lat=%.6flon=%.6f", latitude, longitude); String response = http.get("http://open.mapquestapi.com/nominatim/v1/reverse?format=json&"+params); JSONObject obj = (JSONObject)new JSONParse().parse(reponse); //... } }
但不僅限於構造函數,還可以通過set方法或其他依賴注入框架實現。
為Stub增加一點智能
如這個樁:
Http http = new Http(){ @Override public String get(String uri) throws IOException{ return "{\"address\":{" + "\"house_number\":\"324\"," + "\"road\":\"North Tejon Street\"," } }
這個樁接受任何uri即可返回對應的結果,沒有對輸入進行判斷,我們期望的是:在收到預期參數時提供預期的輸入,可以通過在get()方法中加入判斷實現,這樣的通用功能引入Mock工具。
使用Mock工具簡化測試
public class AddressRetrieverTest { @Test public void answersAppropriateAddressForValidCoordinates() throws IOException, ParseException { Http http = mock(Http.class); when(http.get(contains("lat=38.000000&lon=-104.000000"))).thenReturn( "{\"address\":{" + "\"house_number\":\"324\"," // ... + "}"); AddressRetriever retriever = new AddressRetriever(http); Address address = retriever.retrieve(38.0,-104.0); assertThat(address.houseNumber, equalTo("324")); }
when().thenReturn()模式就是Mockito設置的常用方式。
介紹一種DI工具
DI工具有很多,如Spring DI和Google Guice,但是moctito內建的DI工具也能滿足絕大部分的需要,步驟如下:
- 使用@Mock注解創建一個模擬對象
- 使用@InjectMocks注解聲明一個目標對象
- 在目標對象初始化完畢后,調用MockitoAnnotations.initMocks(this)方法完成注入
下面是示例代碼:
public class AddressRetrieverTest{ @Mock private Http http; @InjectMocks private AddressRetriever retriever; @Before public void createRetriever(){ retriever = new AddressRetriever(); MockitoAnnotations.initMocks(this); } @Test public void answersAppropriateAddressForValidCoordinates() throws IOException, ParseException{ when(http.get(contains("lat=38.000000&lon=-104.000000"))) .thenReturn("{\"address\":{" + "\"house_number\":\"324\"," //... } }
最后需要注意的是,如果使用了Mock,那不是直接測試生產代碼,而是在於生產代碼中加了鴻溝,單元測試的正確性依賴於被Mock對象的正確性,因此單元測試需要配合端到端的集成測試。