Android单元测试——辅助工具介绍

最近在学习单元测试的相关知识,在这里我将分享一下我在学习过程中,使用到的一些辅助工具或框架。我也是一个初学小白,不足之处,还望大家予以指正。

文中使用的IDE是 Android Studio,下面涉及到的插件、工具或导包,都是以Android Studio为例。

鸣谢:1.本文采用的代码来自Robolectric3.0的介绍和实战,该文附带的两篇博客也在单元测试方面给了我很大帮助,强烈推荐。 2.一些点子来自Android单元测试在蘑菇街支付金融部门的实践,该作者小创 有很多关于单元测试的知识值得学习。

一.Code Coverage Tool (代码覆盖率工具):

众所周知,在编写单元测试案例的过程中,有一个重要的指标就是代码覆盖率。编写单元测试并不是为了追求100%覆盖率,但覆盖率在单元测试中仍占据着不可或缺的地位。

经典的Java Coverage Tool : Emma、Eclemma(Eclipse推荐使用)、Cobertura,感兴趣的学者可以自行查阅资料。

接下来,我们来看Android Studio支持的Coverage Tool : jacoco、IntelliJ IDEA。

在Android Stuido新建过工程的同学,应该有注意到,该工程默认会新建androidTest及test的测试包。在Android Stuido中,在androidTest编写的单元测试,默认使用jacoco插件生成包含代码覆盖率的测试报告;而test包下的单元测试代码,则直接使用Android Studio已有工具IntelliJ IDEA生成覆盖率,也可以通过自定义gradle task使用jacoco插件生成与androidTest相同格式的测试报告。


androidTest 与 test:

区别: androidTest是存放一些与View(UI界面层)相关的单元测试案例的测试代码集,需要在真机或虚拟机上运行。一般使用的框架有:Instrumentation、Espresso,也可以编写自动化测试案例(框架:Uiautomator2.0); 而test包则一般只存放与Model(数据层)相关的单元测试案例,但Android几乎无法实现MV完全解耦,所以目前在test包下可能也会涉及到View的测试。直接在JVM虚拟机上运行即可,速度快。框架:Robolectric+Mockito+其他。

运行方式: Android Studio 2.0版本开始,已经能够智能检测当前的测试是androidTest还是test了,低版本的可以在Build Variants中设置Test Artifact或者Add New Configuration 时选择Unit Tests(对应test),Android Instrumentation Tests(对应androidTest)。


jacoco:

  • 先讲androidTest的使用方法

在Module gradle文件中添加以下代码:

apply plugin : 'com.android.application'
apply plugin: 'jacoco'
buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile ('proguard-android.txt' ), 'proguard-rules.pro'
    }
    debug {
        testCoverageEnabled true
    }
}

添加完代码之后,打开右侧的Gradle面板运行connectedAndroidTest任务, 或在Terminal控制台输入 gradlew connectedAndroidTest,回车

在\app\build\reports\androidTests\connected\index.html:

在\app\build\reports\coverage\debug \index.html:

androidTest包下未添加测试案例,所以Test Summary测试条数为0,覆盖率也都为0。可自行添加案例测试

  • 对于test包下,如果使用jacoco生成覆盖率将会麻烦一点点

需要在gradle中自定义task,代码如下:

task jacocoTestReport( type : JacocoReport, dependsOn : "testDebugUnitTest") {
    group = "Reporting"

    description = "Generate Jacoco coverage reports"

    // exclude auto-generated classes and tests
    def fileFilter = [ '**/R.class', '**/R$*.class' ,
                      '**/BuildConfig.*' , '**/Manifest*.*' ,
                      'android/**/*.*' ]
    def debugTree = fileTree( dir:
            " ${ project. buildDir }/intermediates/classes/debug" ,
            excludes : fileFilter)
    def mainSrc = "${ project .projectDir } /src/main/java"

    sourceDirectories = files([mainSrc])
    classDirectories = files([debugTree])
    additionalSourceDirs = files([
            " ${ buildDir} /generated/source/buildConfig/debug" ,
            " ${ buildDir} /generated/source/r/debug"
    ])
    executionData = fileTree( dir: project .projectDir , includes:
            ['**/*.exec' , '**/*.ec'])

    reports {
        xml . enabled = true
        xml . destination = " ${buildDir } /jacocoTestReport.xml"
        csv . enabled = false
        html . enabled = true
        html . destination = " ${buildDir } /reports/jacoco"
    }
}

添加task之后在右侧gradle面板中会发现,多了jacocoTestReport任务:

在\app\build\reports\tests\debug\index.html:

