单元测试之Mock使用详解

Java单元测试对于开发人员质量保证至关重要,尤其当面对一团乱码的遗留代码时,没有高覆盖率的单元测试做保障,没人敢轻易对代码进行重构。然而单元测试的编写也不是一件容易的事情,除非使用TDD方式,否则编写出容易测试的代码不但对开发人员的设计编码要求很高,而且代码中的各种依赖也常常为单元测试带来无穷无尽的障碍。

令人欣慰的是开源社区各种优秀的Mock框架让单元测试不再复杂,本文简单介绍EasyMock,PowerMock等的基本常用用法。

Mock说白了就是打桩(Stub)或则模拟,当你调用一个不好在测试中创建的对象时,Mock框架为你模拟一个和真实对象类似的替身来完成相应的行为。

EasyMock:

使用如下方式在Maven中添加EasyMock的依赖:


  
  
  1. <dependency>
  2. <groupId>org.easymock </groupId>
  3. <artifactId>easymock </artifactId>
  4. <version>3.2 </version>
  5. <scope>test </scope>
  6. </dependency>

EasyMock使用动态代理实现模拟对象创建,其基本步骤为以下四步:

以数据库应用为例的被测试代码如下:


  
  
  1. public class UserServiceImpl{
  2. private UserDao dao;
  3. public User query(String id) throws Exception{
  4. try{
  5. return dao.getById(id);
  6. } catch(Exception e){
  7. throw e;
  8. }
  9. return null;
  10. }
  11. }
  12. public class UserDao{
  13. public User getById(String id) throws Exception{
  14. try{
  15. return ……;
  16. } catch(Exception e){
  17. throw e;
  18. }
  19. return null;
  20. }
  21. }
现在希望对UserServiceImpl进行测试,而UserDao开发组只给出接口,尚未完成功能实现。

使用Mock对UserDao进行模拟来测试UserServiceImpl。

(1).基本的测试代码如下:


  
  
  1. public class UserServiceImplTest {
  2. @Test
  3. public void testQuery() {
  4. User expectedUser = new User();
  5. user.setId(“ 1001”);
  6. UserDao mock = EasyMock.createMock(UserDao.class); //创建Mock对象
  7. Easymock.expect(mock.getById( "1001")).andReturn(expectedUser); //录制Mock对象预期行为
  8. Easymock.replay(mock); //重放Mock对象,测试时以录制的对象预期行为代替真实对象的行为
  9. UserServiceImpl service = new UserServiceImpl();
  10. service.setUserDao(mock);
  11. user user = service.query( "1001"); //调用测试方法
  12. assertEquals(expectedUser, user); //断言测试结果
  13. Easymock.verify(mock); //验证Mock对象被调用
  14. }
  15. }

注意:

在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的方法时传入的参数是数组,代码如下:


  
  
  1. public class UserServiceImpl{
  2. private UserDao dao;
  3. public List<String> queryNames(String[] ids) throws Exception{
  4. try{
  5. return dao.getNames(ids);
  6. } catch(Exception e){
  7. throw e;
  8. }
  9. return null;
  10. }
  11. }
  12. public class UserDao{
  13. public List<String> getNames(String[] ids) throws Exception{
  14. try{
  15. return ……;
  16. } catch(Exception e){
  17. throw e;
  18. }
  19. return null;
  20. }
  21. }
此时有两种办法来进行参数匹配:

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的方法是无返回值类型,例子如下:


  
  
  1. public class UserDao {
  2. public void updateUserById(String id) throws Exception{
  3. try{
  4. update…
  5. } catch(Exception e){
  6. throw e;
  7. }
  8. }
  9. }
a.正常Mock代码如下:


  
  
  1. mock.updateUserById(“TestId”);
  2. EasyMock.expectLastCall().anytimes();
b.模拟发生异常的Mock代码如下:


  
  
  1. mock.updateUserById(“TestId”);
  2. EasyMock.expectLastCall().andThrow( new RuntimeException()).anytimes();
(7).多次调用返回不同值的Mock:

