作为一名开发人员对于单元测试的态度是既爱又很。爱是因为单元测试可以让开发人员在开发阶段尽可能发现问题,避免生成故障的出现。恨是因为单元测试所带来的工作量与项目自身的编码工作量相当,而对于互联网公司讲究的是快速迭代上线,以业务需求为主导,留给开发人员编写单元测试的时间非常少。所以大部分开发人员是抗拒单元测试的,我也是其中之一。但是经历过几次的生产故障之后,开始意识到单元测试的必要性,于是乎开始学习使用单元测试。项目上使用的是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。
- Use the
@RunWith(PowerMockRunner.class)
annotation at the class-level of the test case. - Use the
@PrepareForTest(ClassWithEvilParentConstructor.class)
annotation at the class-level of the test case in combination withsuppress(constructor(EvilParent.class))
to suppress all constructors for the EvilParent class. - Use the
Whitebox.newInstance(ClassWithEvilConstructor.class)
method to instantiate a class without invoking the constructor what so ever. - Use the
@SuppressStaticInitializationFor("org.mycompany.ClassWithEvilStaticInitializer")
annotation to remove the static initializer for the theorg.mycompany.ClassWithEvilStaticInitializer
class. - Use the
@PrepareForTest(ClassWithEvilMethod.class)
annotation at the class-level of the test case in combination withsuppress(method(ClassWithEvilMethod.class, "methodName"))
to suppress the method with name "methodName" in the ClassWithEvilMethod class. - Use the
@PrepareForTest(ClassWithEvilField.class)
annotation at the class-level of the test case in combination withsuppress(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