Android单测调研篇

1. 为什么做单测

单测的好处

减少bug
快速定位bug
提高代码质量
减少调试时间
放心重构

不得不写单测的原因

在成为大牛的路上,单测是必备技能
单测可以给你信心
保住面子

难处

难于坚持,在快速迭代开发过程中,可供写单测的时间过少

扩展

TDD(Test Drive Develop):测试驱动开发,是一种非常高效的开发方式

2. 测试框架

2.1 概述

 junit4.12 (单测框架)
 mockito1.9.5(mock工具)
 robolectric3.1.2 (模拟Android虚拟机)

2.2 junit4.12

Junit测试是程序员测试,即所谓白盒测试,因为程序员知道被测试的软件如何(How)完成功能和完成什么样(What)的功能。Junit是一套框架,继承TestCase类,就可以用Junit进行自动测试了。

示例代码:

@Before: 执行单测之前的初始化操作。

@After:单测完成后收尾工作。

      @Before
      public void setUp() throws Exception {
      }
      @After
      public void tearDown() throws Exception {
      }

assume: 用于判断测试用例的入参是否有业务含义的工具,如果入参不符合预期时会抛出

assumptionViolatedException、assumeTrue/assumeFalse、 assumeNotNull、 assumeThat、 assumeNoException

@RunWith(Theories.class)
public class AssumeTest
{
    @DataPoints
    public static String[] names = {"LiLei", "HanMeiMei"};

    @DataPoints
    public static int[] ages = {10, -2, 12};

    @Theory
    public void printAge(String name, int age)
    {
        Assume.assumeTrue(age > 0);
        System.out.println(String.format("%s's Name is %s.", name, age));
    }
}

assert :用于常用的测试结果验证

AssertTrue、AssertFalse:结果的true、false。

AssertThat:使用Matcher做自定义的校验。

AssertEquals、AssertNotEquals:判断两个对象是否相等。

AssertNull、AssertNotNull:判断对象是否为空。

AssertSame:判断两个对象是否为同一个,不同于equals这里是使用“==”判断。

AssertArrayEquals:判断两个数组是否相等。

    @Test
    public void sum() throws Exception {
        assertEquals(mCalculator.sum(3, 4), 7);
    }

verify : 主要用于验证方法是否执行

    @Test
    public void testVerify() {
        List mockedList = mock(List.class);
        mockedList.add("one");
        mockedList.clear();
        mockedList.add("3");
        // verification
        verify(mockedList).add("one");
        verify(mockedList).clear();
    }

其他高级用法:

@Test(timeout = 1000): 限时操作,若超过制定时间,强制停止

@Test(expected = ArithmeticException.class): 预测抛出指定异常

2.3 mockito1.9.5

创建mock对象不能对final,Anonymous ,primitive类进行mock。
用法:

when… thenRetrun; when… thenThrow

doNothing().doRetrun();

doNothing.doThrow()

