Android 单元测试实践&代码/分支覆盖率(jacoco)

2,225 阅读3分钟

参考:Android+jacoco实现代码覆盖率最正确的实现方式,没有之一! - 腾讯云开发者社区-腾讯云 (tencent.com)

Android ui 单元测试 覆盖率,Android单元测试/Ui测试+JaCoCo覆盖率统计_Microsoft俱乐部的博客-CSDN博客

java+uiautomator——APP自动化——背诵整理_小白龙白龙马的博客-CSDN博客

Android单元测试只看这一篇就够了_gf771115的博客-CSDN博客

一.环境配置

Android Studio IDE

../app/jacoco.gradle

apply plugin: 'jacoco'

jacoco {

    toolVersion = "0.8.2"
}

android {
    defaultPublishConfig "debug"
    buildTypes {
        debug {
            /**打开覆盖率统计开关**/
            testCoverageEnabled = true
        }
    }
}

//源代码路径,你有多少个module,你就在这写多少个路径
def coverageSourceDirs = [
        '../app/src/main/java',
]

//class文件路径,就是我上面提到的class路径,看你的工程class生成路径是什么,替换我的就行
def coverageClassDirs = [
        '../app/build/intermediates/javac/debug/classes',
]

//这个就是具体解析ec文件的任务,会根据我们指定的class路径、源码路径、ec路径进行解析输出
//如果没有自动生成 覆盖率报告的话, 可以执行这个task 手动生成
task jacocoTestReport(type: JacocoReport) {

    group = "Reporting"
    description = "Generate Jacoco coverage reports after running tests."
    reports {
        xml.enabled = true
        html.enabled = true
    }

    classDirectories = files(files(coverageClassDirs).files.collect {
        fileTree(dir: it,
                // 过滤不需要统计的class文件
                excludes: ['**/R*.class',
                           '**/*$InjectAdapter.class',
                           '**/*$ModuleAdapter.class',
                           '**/*$ViewInjector*.class',
                           '**/I*.class',
                           '**/*ForTest.class',
                           '**/MenuItemManager.class',
                           '**/AudioPictureSettingManager.class',
                           '**/audiocontrol/**/*.class',
                           '**/audiopicturesetting/**/*.class',
                           '**/bean/**/*.class',
                           '**/database/**/*.class'

                ])
    })

    sourceDirectories = files(coverageSourceDirs)
    //设备不同 coverage.ec 的名字会发生变化
    executionData = files("$buildDir/outputs/code_coverage/debugAndroidTest/connected/BRAVIA 4K AE1 - 12-coverage.ec")
}

../app/unittest.gradle

//覆盖率统计配置
apply from: 'jacoco.gradle'

//单元测试 配置
configurations.all {
    resolutionStrategy {
        force 'androidx.lifecycle:lifecycle-common:2.0.0'
        force 'androidx.lifecycle:lifecycle-runtime:2.0.0'
        force 'androidx.collection:collection:1.0.0'
    }
}

dependencies {

    //单元测试 junit
    testImplementation 'junit:junit:4.13.2'
    // Core library
    androidTestImplementation 'androidx.test:core:1.4.0'//

    // AndroidJUnitRunner and JUnit Rules
    androidTestImplementation 'androidx.test:runner:1.4.0'
    androidTestImplementation 'androidx.test:rules:1.4.0'//

    // Assertions
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'//
    androidTestImplementation 'androidx.test.ext:truth:1.4.0'
    androidTestImplementation 'com.google.truth:truth:1.0'

    // Espresso dependencies
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
    androidTestImplementation 'androidx.test.espresso:espresso-contrib:3.4.0'
    androidTestImplementation 'androidx.test.espresso:espresso-intents:3.4.0'
    androidTestImplementation 'androidx.test.espresso:espresso-accessibility:3.4.0'
    androidTestImplementation 'androidx.test.espresso:espresso-web:3.4.0'
    androidTestImplementation 'androidx.test.espresso.idling:idling-concurrent:3.4.0'
    // The following Espresso dependency can be either "implementation"
    // or "androidTestImplementation", depending on whether you want the
    // dependency to appear on your APK's compile classpath or the test APK
    // classpath.
    androidTestImplementation 'androidx.test.espresso:espresso-idling-resource:3.4.0'
//    uiautomator
    androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0'//
    //单元测试 powermock
//    testImplementation 'org.powermock:powermock-api-mockito2:1.7.4'
//    testImplementation 'org.powermock:powermock-module-junit4:1.7.4'
}

