單元測試中使用Mockito模擬物件
單元測試應該小巧玲瓏,輕盈快捷。然而,一個待測的物件可能依賴另一個物件。它可能需要跟資料庫、郵箱伺服器、Web Service、訊息佇列等服務進行互動。但是,這些服務可能在測試過程中不可用。假設這些服務可用,依賴這些服務的單元測試可能相當耗時。要是
- Web Service 不可獲得。
- 資料庫因維護而關閉。
- 訊息佇列笨重且緩慢。
這些違背單元測試小巧玲瓏,輕盈快捷的初衷。單元測試被期待在幾毫秒內執行完成。若單元測試緩慢,你的開發過程受阻,這會影響你開發組的效率。解決之道就是模擬(Mocking),
若你遵循OOP的SOILD原則,且使用Spring的依賴注入,單元測試中的模擬Mock變得輕而易舉。你不必連線資料庫。你只需一個能返回你期待值的物件。若你編寫緊密耦合程式碼,模擬會相當艱難。我目睹過許多因緊密耦合其它物件的遺留程式碼不能單元測試。不可測試程式碼不遵循OOP的SOILD原則,且不能使用依賴注入。
Mockito初體驗
接下來將學習使用Mockito框架。它是一套通過簡單的方法對於指定介面或類生產Mock物件的類庫。使用Mockito,在準備階段只需少量時間,可以使用簡潔的API編寫漂亮的測試,可以對具體類建立Mock物件,並且有監視非Mock物件的功能。
這有兩個術語需要了解一下。
-
Stub物件作用是在測試時提供所需的測試資料,可以對各種互動設定相應的迴應。Mockito中
when(...).thenReturn(...)這樣的語法便是設定方法呼叫的返回值。另外也可以設定方法在何時呼叫會拋異常等。 -
Mock物件用來驗證測試中所依賴物件間的互動是否能夠達到預期。Mockito中用
verify(...).methodXxx(...)語法驗證methodXxx()方法是否按照預期進行呼叫。
需要加入到pom.xml的依賴如下:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
<version>2.16.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
建立Mock物件
可以通過兩種方法來Mock物件
- 通過
mock(Class<T> clazz)方法。 - 通過
@Mock註解需要Mock的物件,然後呼叫MockitoAnnotations.initMocks(this)或@RunWith(MockitoJUnitRunner.class)初始化模擬。
//@RunWith(MockitoJUnitRunner.class)
public class MockitoSampleTest {
// 模擬介面
UserService mockUserService = mock(UserService.class);
// 模擬實現類
UserServiceImpl mockServiceImpl = mock(UserServiceImpl.class);
// 基於註釋模擬類
@Mock
User mockUser;
@Before
public void initMocks() {
// 初始化當前測試類所有@Mock註釋模擬物件
MockitoAnnotations.initMocks(this);
}
}
值得注意的是,對於final類、匿名類和Java的基本型別是無法進行Mock的。
設定Mock物件的期望值行為及返回值
有兩種通用基礎設定寫法:
when(...).thenReturn(...);doReturn(...).when(...).someMethod();
但是,doReturn(...).when(mockObj.someMethod());會拋異常。
@Test
public void testMockClass() {
// 對方法設定返回值,也就是設定資料樁
when(mockServiceImpl.findUserByUserName("tom")).thenReturn(new User("tom", "1234"));
doReturn(true).when(mockServiceImpl).hasMatchUser("tom", "1234");
User user = mockServiceImpl.findUserByUserName("tom");
boolean isMatch = mockServiceImpl.hasMatchUser("tom", "1234");
assertNotNull(user);
assertEquals(user.getUserName(), "tom");
assertEquals(isMatch, true);
}
也值得注意的是,static和final修飾的方法無法進行設定的。
驗證互動行為
Mock物件一旦建立便會自動記錄自己的互動行為,所以,可以有選擇地對其互動行為進行驗證。
@Test
// 模擬介面UserService測試
public void testMockInterface() {
// 對方法設定返回值,也就是設定資料樁
when(mockUserService.findUserByUserName("tom")).thenReturn(new User("tom", "1234"));
doReturn(true).when(mockUserService).hasMatchUser("tom", "1234");
// 對void方法進行方法預期設定
User u = new User("John", "1234");
doNothing().when(mockUserService).registerUser(u);
// 執行方法呼叫
User user = mockUserService.findUserByUserName("tom");
boolean isMatch = mockUserService.hasMatchUser("tom", "1234");
mockUserService.registerUser(u);
assertNotNull(user);
assertEquals(user.getUserName(), "tom");
assertEquals(isMatch, true);
// 驗證互動行為
verify(mockUserService).findUserByUserName("tom");
// 驗證方法只調用一次
verify(mockUserService, times(1)).findUserByUserName("tom");
// 驗證方法至少呼叫一次
verify(mockUserService, atLeastOnce()).findUserByUserName("tom");
verify(mockUserService, atLeast(1)).findUserByUserName("tom");
// 驗證方法至多呼叫一次
verify(mockUserService, atMost(1)).findUserByUserName("tom");
verify(mockUserService).hasMatchUser("tom", "1234");
verify(mockUserService).registerUser(u);
}
對Service層進行單元測試
同常主要Java Web應用分Controller,Service,DAO基本三層來進行開發。
接下來通過使用Mockito框架對Service進單元測試。
Domain領域物件:
public class Product {
}
DAO資料連線層:
public interface ProductDao {
int getAvailableProducts(Product product);
int orderProduct(Product product, int orderedQuantity);
}
Service業務邏輯層:
public class ProductService {
private ProductDao productDao;
public boolean buy(Product product, int orderedQuantity) {
int availableQuantity = productDao.getAvailableProducts(product);
if (orderedQuantity > availableQuantity) {
return false;
}
productDao.orderProduct(product, orderedQuantity);
return true;
}
}
Service測試用例:
public class ProductServiceTest {
private ProductDao productDao;
@Before
public void setupMock() {
//模擬Dao層
productDao = mock(ProductDao.class);
}
@Test
public void testBuy() {
int availableQuantity = 30;
Product product = new Product();
ProductService productService = new ProductService();
//設定資料樁
when(productDao.getAvailableProducts(product)).thenReturn(availableQuantity);
//doReturn(availableQuantity).when(productDao).getAvailableProducts(product);
//這寫法不行
//doReturn(availableQuantity).when(productDao.getAvailableProducts(product));
//通過Spring測試框架提供的工具類為目標物件私有屬性設值,這樣就不用ProductDao另建setProductDao()方法
ReflectionTestUtils.setField(productService, "productDao", productDao);
Assert.assertFalse(productService.buy(product, 31));
Assert.assertTrue(productService.buy(product, 3));
//驗證互動行為
verify(productDao).orderProduct(product, 3);
verify(productDao, times(2)).getAvailableProducts(product);
}
}
測試用例中,用到Spring test框架的ReflectionTestUtils,它可以為目標物件非公有屬性設值,或呼叫非公有setter方法,方便測試過程中使用。
為了使用ReflectionTestUtils,需要向pom.xml新增下面的依賴
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>3.0.5.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>3.0.5.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
<version>1.2</version>
<scope>test</scope>
</dependency>
總結
本文介紹了Mockito的基本用法,以及通過它Mock物件對Service層輔助測試用例。在Mockito輔助下,單元測試變得如虎添翼啊!
在編寫程式碼過程中,必須反覆除錯它,保證他順利通過。雖程式碼通過編譯,只是說明其語法正確,但不能保證其語義也正確。沒有任何人可以輕易承諾這段程式碼的行為一定是正確的。幸運的是,單元測試會為我們的承諾作出保證。編寫單元測試就是用來驗證這段程式碼的行為是否與我們期望的一樣。有了單元測試,我們可以自信地交付自己的程式碼,減少後顧之憂。