对于迭代器类型的遍历代码来说,需要在不同调用时间返回不同的结果,以JDBC结果集为例代码如下:


  
  
  1. public List<String> getUserNames () throws Exception{
  2. List<String> usernames = new ArrayList<String>();
  3. ResultSet rs = pstmt.executeQuery(query);
  4. try {
  5. while(rs.next()){
  6. usernames.add(rs.getString( 2));
  7. }
  8. } catch (SQLException e) {
  9. throw e;
  10. }
  11. }
在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相关依赖:


  
  
  1. <dependency>
  2. <groupId>org.powermock </groupId>
  3. <artifactId>powermock-api-easymock </artifactId>
  4. <version>1.5.1 </version>
  5. <scope>test </scope>
  6. </dependency>
  7. <dependency>
  8. <groupId>org.powermock </groupId>
  9. <artifactId>powermock-module-junit4 </artifactId>
  10. <version>1.5.1 </version>
  11. <scope>test </scope>
  12. </dependency>

例子如下:

(1).Miock final类的静态方法:

如果测试代码中使用到了java.lang.System类,代码如下:


  
  
  1. public  class SystemPropertyMockDemo {     
  2.      public String getSystemProperty() throws IOException {     
  3.          return System.getProperty( "property");     
  4.     }     
  5. }
