使用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使用動態代理實現模擬物件建立,其基本步驟為以下四步:以資料庫應用為例的被測試程式碼如下:
現在希望對UserServiceImpl進行測試,而UserDao開發組只給出介面,尚未完成功能實現。使用Mock對UserDao進行模擬來測試UserServiceImpl。(1).基本的測試程式碼如下: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; } }
(2).呼叫測試設定:如果想測試UserServiceImpl呼叫了UserDao的getById方法3次,則使用如下程式碼即可: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物件被呼叫 } }
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/。