Powermock遇到的问题

做为一名开发人员对于单元测试的态度是既爱又很。爱是由于单元测试可让开发人员在开发阶段尽量发现问题,避免生成故障的出现。恨是由于单元测试所带来的工做量与项目自身的编码工做量至关,而对于互联网公司讲究的是快速迭代上线,以业务需求为主导,留给开发人员编写单元测试的时间很是少。因此大部分开发人员是抗拒单元测试的,我也是其中之一。可是经历过几回的生产故障以后,开始意识到单元测试的必要性,因而乎开始学习使用单元测试。项目上使用的是JUnit单元测试框架,Powermock做为mock工具。如下是在使用Powermock的过程当中遇到的题。java

import java.util.ArrayList;
import java.util.List;
 public class UserDao {
   public UserDao() {
      String db_mode = System.getProperty("db_mode");
      Assert.notNull(db_mode);
   }

   public List<String> getAllUserNames() {
      return new ArrayList<>();
   }
}
import java.util.List;
 public class UserService {
   private UserDao userDao = new UserDao();

   public List<String> getAllUserNames() {
      return userDao.getAllUserNames();
   }

}
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.PowerMockRunner;

import java.util.Arrays;
import java.util.List;

import static org.powermock.api.mockito.PowerMockito.when;
 @PowerMockIgnore({ "javax.management.*" })
@RunWith(PowerMockRunner.class)
@PrepareForTest(UserService.class)
public class UserServiceTest {
   @InjectMocks
   private UserService userService;

   @Mock
   private UserDao userDao;

   @Test
   public void getAllUserNamesTest() {
      List<String> userNames = Arrays.asList("tom", "jack");
      when(userDao.getAllUserNames()).thenReturn(userNames);

      List<String> res = userService.getAllUserNames();
      Assert.assertEquals(2L, res.size());
      Assert.assertTrue(res.contains("tom"));
   }
}

运行UserServiceTest的getAllUserNamesTest测试Case,结果以下git

Process finished with exit code -2Caused by: org.mockito.exceptions.base.MockitoException: 
Cannot instantiate @InjectMocks field named 'userService' of type 'class com.demo.UserService'.
You haven't provided the instance at field declaration so I tried to construct the instance.
However the constructor or the initialization block threw an exception : [Assertion failed] - this argument is required; it must not be null

github

分析:在UserDao类的默认构造函数中须要读取db_mode的环境变量,而且校验该环境变量不能为空,不然抛出异常,而个人本地环境刚好没有设置db_mode环境变量。此时有人会想“把db_mode环境变量配置上不就解决了吗?”对,配置上db_mode环境变量确实能够解决问题;但违背了mock的本意,咱们应该把一切影响TestCase正常运行的外在条件给mock掉,保证在任何环境下(jdk版本必须相同)均可以正常运行,而且获得预期结果。否则测试人员在jenkins上去执行单元测试时,确定跑不经过,回头测试该抱怨了。api

好吧,那咱们应该想办法把UserDao的构造函数给mock掉。回到UserServiceTest代码,发现userDao属性上使用了@Mock注解。app

@Mock
private UserDao userDao;

@Mock: 建立一个空白实例,没有属性没有方法。既然如此为何还调用了UserDao的默认构造函数呢?框架

@InjectMocks
private UserService userService;

咱们发现userService使用了@InjectMocks注解。 ide

@InjectMocks:建立一个目标类的实例,其他用@Mock(或@Spy)注解建立的mock将被注入到用该实例中经过调试代码发现,mockito框架对于@InjectMocks的属性试图经过反射机制建立实例。函数

public FieldInitializationReport instantiate() {
        final AccessibilityChanger changer = new AccessibilityChanger();
        Constructor<?> constructor = null;
        try {
            constructor = field.getType().getDeclaredConstructor();
            changer.enableAccess(constructor);

            final Object[] noArg = new Object[0];
            Object newFieldInstance = constructor.newInstance(noArg);
            new FieldSetter(testClass, field).set(newFieldInstance);

            return new FieldInitializationReport(field.get(testClass), true, false);
        } catch (NoSuchMethodException e) {
            throw new MockitoException("the type '" + field.getType().getSimpleName() + "' has no default constructor", e);
        } catch (InvocationTargetException e) {
            throw new MockitoException("the default constructor of type '" + field.getType().getSimpleName() + "' has raised an exception (see the stack trace for cause): " + e.getTargetException().toString(), e);
        } catch (InstantiationException e) {
            throw new MockitoException("InstantiationException (see the stack trace for cause): " + e.toString(), e);
        } catch (IllegalAccessException e) {
            throw new MockitoException("IllegalAccessException (see the stack trace for cause): " + e.toString(), e);
        } finally {
            if(constructor != null) {
                changer.safelyDisableAccess(constructor);
            }
        }
    }
}

Java类的初始化顺序:
1. 父类静态变量初始化 
2. 父类静态语句块 
3. 子类静态变量初始化 
4. 子类静态语句块 
5. 父类变量初始化 
6. 父类语句块 
7. 父类构造函数 
8. 子类变量初始化 
9. 子类语句块 
10. 子类构造函数
工具

在UserService类中有一个userDao属性,而且经过new UserDao()进行了实例化。在初始化UserService类实例的时候,间接地调用了new UserDao()。单元测试

解决:网上搜索了一下解决方案,大部分都是经过在测试类加上@PrepareForTest注解去阻止不想要的行为,但是我明明已经使用了@PrepareForTest注解。这时候我想到了应该求助官方的帮助文档,https://github.com/powermock/powermock/wiki/Suppress-Unwanted-Behavior。

  1. Use the @RunWith(PowerMockRunner.class) annotation at the class-level of the test case.
  2. Use the @PrepareForTest(ClassWithEvilParentConstructor.class) annotation at the class-level of the test case in combination with suppress(constructor(EvilParent.class)) to suppress all constructors for the EvilParent class.
  3. Use the Whitebox.newInstance(ClassWithEvilConstructor.class) method to instantiate a class without invoking the constructor what so ever.
  4. Use the @SuppressStaticInitializationFor("org.mycompany.ClassWithEvilStaticInitializer")annotation to remove the static initializer for the the org.mycompany.ClassWithEvilStaticInitializer class.
  5. Use the @PrepareForTest(ClassWithEvilMethod.class) annotation at the class-level of the test case in combination with suppress(method(ClassWithEvilMethod.class, "methodName")) to suppress the method with name "methodName" in the ClassWithEvilMethod class.
  6. Use the @PrepareForTest(ClassWithEvilField.class) annotation at the class-level of the test case in combination with suppress(field(ClassWithEvilField.class, "fieldName")) to suppress the field with name "fieldName" in the ClassWithEvilField class.

显然,要达到抑制的效果,必须配合suppress(constructor(TargetClass.class))函数一块儿使用。在UserServiceTest类中添加一下代码,问题搞定。

@BeforeClass
public static void suppressUnWanted() {
   suppress(constructor(UserDao.class));
}

另外,@SuppressStaticInitializationFor注解也是常常用到的,能够阻止Class中的静态属性和静态块的初始化。


做者:vip - dani.he

日期:2018-03-04