单元测试
单元测试,是为了测试某一个类的某一个方法能否正常工作,而写的测试代码。
每个test case的报告可以在project_root/app/build/reports/tests/debug/index.html 这个xml里面看到。
一般来说,单元测试不会接触到数据库,不会接触到网络,不会接触到一些复杂的外部环境,如果有的话,那可能是你测试的方式有误,测试的粒度不够“单元”,希望这篇文章能将这两者的区别解释清楚。
- 单元测试,的确是一门需要学习的技术。
不仅需要学习,而且你要学习的东西还真不少,你要学习JUnit的使用,你要学习Mokito的使用,Robolectric的使用,依赖注入的概念和使用等等等待。此外,在刚开始的时候,你的确也会遇到很多坑,现有代码的坑,Android的坑,Robolectric的坑等等。这个在安卓开发这边显得更是如此,因为Android开发环境是公认的最不利用写单元测试的环境之一。你需要花一些时间去学习如何处理,或者是绕过这些坑。
因为容易测试的项目,往往意味着更灵活,更具备扩展性。
你可以慢慢的搭建自己的一套测试框架,简化一些常用的繁琐的写法,让写单元测试变得更简便快捷。
但是如果使用Activity,我们就需要用到Robolectric框架。
怎么验证LoginPresenter里面的mUserManager的performLogin()方法得到了调用,以及它的参数是正确性呢?这里需要用到mock,那么接下来,我们就介绍mock这个东西。
所谓的mock就是创建一个类的虚假的对象,在测试环境中,用来替换掉真实的对象,以达到两大目的:
验证这个对象的某些方法的调用情况,调用了多少次,参数是什么等等
指定这个对象的某些方法的行为,返回特定的值,或者是执行特定的动作
Mockito.mock(UserManager.class); //Mock一个对象
2个误解:
1,Mockito.mock()并不是mock一整个类,而是根据传进去的一个类,mock出属于这个类的一个对象,并且返回这个mock对象;而传进去的这个类本身并没有改变,用这个类new出来的对象也没有受到任何改变!
2,mock出来的对象并不会自动替换掉正式代码里面的对象,你必须要有某种方式把mock对象应用到正式代码里面
正确的代码如下:
public class LoginPresenter {
private UserManager mUserManager = new UserManager();
public void login(String username, String password) {
if (username == null || username.length() == 0) return;
if (password == null || password.length() < 6) return;
mUserManager.performLogin(username, password);
}
public void setUserManager(UserManager userManager) { //<==
this.mUserManager = userManager;
}
}
@Test
public void testLogin() throws Exception {
UserManager mockUserManager = Mockito.mock(UserManager.class);
LoginPresenter loginPresenter = new LoginPresenter();
loginPresenter.setUserManager(mockUserManager); //<==
loginPresenter.login("xiaochuang", "xiaochuang password");
Mockito.verify(mockUserManager).performLogin("xiaochuang", "xiaochuang password");
}
如果你的正式代码里面没有任何地方用到了那个setter的话,那么专门为了测试而增加了一个方法,毕竟不是很优雅的解决办法,更好的解决办法是使用依赖注入,简单解释就是把UserManager作为LoginPresenter的构造函数的参数,传进去。
Mockito.verify(mockUserManager, Mockito.times(3)).performLogin(...); //验证mockUserManager的performLogin得到了三次调用。
mock的第二大作用,指定mock对象的某个方法返回特定的值。
Mockito.when(mockObject.targetMethod(args)).thenReturn(desiredReturnValue);
//先创建一个mock对象
PasswordValidator mockValidator = Mockito.mock(PasswordValidator.class);
//当调用mockValidator的verifyPassword方法,同时传入"xiaochuang_is_handsome"时,返回true
Mockito.when(mockValidator.verifyPassword("xiaochuang_is_handsome")).thenReturn(true);
//当调用mockValidator的verifyPassword方法,同时传入"xiaochuang_is_not_handsome"时,返回false
Mockito.when(validator.verifyPassword("xiaochuang_is_not_handsome")).thenReturn(false);
下面的代码可以读读:
//假设目标类的实现是这样的
public class PasswordValidator {
public boolean verifyPassword(String password) {
return "xiaochuang_is_handsome".equals(password);
}
}
@Test
public void testSpy() {
//跟创建mock类似,只不过调用的是spy方法,而不是mock方法。spy的用法
PasswordValidator spyValidator = Mockito.spy(PasswordValidator.class);
//在默认情况下,spy对象会调用这个类的真实逻辑,并返回相应的返回值,这可以对照上面的真实逻辑
spyValidator.verifyPassword("xiaochuang_is_handsome"); //true
spyValidator.verifyPassword("xiaochuang_is_not_handsome"); //false
//spy对象的方法也可以指定特定的行为
Mockito.when(spyValidator.verifyPassword(anyString())).thenReturn(true);
//同样的,可以验证spy对象的方法调用情况
spyValidator.verifyPassword("xiaochuang_is_handsome");
Mockito.verify(spyValidator).verifyPassword("xiaochuang_is_handsome"); //pass
}
总结:
验证方法的调用
//验证mockUserManager这个对象调用了performLogin这个方法 Mockito.verify(mockUserManager).performLogin("xiaochuang", "xiaochuang password");
验证方法调用的次数
//验证mockUserManager的performLogin方法调用了3次 Mockito.verify(mockUserManager, Mockito.times(3)).performLogin(...);
指定方法返回特定值:
Mockito.when(mockValidator.verifyPassword("xiaochuang_is_handsome")).thenReturn(true);
//返回true,无论参数是什么 Mockito.when(validator.verifyPassword(anyString())).thenReturn(true);
spy与mock的唯一区别就是默认行为不一样:spy对象的方法默认调用真实的逻辑,mock对象的方法默认什么都不做,或直接返回默认值。
public class PasswordValidator {
public boolean verifyPassword(String password) {
return "xiaochuang_is_handsome".equals(password);
}
}
@Test
public void testSpy() {
//跟创建mock类似,只不过调用的是spy方法,而不是mock方法。spy的用法
PasswordValidator spyValidator = Mockito.spy(PasswordValidator.class);
//在默认情况下,spy对象会调用这个类的真实逻辑,并返回相应的返回值,这可以对照上面的真实逻辑
spyValidator.verifyPassword("xiaochuang_is_handsome"); //true
spyValidator.verifyPassword("xiaochuang_is_not_handsome"); //false
//spy对象的方法也可以指定特定的行为
Mockito.when(spyValidator.verifyPassword(anyString())).thenReturn(true);
//同样的,可以验证spy对象的方法调用情况
spyValidator.verifyPassword("xiaochuang_is_handsome");
Mockito.verify(spyValidator).verifyPassword("xiaochuang_is_handsome"); //pass
}
如果你想了解Mockito更详细的用法可以参考这篇文章,写的是相当的好:
Robolectric就是一个能够让我们在JVM上跑测试时够调用安卓的类的框架,测一个小的独立的代码单元,Robolectric的角色,应该是一个让我们在做 单元测试 的过程中,能够调用安卓的类,测试安卓的类,把安卓的类当做普通的纯java类的一个framework,仅此而已。
在安卓上面写测试,有很多技术方案。有JUnit、Instrumentation test、Espresso、UiAutomator等等,还有第三方的Appium、Robotium、Calabash等等。
mock框架
Mockito
不能mock static method 和 final class、final method
- JMockit (可以支持上面不能支持的)
以上就是现在我们这边单元测试用到的几个基本技术:JUnit4 + Mockito + Dagger2 + Robolectric。基本来说,并没有什么黑科技,都是业界标准。
尽量写出易于测试的代码 static method、直接new object、singleton、Global state等等这些都是一些不利于测试的代码方式,应该尽量避免,用依赖注入来代替这些方式。
公共的单元测试library 如果你们公司也是组件化开发的话,抽出一个公共的单元测试类库来做单元测试,里面可以放一些公共的helper、utils、rules等等,这个可以极大的提高写单元测试的速度。
关于JUnit Rule的使用
https://static.javadoc.io/org.mockito/mockito-core/2.21.0/org/mockito/InjectMocks.html
**单元测试用于执行项目中的目标函数并验证其状态或者结果,其中,单元指的是测试的最小模块,通常指函数。这些代码能够检测目标代码的正确性,打包时单元测试的代码不会被编译进入APK中。**
针对每个函数进行设计单元测试用例。
单元测试的目标函数主要有两种
有明确的返回值
做单元测试时,只需调用这个函数,然后验证函数的返回值是否符合预期结果。
没有返回值
这个函数只改变其对象内部的一些属性或者状态,就验证它所改变的属性和状态。 一些函数没有返回值,也没有直接改变哪个值的状态,这就需要验证其行为,比如点击事件。
既没有返回值,也没有改变状态,又没有触发行为的函数是不可测试的,在项目中不应该存在。
当存在同时具备上述多种特性时,本文建议采用多个case来对每一种特性逐一验证,或者采用一个case,逐一执行目标函数并验证其影响,构造用例的原则是测试用例与函数一对一。
Java单元测试中,测试框架是JUnit,良好的单元测试是需要保证所有函数执行正确的,即所有边界条件都验证过,一个用例只测一个函数,便于维护。在Android单元测试中,并不要求对所有函数都覆盖到,像Android SDK中的函数回调则不用测试。
在Java中,编写代码面对的只有类、对象、函数。
在Android中,编写代码需要面对的是组件、控件、生命周期、异步任务、消息传递等,虽然本质是SDK主动执行了一些实例的函数,但创建一个Activity并不能让它执行到resume的状态,因此需要JUnit之外的框架支持。
当前主流的单元测试框架AndroidTest和Robolectric,前者需要运行在Android环境上,后者可以直接运行在JVM上,速度也更快,可以直接由Jenkins周期性执行,无需准备Android环境。因此我们的单元测试基于Robolectric。对于一些测试对象依赖度较高而需要解除依赖的场景,我们可以借助Mock框架。
如果要测试的目标对象依赖关系较多,需要解除依赖关系,以免测试用例过于复杂,推荐更加简单的Mock框架,比如Mockito,该框架可以模拟出对象来,而且本身提供了一些验证函数执行的功能。
Robolectric支持单元测试范围从Activity的跳转、Activity展示View(包括菜单)和Fragment到View的点击触摸以及事件响应,同时Robolectric也能测试Toast和Dialog。对于需要网络请求数据的测试,Robolectric可以模拟网络请求的response。对于一些Robolectric不能测试的对象,比如ConcurrentTask,可以通过自定义Shadow的方式现实测试。
Robolectric的本质是在Java运行环境下,采用Shadow的方式对Android中的组件进行模拟测试,从而实现Android单元测试。对于一些Robolectirc暂不支持的组件,可以采用自定义Shadow的方式扩展Robolectric的功能。
可以参考Robolectric官网,学习一下。
JUnit4基础方法注解
@Before
它会在每个测试方法执行前都调用一次。
@BeforeClass
它会在所有的测试方法执行之前调用一次。与@Before的差别是:@Before注解的方法在每个方法执行前都会调用一次,有多少个测试方法就会掉用多少次;而@BeforeClass注解的方法只会执行一次,在所有的测试方法执行前调用一次。注意该注解的测试方法必须是public static void修饰的。
@AfterClass
与@BeforeClass对应,它会在所有的测试方法执行完成后调用一次。注意该注解的测试方法必须是public static void修饰的。
@Ignore
忽略该测试方法,有时我们不想运行某个测试方法时,可以加上该注解。
单元测试不会接触到数据库,不会接触到网络,不会接触到一些复杂的外部环境,如果有的话,那可能是你测试的方式有误,测试的粒度不够“单元”。
什么情况下必须用dagger2、而什么时候可以不用呢?答案是,如果被测类(比如说LoginActivity)的Dependency(LoginPresenter)是通过 field injection inject进去的,那么再测这个类(LoginActivity)的时候,就必须用dagger2,不然很难优雅的把mock传进去。
而dagger2的作用,或者说角色,在于它让我们写正式代码的时候使用DI变得易如反掌,程序及其简洁优雅可读性高。同时,它在某些情况下让原来很难测的代码变得用以测试。
由于基于Android sdk 环境的安卓单元测试存在依赖安卓设备,需要打包安装,运行速度慢等原因,故单元测试选型为Junit与mockito相结合并配合robolectric基于纯Java环境下的单元测试。
首先在build.gradle文件的dependencies下添加test依赖,对于担心添加依赖影响安卓apk安装包大小的同学可以不必担心,因为testCompile的依赖仅供test文件使用,是不会打包的apk中的。如果你愿意,其实添加多少test依赖都无所谓。
testCompile 'junit:junit:4.12'
testCompile "org.mockito:mockito-core:1.9.5"
testCompile 'org.robolectric:robolectric:3.3.2'
testCompile 'org.robolectric:shadows-support-v4:3.0'
如果你不需要做单元测试,而只是使用dagger2来做DI,组织app的结构的话,其实AppModule里面的很多Provider方法是不需要定义的。比如说在这种情况下,LoginPresenter的Provider方法 provideLoginPresenter(UserManager userManager, PasswordValidator validator) 就不需要定义,你只需要在定义LoginPresenter的时候,给它的Constructor加上 @Inject修饰一下:
public class LoginPresenter {
private final UserManager mUserManager;
private final PasswordValidator mPasswordValidator;
@Inject
public LoginPresenter(UserManager userManager, PasswordValidator passwordValidator) {
this.mUserManager = userManager;
this.mPasswordValidator = passwordValidator;
}
//other methods
}
然而遗憾的是,这种方式将导致我们做单元测试的时候无法mock这中间的Dependency。