轉載自:http://blog.csdn.net/chjttony/article/details/14522771
Java單元測試對於開發人員質量保證至關重要,尤其當面對一團亂碼的遺留代碼時,沒有高覆蓋率的單元測試做保障,沒人敢輕易對代碼進行重構。然而單元測試的編寫也不是一件容易的事情,除非使用TDD方式,否則編寫出容易測試的代碼不但對開發人員的設計編碼要求很高,而且代碼中的各種依賴也常常爲單元測試帶來無窮無盡的障礙。
令人欣慰的是開源社區各種優秀的Mock框架讓單元測試不再複雜,本文簡單介紹EasyMock,PowerMock等的基本常用用法。
Mock說白了就是打樁(Stub)或則模擬,當你調用一個不好在測試中創建的對象時,Mock框架爲你模擬一個和真實對象類似的替身來完成相應的行爲。
EasyMock:
使用如下方式在Maven中添加EasyMock的依賴:
-
<dependency>
-
<groupId>org.easymock</groupId>
-
<artifactId>easymock</artifactId>
-
<version>3.2</version>
-
<scope>test</scope>
-
</dependency>
EasyMock使用動態代理實現模擬對象創建,其基本步驟爲以下四步:
以數據庫應用爲例的被測試代碼如下:
-
public class UserServiceImpl{
-
private UserDao dao;
-
public User query(String id) throws Exception{
-
try{
-
return dao.getById(id);
-
}catch(Exception e){
-
throw e;
-
}
-
return null;
-
}
-
}
-
-
public class UserDao{
-
public User getById(String id) throws Exception{
-
try{
-
return ……;
-
}catch(Exception e){
-
throw e;
-
}
-
return null;
-
}
-
}
現在希望對UserServiceImpl進行測試,而UserDao開發組只給出接口,尚未完成功能實現。
使用Mock對UserDao進行模擬來測試UserServiceImpl。
(1).基本的測試代碼如下:
-
public class UserServiceImplTest {
-
@Test
-
public void testQuery() {
-
User expectedUser = new User();
-
user.setId(“1001”);
-
UserDao mock = EasyMock.createMock(UserDao.class);
-
Easymock.expect(mock.getById("1001")).andReturn(expectedUser);
-
Easymock.replay(mock);
-
-
UserServiceImpl service = new UserServiceImpl();
-
service.setUserDao(mock);
-
user user = service.query("1001");
-
assertEquals(expectedUser, user);
-
Easymock.verify(mock);
-
}
-
}
注意:
在EasyMock3.0之前,org.easymock.EasyMock使用JDK的動態代理實現Mock對象創建,因此只能針對接口進行Mock,org.easymock.classextension.EasyMock使用CGLIB動態代理創建Mock對象,可以針對普通類進行Mock。
在EasyMock3.0之後,org.easymock.classextension.EasyMock被廢棄,使用org.easymock.EasyMock可以針對接口和普通類進行Mock對象創建。
(2).調用測試設定:
如果想測試UserServiceImpl調用了UserDao的getById方法3次,則使用如下代碼即可:
-
Easymock.expect(mock.getById("1001")).andReturn(exceptUser).times(3);
(3).方法異常:
如果想測試UserServiceImpl在調用UserDao的getById方法時發生異常,可以使用如下代碼:
-
Easymock.expect(mock.getById("1001")).andThrow(new RuntimeException());
在測試UserServiceImpl時就可以使用try-catch捕獲Mock的異常。
(4).基本參數匹配:
上面的方法在Mock UserDao的getById方法時傳入了“0001”的預期值,這種方式是精確參數匹配,如果UserServiceImpl在調用是傳入的參數不是“0001”就會發生Unexpect method的Mock異常,可以使用下面的方法在Mock時進行參數匹配:
-
Easymock.expect(mock.getById(Easymock.isA(String.class))).andReturn(exceptedUser).times(3);
isA()方法會使用instanceof進行參數類型匹配,類似的方法還有anyInt(),anyObject(), isNull(),same(), startsWith()......
(5).數組類型參數匹配:
如果UserServiceImpl在調用UserDao的方法時傳入的參數是數組,代碼如下:
-
public class UserServiceImpl{
-
private UserDao dao;
-
public List<String> queryNames(String[] ids) throws Exception{
-
try{
-
return dao.getNames(ids);
-
}catch(Exception e){
-
throw e;
-
}
-
return null;
-
}
-
}
-
-
public class UserDao{
-
public List<String> getNames(String[] ids) throws Exception{
-
try{
-
return ……;
-
}catch(Exception e){
-
throw e;
-
}
-
return null;
-
}
-
}
此時有兩種辦法來進行參數匹配:
a.數組必須和測試給定的一致:
-
Easymock.expect(mock.getNames(EasyMock.aryEq(testIds))).andReturn(exceptedNames);
b.不考慮測試數組內容:
-
Easymock.expect(mock.getNames(EasyMock.isA(String[].class))).andReturn(exceptedNames);
(6).void方法Mock:
如果要Mock的方法是無返回值類型,例子如下:
-
public class UserDao {
-
public void updateUserById(String id) throws Exception{
-
try{
-
update…
-
}catch(Exception e){
-
throw e;
-
}
-
}
-
}
a.正常Mock代碼如下:
-
mock.updateUserById(“TestId”);
-
EasyMock.expectLastCall().anytimes();
b.模擬發生異常的Mock代碼如下:
-
mock.updateUserById(“TestId”);
-
EasyMock.expectLastCall().andThrow(new RuntimeException()).anytimes();
(7).多次調用返回不同值的Mock:
對於迭代器類型的遍歷代碼來說,需要在不同調用時間返回不同的結果,以JDBC結果集爲例代碼如下:
-
public List<String> getUserNames () throws Exception{
-
List<String> usernames = new ArrayList<String>();
-
ResultSet rs = pstmt.executeQuery(query);
-
try {
-
while(rs.next()){
-
usernames.add(rs.getString(2));
-
}
-
} catch (SQLException e) {
-
throw e;
-
}
-
}
在Mock結果集的next方法時如果總返回true,則代碼就會陷入死循環,如果總返回false則代碼邏輯根本無法執行到循環體內。
正常的測試邏輯應該是先返回幾次true執行循環體,然後在返回false退出循環,使用Mock可以方便模擬這種預期的行爲,代碼如下:
-
EasyMock.expect(rs.next()).andReturn(true).times(2).andReturn(false).times(1);
更多的關於EasyMock的用法,請參考EasyMock官方文檔:
http://easymock.org/EasyMock3_0_Documentation.html。
PowerMock:
上面介紹的EasyMock可以滿足單元測試中的大部分需求,但是由於動態代理是使用了面向對象的繼承和多態特性,JDK自身的動態代理只針對接口進行代理,其本質是爲接口生成一個實現類,而CGLIB可以針對類進行代理,其本質是將類自身作爲基類。
如果遇到了靜態、final類型的類和方法,以及私有方法,EasyMock的動態代理侷限性使得無法測試這些特性情況。
PowerMock是在EasyMock基礎上進行擴展(只是補充,不是替代),使用了字節碼操作技術直接對生成的字節碼類文件進行修改,從而可以方便對靜態,final類型的類和方法進行Mock,還可以對私有方法進行Mock,更可以對類進行部分Mock。
PowerMock的工作過程和EasyMock類似,不同之處在於需要在類層次聲明@RunWith(PowerMockRunner.class)註解,以確保使用PowerMock框架引擎執行單元測試。
通過如下方式在maven添加PowerMock相關依賴:
-
<dependency>
-
<groupId>org.powermock</groupId>
-
<artifactId>powermock-api-easymock</artifactId>
-
<version>1.5.1</version>
-
<scope>test</scope>
-
</dependency>
-
<dependency>
-
<groupId>org.powermock</groupId>
-
<artifactId>powermock-module-junit4</artifactId>
-
<version>1.5.1</version>
-
<scope>test</scope>
-
</dependency>
例子如下:
(1).Miock final類的靜態方法:
如果測試代碼中使用到了java.lang.System類,代碼如下:
-
public class SystemPropertyMockDemo {
-
public String getSystemProperty() throws IOException {
-
return System.getProperty("property");
-
}
-
}
如果對System.getProperty()方法進行Mock,代碼如下:
-
@RunWith(PowerMockRunner.class)
-
@PrepareForTest({SystemPropertyMockDemo.class})
-
public class SystemPropertyMockDemoTest {
-
@Test
-
public void demoOfFinalSystemClassMocking() throws Exception {
-
PowerMock.mockStatic(System.class);
-
EasyMock.expect(System.getProperty("property")).andReturn("my property");
-
PowerMock.replayAll();
-
Assert.assertEquals("my property",
-
new SystemPropertyMockDemo().getSystemProperty());
-
PowerMock.verifyAll();
-
}
-
}
非final類的靜態方法代碼相同,注意(上述代碼只能在EasyMock3.0之後版本正常運行)
如果要在EasyMock3.0之前版本正常Mock final類的靜態方法,需要使用PowerMockito,
通過如下方式在maven中添加PowerMockito相關依賴:
-
<dependency>
-
<groupId>org.powermock</groupId>
-
<artifactId>powermock-api-mockito</artifactId>
-
<version>1.5.1</version>
-
<scope>test</scope>
-
</dependency>
代碼如下:
-
@RunWith(PowerMockRunner.class)
-
@PrepareForTest({SystemPropertyMockDemo.class})
-
public class SystemPropertyMockDemoTest {
-
@Test
-
public void demoOfFinalSystemClassMocking() throws Exception {
-
PowerMockito.mockStatic(System.class);
-
PowerMockito.when(System.getProperty("property")).thenReturn("my property");
-
PowerMock.replayAll();
-
Assert.assertEquals("my property",
-
new SystemPropertyMockDemo().getSystemProperty());
-
PowerMock.verifyAll();
-
}
-
}
注意:
對於JDK的類如果要進行靜態或final方法Mock時,@PrepareForTest()註解中只能放被測試的類,而非JDK的類,如上面例子中的SystemPropertyMockDemo.class。
對於非JDK的類如果需要進行靜態活final方法Mock時, @PrepareForTest()註解中直接放方法所在的類,若上面例子中的System不是JDK的類,則可以直接放System.class。
@PrepareForTest({......}) 註解既可以加在類層次上(對整個測試文件有效),也可以加在測試方法上(只對測試方法有效)。
(2).Mock非靜態的final方法:
被測試代碼如下:
-
public class ClassDependency {
-
public final boolean isAlive() {
-
return false;
-
}
-
}
-
-
public class ClassUnderTest{
-
public boolean callFinalMethod(ClassDependency refer) {
-
return refer.isAlive();
-
}
-
}
使用PowerMock的測試代碼如下:
-
@RunWith(PowerMockRunner.class)
-
public class FinalMethodMockDemoTest {
-
@Test
-
@PrepareForTest(ClassDependency.class)
-
public void testCallFinalMethod() {
-
ClassDependency depencency = PowerMock.createMock(ClassDependency.class);
-
ClassUnderTest underTest = new ClassUnderTest();
-
EasyMock.expect(depencency.isAlive()).andReturn(true);
-
PowerMock.replayAll();
-
Assert.assertTrue(underTest.callFinalMethod(depencency));
-
PowerMock.verifyAll();
-
}
-
}
(3)部分Mock和私有方法Mock:
如果被測試類某個方法不太容易調用,可以考慮只對該方法進行Mock,而其他方法全部使用被測試對象的真實方法,可以考慮使用PowerMock的部分Mock,被測試代碼如下:
-
public class DataService {
-
public boolean replaceData(final String dataId, final byte[] binaryData) {
-
return modifyData(dataId, binaryData);
-
}
-
public boolean deleteData(final String dataId) {
-
return modifyData(dataId, null);
-
}
-
-
private boolean modifyData(final String dataId, final byte[] binaryData) {
-
return true;
-
}
-
}
只對modifyData方法進行Mock,而其他方法調用真實方法,測試代碼如下:
-
@RunWith(PowerMockRunner.class)
-
@PrepareForTest(DataService.class)
-
public class DataServiceTest {
-
@Test
-
public void testReplaceData() throws Exception {
-
DataService tested = PowerMock.createPartialMock(DataService.class, “modifyData”);
-
PowerMock.expectPrivate(tested, “modifyData”, “id”, null).andReturn(true);
-
PowerMock.replay(tested);
-
assertTrue(tested.deleteData(“id”));
-
PowerMock.verify(tested);
-
}
-
}
部分Mock在被測試方法的依賴在同一個類,且不容易創建時比較有用。
個人認爲私有方法的Mock意義不是很大,完全可以使用反射機制直接調用。
(4).調用對象的構造方法Mock對象:
在被測試方法內部調用構造創建了一個對象很常見,被測試代碼如下:
-
public class PersistenceManager {
-
public boolean createDirectoryStructure(String directoryPath) {
-
File directory = new File(directoryPath);
-
if (directory.exists()) {
-
throw new IllegalArgumentException("\"" + directoryPath + "\" already exists.");
-
}
-
return directory.mkdirs();
-
}
-
}
創建文件操作(new File(path))依賴與操作系統底層實現,如果給定的路徑不合法,將會出現異常導致測試無法正常覆蓋,此時需要使用PowerMock的提供的調用構造方法創建Mock對象,測試代碼如下:
-
@RunWith(PowerMockRunner.class)
-
@PrepareForTest( PersistenceManager.class )
-
public class PersistenceManagerTest {
-
@Test
-
public void testCreateDirectoryStructure_ok() throws Exception {
-
File fileMock = PowerMock.createMock(File.class);
-
PersistenceManager tested = new PersistenceManager();
-
PowerMock.expectNew(File.class, "directoryPath").andReturn(fileMock);
-
EasyMock.expect(fileMock.exists()).andReturn(false);
-
EasyMock.expect(fileMock.mkdirs()).andReturn(true);
-
PowerMock.replay(fileMock, File.class);
-
assertTrue(tested.createDirectoryStructure("directoryPath"));
-
PowerMock.verify(fileMock, File.class);
-
}
-
}
也可以使用更簡便的方法:
FilefileMock = PowerMock.createMockAndExpectNew(File.class,“directoryPath”);
通過EasyMock+PowerMock,開發中絕大部分的方法都可以被測試完全覆蓋。
更多關於PowerMock的用法和參考文檔請參考PowerMock官方網址:
https://code.google.com/p/powermock/。