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