Powermock遇到的问题

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

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,结果如下

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

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

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

@Mock
private UserDao userDao;

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

@InjectMocks
private UserService userService;

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

@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。

扫描二维码关注公众号,回复: 766814 查看本文章
  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

猜你喜欢

转载自blog.csdn.net/vipshop_fin_dev/article/details/79439334
今日推荐