anyInt、anyString、anyMap…..(参数匹配器)

    @Test
    public void argumentMatcherTest2(){
        Map

2.4 robolectric3.1.2

  实现一套JVM能运行的Android代码,从而做到脱离Android环境进行测试

1.测试跳转

    /**
     * Activity跳转测试
     */
    @Test
    public void testStartActivity() {
        //按钮点击后跳转到下一个Activity
        forwardBtn.performClick();
        Intent expectedIntent = new Intent(sampleActivity, LoginActivity.class);
        Intent actualIntent = ShadowApplication.getInstance().getNextStartedActivity();
        assertEquals(expectedIntent.getComponent(), actualIntent.getComponent());
    }

2.模拟activity

        sampleActivity = Robolectric.setupActivity(SampleActivity.class);
  1. UI测试:
    /**
     * Toast的测试
     */
    @Test
    public void testToast() {
        //点击按钮,出现吐司
        toastBtn.performClick();
        assertEquals(ShadowToast.getTextOfLatestToast(), "we love UT");
    }
    /**
     * Dialog的测试
     */
    @Test
    public void testDialog() {
        //点击按钮,出现对话框
        dialogBtn.performClick();
        AlertDialog latestAlertDialog = ShadowAlertDialog.getLatestAlertDialog();
        assertNotNull(latestAlertDialog);
    }
    /**
     * 测试控件状态
     */
    @Test
    public void testViewState() {
        CheckBox checkBox = (CheckBox) sampleActivity.findViewById(R.id.checkbox);
        Button inverseBtn = (Button) sampleActivity.findViewById(R.id.btn_inverse);
        assertTrue(inverseBtn.isEnabled());
        checkBox.setChecked(true);
        //点击按钮,CheckBox反选
        inverseBtn.performClick();
        assertTrue(!checkBox.isChecked());
        inverseBtn.performClick();
        assertTrue(checkBox.isChecked());
    }
    /**
     * 资源文件访问测试
     */
    @Test
    public void testResources() {
        Application application = RuntimeEnvironment.application;
        String appName = application.getString(R.string.app_name);
        String activityTitle = application.getString(R.string.title_activity_simple);
        assertEquals("LoveUT", appName);
        assertEquals("SimpleActivity", activityTitle);
    }
    /**
     * 测试广播
     */
    @Test
    public void testBoradcast() {
        ShadowApplication shadowApplication = ShadowApplication.getInstance();
        String action = "com.geniusmart.loveut.login";
        Intent intent = new Intent(action);
        intent.putExtra("EXTRA_USERNAME", "geniusmart");
        //测试是否注册广播接收者
        assertTrue(shadowApplication.hasReceiverForIntent(intent));
        //以下测试广播接受者的处理逻辑是否正确
        MyReceiver myReceiver = new MyReceiver();
        myReceiver.onReceive(RuntimeEnvironment.application, intent);
        SharedPreferences preferences = RuntimeEnvironment.application.getSharedPreferences("account", Context.MODE_PRIVATE);
        assertEquals("geniusmart", preferences.getString("USERNAME", ""));
    }
    /**
     * 测试Fragment
     */
    @Test
    public void testFragment() {
        SampleFragment sampleFragment = new SampleFragment();
        //此api可以主动添加Fragment到Activity中,因此会触发Fragment的onCreateView()
        SupportFragmentTestUtil.startFragment(sampleFragment);
        assertNotNull(sampleFragment.getView());
    }
  4.登录场景测试
    @Test
    public void loginSuccess() {
        emailView.setText("zhangzhan35@gmail.com");
        passwordView.setText("123");
        button.performClick();
        ShadowApplication application = ShadowApplication.getInstance();
        assertThat("Next activity has started", application.getNextStartedActivity(), is(notNullValue()));
    }
    @Test
    public void loginWithEmptyUsernameAndPassword() {
        button.performClick();
        ShadowApplication application = ShadowApplication.getInstance();
        assertThat("Next activity should not started", application.getNextStartedActivity(), is(nullValue()));
        assertThat("Show error for Email field ", emailView.getError(), is(notNullValue()));
        assertThat("Show error for Password field ", passwordView.getError(), is(notNullValue()));
        assertEquals(emailView.getError().toString(), RuntimeEnvironment.application.getString(R.string.error_field_required));
    }
    @Test
    public void loginFailure() {
        emailView.setText("invalid@email");
        passwordView.setText("invalidpassword");
        button.performClick();
        ShadowApplication application = ShadowApplication.getInstance();
        assertThat("Next activity should not started", application.getNextStartedActivity(), is(nullValue()));
        assertThat("Show error for Email field ", emailView.getError(), is(notNullValue()));
        assertThat("Show error for Password field ", passwordView.getError(), is(notNullValue()));
    }

更多场景还需探索。。。

与espresso的对比

Google 官方提供的一个易于测试 Android UI 的开源框架 , 于2013年10月推出它的 released 版本 , 目前最新版本已更新到2.x . 并且在AndroidStudio 2.2 预览版中已经默认集成该测试库 。

ViewMatchers - 在当前View层级去匹配指定的View .

ViewActions - 执行Views的某些行为,如点击事件 .

ViewAssertions - 检查Views的某些状态,如是否显示 .

@RunWith(AndroidJUnit4.class)
public class LoginUITest {
    @Rule
    public ActivityTestRule rule=new ActivityTestRule(LogingActivity.class,true);
    @Test
    public void login(){
        //login
        onView(withId(R.id.userName)).perform(typeText("Jack"),closeSoftKeyboard());
        onView(withId(R.id.password)).perform(typeText("1234"),closeSoftKeyboard());
        onView(withText("登录")).perform(click());
        //verify
        onView(withId(R.id.content)).check(matches(isDisplayed()));
    }
}

espresso更偏向于自动化测试,集成后执行单元测试需要跑在Android手机上,其有个高级功能,根据你的点击轨迹,自动生成自动测试代码。

3. 覆盖率

 jacoco:Android Studio自带的生成单元测试覆盖率报告的工具。

行覆盖率:度量被测程序的每行代码是否被执行,判断标准行中是否至少有一个指令被执行。

类覆盖率:度量计算class类文件是否被执行。

分支覆盖率:度量if和switch语句的分支覆盖情况,计算一个方法里面的

总分支数,确定执行和不执行的 分支数量。

方法覆盖率:度量被测程序的方法执行情况,是否执行取决于方法中是否有至少一个指令被执行。

指令覆盖:计数单元是单个java二进制代码指令,指令覆盖率提供了代码是否被执行的信息,度量完全 独立源码格式。

圈复杂度:在(线性)组合中,计算在一个方法里面所有可能路径的最小数目,缺失的复杂度同样表示测试案例没有完全覆盖到这个模块。

 参考自:http://blog.csdn.net/tmq1225/article/details/52221187

集成配置:

apply plugin: 'jacoco'
android {

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
        debug{
            testCoverageEnabled true
        }
    }
}
jacoco {
    toolVersion = "0.7.5.201505241946"
}

jacoco覆盖率报告分为两种:

   1. 只生成java层代码覆盖率报告

   2. 在运行app期间执行的覆盖率报告
task jacocoTestReport(type:JacocoReport, dependsOn: "testDebugUnitTest") {
    println("=========jacocoTestReport start");
    group = "Reporting"
    description = "Generate Jacoco coverage reports"
    classDirectories = fileTree(
            dir: "${project.buildDir}/intermediates/classes/debug",
            excludes: ['**/R.class',
                       '**/R$*.class',
                       '**/*$ViewInjector*.*',
                       '**/BuildConfig.*',
                       '**/Manifest*.*']
    )
    println("path==========>>" + "${project.buildDir}/intermediates/classes/debug")
    def coverageSourceDirs = "${project.projectDir}/src/main/java"
    println("coverageSourceDirs==========>>" + coverageSourceDirs)
    additionalSourceDirs = files(coverageSourceDirs)
    sourceDirectories = files(coverageSourceDirs)
    executionData = fileTree(dir: project.projectDir, includes:['**/*.exec', '**/*.ec'])
    reports {
        xml.enabled = true
        html.enabled = true
    }
}

task jacocoAndroidTestReport(type:JacocoReport,dependsOn:"connectedAndroidTest"){
    group = "Reporting"
    description = "Generate Jacoco coverage reports after running tests."
    reports{
        xml.enabled = true
        html.enabled = true
        csv.enabled = false
    }
    classDirectories = fileTree(
            dir : "$buildDir/intermediates/classes/debug",
            excludes : [
                    '**/*Test.class',
                    '**/R.class',
                    '**/R$*.class',
                    '**/BuildConfig.*',
                    '**/Manifest*.*'
            ]
    )
    def coverageSourceDirs = ['src/main/java']
    additionalSourceDirs = files(coverageSourceDirs)
    sourceDirectories = files(coverageSourceDirs)
    additionalClassDirs = files(coverageSourceDirs)
    executionData = files("$buildDir/outputs/code-coverage/connected/coverage.ec")
}

结果展示

jekins单元测试覆盖率报告

这里写图片描述

总结

单元测试的一些原则

  1. 在最低的功能/参数上验证程序的正确性
  2. 单元测试过后,机器状态保持不变。(数据库,sp等)
  3. 单元测试应该产生可重复、一致的结果。(尽量减少一切随机)
  4. 独立性,单元测试的运行/通过/失败不依赖于别的测试,可以人为构造数据,以保持单元测试的独立性
  5. 单元测试应该覆盖所有代码路径,包括错误处理路径,为了保证单元测试的代码覆盖率,单元测试必须测试公开的和私有的函数/方法
  6. 单元测试必须和产品代码一起保存和维护。
  7. 单元测试要快(一个测试运行时间是几秒钟,而不是几分钟)
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值