如果对System.getProperty()方法进行Mock,代码如下:


  
  
  1. @RunWith(PowerMockRunner.class)     
  2. @PrepareForTest({SystemPropertyMockDemo.class}) //声明要Mock的类     
  3. public  class SystemPropertyMockDemoTest {     
  4.      @Test    
  5.      public void demoOfFinalSystemClassMocking() throws Exception {     
  6.         PowerMock.mockStatic(System.class); //Mock静态方法     
  7.         EasyMock.expect(System.getProperty( "property")).andReturn( "my property"); //录制Mock对象的静态方法     
  8.         PowerMock.replayAll(); //重放Mock对象     
  9.         Assert.assertEquals( "my property",     
  10.                                    new SystemPropertyMockDemo().getSystemProperty());     
  11.         PowerMock.verifyAll(); //验证Mock对象     
  12.     }     
非final类的静态方法代码相同,注意(上述代码只能在EasyMock3.0之后版本正常运行)

如果要在EasyMock3.0之前版本正常Mock final类的静态方法,需要使用PowerMockito,

通过如下方式在maven中添加PowerMockito相关依赖:


  
  
  1. <dependency>
  2. <groupId>org.powermock </groupId>
  3. <artifactId>powermock-api-mockito </artifactId>
  4. <version>1.5.1 </version>
  5. <scope>test </scope>
  6. </dependency>

代码如下:


  
  
  1. @RunWith(PowerMockRunner.class)     
  2. @PrepareForTest({SystemPropertyMockDemo.class})     
  3. public  class SystemPropertyMockDemoTest {     
  4.      @Test    
  5.      public void demoOfFinalSystemClassMocking() throws Exception {     
  6.         PowerMockito.mockStatic(System.class);     
  7.         PowerMockito.when(System.getProperty( "property")).thenReturn( "my property");     
  8.         PowerMock.replayAll();     
  9.         Assert.assertEquals( "my property",     
  10.                                    new SystemPropertyMockDemo().getSystemProperty());     
  11.         PowerMock.verifyAll();     
  12.     }     
  13. }

注意:  

对于JDK的类如果要进行静态或final方法Mock时,@PrepareForTest()注解中只能放被测试的类,而非JDK的类,如上面例子中的SystemPropertyMockDemo.class。  

对于非JDK的类如果需要进行静态活final方法Mock时, @PrepareForTest()注解中直接放方法所在的类,若上面例子中的System不是JDK的类,则可以直接放System.class。

@PrepareForTest({......}) 注解既可以加在类层次上(对整个测试文件有效),也可以加在测试方法上(只对测试方法有效)。

(2).Mock非静态的final方法:

被测试代码如下:


  
  
  1. public class ClassDependency {
  2. public final boolean isAlive() {
  3. return false;
  4. }
  5. }
  6. public class ClassUnderTest{
  7. public boolean callFinalMethod(ClassDependency refer) {
  8. return refer.isAlive();
  9. }
  10. }
使用PowerMock的测试代码如下:


  
  
  1. @RunWith(PowerMockRunner.class)        
  2. public  class FinalMethodMockDemoTest {     
  3.      @Test  
  4.      @PrepareForTest(ClassDependency.class)  
  5.      public void testCallFinalMethod() {  
  6.         ClassDependency depencency = PowerMock.createMock(ClassDependency.class);  //创建Mock对象
  7.         ClassUnderTest underTest =  new ClassUnderTest();  
  8.         EasyMock.expect(depencency.isAlive()).andReturn( true);  
  9. PowerMock.replayAll();
  10.         Assert.assertTrue(underTest.callFinalMethod(depencency));  
  11. PowerMock.verifyAll();
  12.     }
  13. }
(3)部分Mock和私有方法Mock:

如果被测试类某个方法不太容易调用,可以考虑只对该方法进行Mock,而其他方法全部使用被测试对象的真实方法,可以考虑使用PowerMock的部分Mock,被测试代码如下:


  
  
  1. public class DataService {
  2. public boolean replaceData(final String dataId, final byte[] binaryData) {
  3. return modifyData(dataId, binaryData);
  4. }
  5. public boolean deleteData(final String dataId) {
  6. return modifyData(dataId, null);
  7. }
  8. private boolean modifyData(final String dataId, final byte[] binaryData) {
  9. return true;
  10. }
  11. }
只对modifyData方法进行Mock,而其他方法调用真实方法,测试代码如下:


  
  
  1. @RunWith(PowerMockRunner.class)
  2. @PrepareForTest(DataService.class)
  3. public class DataServiceTest {
  4. @Test
  5. public void testReplaceData() throws Exception {
  6. DataService tested = PowerMock.createPartialMock(DataService.class, “modifyData”); //创建部分mock对象,只对modifyData方法Mock
  7. PowerMock.expectPrivate(tested, “modifyData”, “id”, null).andReturn( true); //录制私有方法
  8. PowerMock.replay(tested);
  9. assertTrue(tested.deleteData(“id”));
  10. PowerMock.verify(tested);
  11. }
  12. }
部分Mock在被测试方法的依赖在同一个类,且不容易创建时比较有用。

个人认为私有方法的Mock意义不是很大,完全可以使用反射机制直接调用。

(4).调用对象的构造方法Mock对象:

在被测试方法内部调用构造创建了一个对象很常见,被测试代码如下:


  
  
  1. public class PersistenceManager {
  2. public boolean createDirectoryStructure(String directoryPath) {
  3. File directory = new File(directoryPath);
  4. if (directory.exists()) {
  5. throw new IllegalArgumentException( "\"" + directoryPath + "\" already exists.");
  6. }
  7. return directory.mkdirs();
  8. }
  9. }
创建文件操作(new File(path))依赖与操作系统底层实现,如果给定的路径不合法,将会出现异常导致测试无法正常覆盖,此时需要使用PowerMock的提供的调用构造方法创建Mock对象,测试代码如下:


  
  
  1. @RunWith(PowerMockRunner.class)
  2. @PrepareForTest( PersistenceManager.class )
  3. public class PersistenceManagerTest {
  4. @Test
  5. public void testCreateDirectoryStructure_ok() throws Exception {
  6. File fileMock = PowerMock.createMock(File.class);
  7. PersistenceManager tested = new PersistenceManager();
  8. PowerMock.expectNew(File.class, "directoryPath").andReturn(fileMock);
  9. EasyMock.expect(fileMock.exists()).andReturn( false);
  10. EasyMock.expect(fileMock.mkdirs()).andReturn( true);
  11. PowerMock.replay(fileMock, File.class);
  12. assertTrue(tested.createDirectoryStructure( "directoryPath"));
  13. PowerMock.verify(fileMock, File.class);
  14. }
  15. }
也可以使用更简便的方法:

FilefileMock = PowerMock.createMockAndExpectNew(File.class,“directoryPath”);

通过EasyMock+PowerMock,开发中绝大部分的方法都可以被测试完全覆盖。

更多关于PowerMock的用法和参考文档请参考PowerMock官方网址:

参考: https://code.google.com/p/powermock/

原文:https://blog.csdn.net/chjttony/article/details/14522771

猜你喜欢

转载自blog.csdn.net/lixinkuan328/article/details/94668675