1. 程式人生 > >使用PowerMock和Easymock進行單元測試

使用PowerMock和Easymock進行單元測試

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);//建立Mock物件
            Easymock.expect(mock.getById("1001")).andReturn(expectedUser);//錄製Mock物件預期行為
            Easymock.replay(mock);//重放Mock物件,測試時以錄製的物件預期行為代替真實物件的行為

            UserServiceImpl  service = new UserServiceImpl();
            service.setUserDao(mock);
            user user = service.query("1001");//呼叫測試方法
            assertEquals(expectedUser, user); //斷言測試結果 
            Easymock.verify(mock);//驗證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());
(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})//宣告要Mock的類       
public class SystemPropertyMockDemoTest {       
    @Test      
    public void demoOfFinalSystemClassMocking() throws Exception {       
        PowerMock.mockStatic(System.class);//Mock靜態方法       
        EasyMock.expect(System.getProperty("property")).andReturn("my property");//錄製Mock物件的靜態方法       
        PowerMock.replayAll();//重放Mock物件       
        Assert.assertEquals("my property",       
                                  new SystemPropertyMockDemo().getSystemProperty());       
        PowerMock.verifyAll();//驗證Mock物件       
    }       
}   
非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); //建立Mock物件  
        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”);//建立部分mock物件,只對modifyData方法Mock  
        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/