../app/build.gradle

plugins {
    id 'com.android.application'
    id 'kotlin-android'
}

apply from: 'unittest.gradle'
android {...}
...

二. 实践

1.编写测试代码:

androidTest 中是测试android 代码的地方

..\app\src\androidTest

demo比较简单, 主要是测试AIDL 接口, 接口功能是弹出dialog, 所以要判断是否弹出dialog

MyTest.java

@RunWith(AndroidJUnit4.class)
public class MyTest {
  
    long TIMEOUT = 10 * 1000;
    public static final String TAG = "MyTAG";
    @Rule
    public final ServiceTestRule mServiceRule = new ServiceTestRule();
    IMyService iService ;    UiDevice device;
    Context appContext;
    CyclicBarrier barrier = new CyclicBarrier(2);


    @Before
    public void setUp() throws Exception {

        appContext = InstrumentationRegistry.getInstrumentation().getTargetContext();
        device = UiDevice.getInstance(InstrumentationRegistry.getInstrumentation());
        Intent serviceIntent = new Intent(ApplicationProvider.getApplicationContext(), MyService.class);
        // Bind the service and grab a reference to the binder.
        IBinder binder = mServiceRule.bindService(serviceIntent);
        iService = IMyService.Stub.asInterface(binder);    }

    @After
    public void tearDown() throws Exception {
        mServiceRule.unbindService();
    }
    
    private void sleeSsecondsOf(int second) {
        barrier.reset();
        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000 * second);
                    barrier.await();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }).start();
        try {
            barrier.await();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    @Category(CategoryTest.QuickCategory.class)
    @Test
    public void testShowDangbeiDialog() throws RemoteException {
        iService.showDangbeiMessage("测试Dialog", "test");
        sleeSsecondsOf(5);
        boolean showing = false;
        UiObject dialog = device.findObject(new UiSelector().textContains("测试Dialog"));
        try {
            Log.d(TAG, "testShowDangbeiDialog: " + dialog.getClassName());
            showing = true;
        } catch (UiObjectNotFoundException e) {
            showing = false;
            Log.d(TAG, "testShowDangbeiDialog: " + e.toString());
            e.printStackTrace();
        }
        assertThat(showing, equalTo(true));


        iService.removeDangbeiMessage();        sleeSsecondsOf(2);
        dialog = device.findObject(new UiSelector().textContains("测试Dialog"));
        try {
            Log.d(TAG, "testShowDangbeiDialog: " + dialog.getClassName());
            showing = true;
        } catch (UiObjectNotFoundException e) {
            showing = false;
            Log.d(TAG, "testShowDangbeiDialog: " + e.toString());
            e.printStackTrace();
        }
        assertThat(showing, equalTo(false));
    }
}


2.运行测试代码

在Android Studio 右边栏 的Gradle 中双击Tasks/verification/createDebugCoverageReport 进行测试

运行完之后:

测试报告目录:..\app\build\reports\androidTests\connected\index.html

覆盖率目录:  ..\app\build\reports\coverage\debug\index.html

在浏览器中打开即可查看

有时候覆盖率文件没有自动生成, 这个时候就需要用到上面 jacoco.gradle 中的task 了: Android Studio 下边栏 Terminal 中执行 gradlew jacocoTestReport   

执行完毕后会覆盖率报告会在如下位置:

..\app\build\reports\jacoco\jacocoTestReport\html\index.html