單元測試應該小巧玲瓏,輕盈快捷。然而,一個待測的對象可能依賴另一個對象。它可能需要跟數據庫、郵箱服務器、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);// 基于注釋模擬類@MockUser mockUser;@Beforepublic 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;@Beforepublic void setupMock() {//模擬Dao層productDao = mock(ProductDao.class);}@Testpublic 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輔助下,單元測試變得如虎添翼啊!
在編寫代碼過程中,必須反復調試它,保證他順利通過。雖代碼通過編譯,只是說明其語法正確,但不能保證其語義也正確。沒有任何人可以輕易承諾這段代碼的行為一定是正確的。幸運的是,單元測試會為我們的承諾作出保證。編寫單元測試就是用來驗證這段代碼的行為是否與我們期望的一樣。有了單元測試,我們可以自信地交付自己的代碼,減少后顧之憂。
引用
-
Mocking in Unit Tests with Mockito
-
Mockito (Mockito 2.16.0 API)
-
《Spring 3.x企業應用開發實戰》陳雄華、林開雄 著
-
Spring Framework Reference Documentation 11. Testing