在\app\build\reports\jacoco\index.html:

那么问题来了:如果同时编写了androidTest与test单元测试代码,能否使用jacoco生成一份综合的代码覆盖率测试报告呢?此处应有思考。

IntelliJ IDEA:

目前,只有test才支持此方式,操作步骤请看下图:

Tracing模式会增加消耗,但测量会更精确,但一般使用Sampling就足够。生成报告:


jacoco 与 IntelliJ IDEA:

两者各有优缺点,最明显的不同是,jacoco生成的是html报告,如果使用Jenkins自动构建测试,应该使用jacoco;而 IntelliJ IDEA,其实也是采用jacoco检测覆盖率的,但其在面板中显示,较为直观,推荐在编写调试案例时使用。分不同情况使用不同的覆盖率工具,两者可以相互取长补短。


二.静态代码检测工具:FindBugs

what:静态代码分析是指无需运行被测代码,仅通过分析或检查源程序的语法、结构、过程、接口等来检查程序的正确性,找出代码隐藏的错误和缺陷,如参数不匹配,有歧义的嵌套语句,错误的递归,非法计算,可能出现的空指针引用等等。

安装:AndroidStudio->Settigns->Plugins->Browse repositories->search “findBUgs-IDEA” 安装重启AndroidStudio即可。

安装完之后,工程右键选择>FindBugs>Analyze Module Files,即可开始检测代码。

若部分错误不想扫描,可右键工程->Open Module Settings里设置:

扫描结果分析: 该工程扫描出两种类型的错误Bad practice(坏的实践:不建议的写法,建议修改为约定俗成的写法)和Dodgy Code(危险代码: 具有潜在危险的代码,可能运行期产生错误),并给出了详细的说明。此工程的代码量较少,且编码风格也较好,所以查出来的bug较少,FindBugs关于FindBugs更多信息,可查看一些你不知道的事,findbugs的配置跟使用。

需要注意的是,虽然FindBugs能够对代码进行静态检测,但并不是FindBugs查找出来的bug就一定存在问题,也并不是所有潜在的bug,FidBugs都能查找出来,还需要开发者结合具体代码进行判断。

为啥不用静态代码检查工具呢? 静态代码检查从入门到放弃。

三.Annotation、@Rule介绍

Annotation 与 JUnit Rule 的详细使用,需要自行学习,这里只介绍Android单元测试在蘑菇街支付金融部门的实践中提到的结合两者来编写测试方法的注释,来规避测试方法名冗长、难以定义、格式不美观的问题。当测试失败时,会在控制台上添加自定义的错误信息。这的确是一个不错的方法。

1).新建Annotation类,需要注意的是,单元测试以JUnit形式运行,需要添加@Retention,否则运行时无法检测到该类

@Retention (RetentionPolicy. RUNTIME)
public @interface Purpose {
    String desc () default "" ;
}

2).新建实现MethodRule接口的自定义Rule类,在apply方法中处理案例执行前、执行成功或失败所做的操作:

public class ErrorRule implements MethodRule {
    @Override
    public Statement apply( final Statement base, final FrameworkMethod method , Object target) {
        return new Statement() {
            @Override
            public void evaluate () throws Throwable {
                /***starting(method);***/
                ShadowLog. stream = System. out ;//此处是使用Robolectric框架的方法,将该测试案例的log输出到控制台,方便查看log
                try {
                    base.evaluate() ;
                    /***succeeded(method);***/
                } catch (Throwable t) {
                    /***failed(t, method);***/
                    try {
                        System. err .print("@" + method.getName() + "--->" + method .getAnnotation(Purpose . class).desc()) ;
                    } catch (NullPointerException e) {

                    } finally {

                        throw t;
                    }

                } finally {
                    /***finished(method);***/
                }
            }
        };
    }
}

3).在测试类中调用Purpose、ErrorRule

@RunWith (RobolectricGradleTestRunner. class)
@Config (constants = BuildConfig. class)
public class MainActivityTest {

    @Rule
    public ErrorRule rule = new ErrorRule();

    @Test
    @Purpose( desc = "when onCreate, the menu shoule be 'First menu item' and 'Second menu item'. ")
    public void onCreateShouldInflateTheMenu () {
        Activity activity = Robolectric. setupActivity(MainActivity. class) ;

        final Menu menu = shadowOf(activity).getOptionsMenu() ;
        assertEquals(menu.findItem(R.id. item1).getTitle() , "first menu item") ;
        assertEquals(menu.findItem(R.id. item2).getTitle() , "Second menu item") ;

    }
}

4).运行结果: