单元测试

单元测试,是为了测试某一个类的某一个方法能否正常工作,而写的测试代码。

每个test case的报告可以在project_root/app/build/reports/tests/debug/index.html 这个xml里面看到。

一般来说,单元测试不会接触到数据库,不会接触到网络,不会接触到一些复杂的外部环境,如果有的话,那可能是你测试的方式有误,测试的粒度不够“单元”,希望这篇文章能将这两者的区别解释清楚。

  1. 单元测试,的确是一门需要学习的技术。

不仅需要学习,而且你要学习的东西还真不少,你要学习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 (可以支持上面不能支持的)

对比Mockito和JMockit的文章:

以上就是现在我们这边单元测试用到的几个基本技术:JUnit4 + Mockito + Dagger2 + Robolectric。基本来说,并没有什么黑科技,都是业界标准。

  1. 尽量写出易于测试的代码 static method、直接new object、singleton、Global state等等这些都是一些不利于测试的代码方式,应该尽量避免,用依赖注入来代替这些方式。

  2. 公共的单元测试library 如果你们公司也是组件化开发的话,抽出一个公共的单元测试类库来做单元测试,里面可以放一些公共的helper、utils、rules等等,这个可以极大的提高写单元测试的速度。

关于JUnit Rule的使用

https://static.javadoc.io/org.mockito/mockito-core/2.21.0/org/mockito/InjectMocks.html


Android 单元测试只看这一篇就够了

Android 开发应该尝试的 UI 自动化测试

**单元测试用于执行项目中的目标函数并验证其状态或者结果,其中,单元指的是测试的最小模块,通常指函数。这些代码能够检测目标代码的正确性,打包时单元测试的代码不会被编译进入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。

results matching ""

    No results matching ""