Android单元测试

一.基本介绍

背景:

目前处于高速迭代开发中的Android项目往往需要除黑盒测试外更加可靠的质量保障,这正是单元测试的用武之地。单元测试周期性对项目进行函数级别的测试,在良好的覆盖率下,能够持续维护代码逻辑,从而支持项目从容应对快速的版本更新。

正是由于测试在开发中的重要地位,才会在IT界刮起了 TDD 的旋风。TDD,也就是测试驱动开发模式。它旨在强调在开发功能代码之前,先编写测试代码。也就是说在明确要开发某个功能后,首先思考如何对这个功能进行测试,并完成测试代码的编写,然后编写相关的代码满足这些测试用例。然后循环进行添加其他功能,直到完成全部功能的开发。

二.Java 测试工具(框架)

1.JUnit(推荐使用JUnit4)
JUnit 在日常开发中还是很常用的,而且 Java 的各种 IDE (Eclipse、MyEclipse、IntelliJ IDEA)都集成了 JUnit 的组件。当然,自己添加插件也是很方便的。JUnit 框架是 Java 语言单元测试当前的一站式解决方案。这个框架值得称赞,因为它把测试驱动的开发思想介绍给 Java 开发人员并教给他们如何有效地编写单元测试。

2.TestNG
TestNG,即Testing Next Generation,下一代测试技术。是根据JUnit和NUnit思想,采用 jdk 的 annotation 技术来强化测试功能并借助XML 文件强化测试组织结构而构建的测试框架。TestNG 的强大之处还在于不仅可以用来做单元测试,还可以用来做集成测试。

重点介绍下JUnit4

JUnit是Java单元测试框架,已经在Eclipse中默认安装。目前主流的有JUnit3和JUnit4。JUnit3中,测试用例需要继承TestCase类。JUnit4中,测试用例无需继承TestCase类,只需要使用@Test等注解,建议使用JUnit4。

JUnit4通过注解的方式来识别测试方法。目前支持的主要注解有:

  • @BeforeClass 全局只会执行一次,而且是第一个运行
  • @Before 在测试方法运行之前运行
  • @Test 测试方法
  • @After 在测试方法运行之后允许
  • @AfterClass 全局只会执行一次,而且是最后一个运行
  • @Ignore 忽略此方法

@Before 该方法在每次测试方法调用前都会调用 @Test 说明了该方法需要测试 @BeforeClass 该方法在所有测试方法之前调用,只会被调用一次 @After 该方法在每次测试方法调用后都会调用 @AfterClass 该方法在所有测试方法之后调用,只会被调用一次 @Ignore 忽略该方法

三.单元测试范围

一般来说,单元测试任务包括

  1. 接口功能测试:用来保证接口功能的正确性。
  2. 局部数据结构测试(不常用):用来保证接口中的数据结构是正确的。 比如(1).变量有无初始值,(2).变量是否溢出.
  3. 边界条件测试
    (1).变量没有赋值(即为NULL)
    (2).变量是数值(或字符)
    -主要边界:最小值,最大值,无穷大(对于DOUBLE等)
    -溢出边界(期望异常或拒绝服务):最小值-1,最大值+1
    -临近边界:最小值+1,最大值-1
    (3). 变量是字符串
    -引用“字符变量”的边界
    -空字符串
    -对字符串长度应用“数值变量”的边界
    (4).变量是集合
    -空集合
    -对集合的大小应用“数值变量”的边界
    -调整次序:升序、降序
    (5). 变量有规律
    -比如对于Math.sqrt,给出n2-1,和n2+1的边界
    (6). 所有独立执行通路测试:保证每一条代码,每个分支都经过测试
    -代码覆盖率
    1>.语句覆盖:保证每一个语句都执行到了
    2>.判定覆盖(分支覆盖):保证每一个分支都执行到
    3>.条件覆盖:保证每一个条件都覆盖到true和false(即if、while中的条件语句)
    4>.路径覆盖:保证每一个路径都覆盖到

-相关软件 (Cobertura:语句覆盖)

  1. 各条错误处理通路测试:保证每一个异常都经过测试

如下是一个JUnit4的示例:

/**
 * Created by huanming on 17/3/13.
 */
public class Junit4TestCase {

    @BeforeClass
    public static void setUpBeforeClass() {
        System.out.println("Set up before class");
    }

    @Before
    public void setUp() throws Exception {
        System.out.println("Set up");
    }

    @Test
    public void testMathPow() {
        System.out.println("Test Math.pow");
        Assert.assertEquals(4.0, Math.pow(2.0, 2.0), 0.0);
    }

    @Test
    public void testMathMin() {
        System.out.println("Test Math.min");
        Assert.assertEquals(2.0, Math.min(2.0, 4.0), 0.0);
    }

    // 期望此方法抛出NullPointerException异常
    @Test(expected = NullPointerException.class)
    public void testException() {
        System.out.println("Test exception");
        Object obj = null;
        obj.toString();
    }

    // 忽略此测试方法
    @Ignore
    @Test
    public void testMathMax() {
        Assert.fail("没有实现");
    }
    // 使用“假设”来忽略测试方法
    @Test
    public void testAssume(){
        System.out.println("Test assume");
        // 当假设失败时,则会停止运行,但这并不会意味测试方法失败。
        Assume.assumeTrue(false);
        Assert.fail("没有实现");
    }

    @After
    public void tearDown() throws Exception {
        System.out.println("Tear down");
    }

    @AfterClass
    public static void tearDownAfterClass() {
        System.out.println("Tear down After class");
    }

}

运行结果:


屏幕快照 2017-03-17 下午2.26.04.png

四. 单元测试框架>Robolectric

参考文章:
http://robolectric.org
https://github.com/robolectric/robolectric
https://en.wikipedia.org/wiki/Unit_testing
https://github.com/square/okhttp/tree/master/mockwebserver

  1. 介绍
    (1). Robolectric 是一个开源的framework,他们的做法是通过实现一套JVM能运行的Android代码,然后在unit test运行的时候去截取android相关的代码调用,然后转到他们的他们实现的代码去执行这个调用的过程。
    举个例子说明一下,比如android里面有个类叫TextView,他们实现了一个类叫ShadowTextView。这个类基本上实现了TextView的所有公共接口,假设你在unit test里面写到
    String text = textView.getText().toString();。在这个unit test运行的时候,Robolectric会自动判断你调用了Android相关的代码textView.getText(),然后这个调用过程在底层截取了,转到ShadowTextViewgetText实现。而ShadowTextView是真正实现了getText这个方法的,所以这个过程便可以正常执行。
    (2). 除了实现Android里面的类的现有接口,Robolectric还做了另外一件事情,极大地方便了unit testing的工作。那就是他们给每个Shadow类额外增加了很多接口,可以读取对应的Android类的一些状态。比如我们知道ImageView有一个方法叫setImageResource(resourceId),然而并没有一个对应的getter方法叫getImageResourceId(),这样你是没有办法测试这个ImageView是不是显示了你想要的image。而在Robolectric实现的对应的ShadowImageView里面,则提供了getImageResourceId()这个接口。你可以用来测试它是不是正确的显示了你想要的Image.

  2. 环境配置
    Android单元测试依旧需要JUnit框架的支持,Robolectric只是提供了Android代码的运行环境。如果使用Robolectric 3.0,依赖配置如下:

testCompile 'junit:junit:4.12'
    testCompile('org.robolectric:robolectric:3.0') {
        exclude module: 'commons-logging'
    }

Gradle对Robolectric 2.4的支持并不像3.0这样好,但Robolectric 2.4所有的测试框架均在一个包里,如果使用Robolectric 2.4,则需要如下配置:

//这行配置在buildscript的dependencies中
classpath 'org.robolectric:robolectric-gradle-plugin:0.14.+'
apply plugin: 'robolectric'
androidTestCompile 'org.robolectric:robolectric:2.4'

需要注意:Android Studio小于2.0的版本,要支持单元测试需要设置“Build Variants”,路径是“View -->Tool Windows-->Build Variants”,然后设置为“Unit Tests”;当版本为2.0时,默认就支持。

屏幕快照 2017-03-19 下午1.47.32.png
                           图2 单元测试工程位置

如图1所示的绿色文件夹即是单元测试工程。这些代码能够检测目标代码的正确性,打包时单元测试的代码不会被编译进入APK中。

Robolectric最麻烦就是下载依赖! 由于我们生活在天朝,下载国外的依赖很慢,即使有了翻墙,效果也一般。

注意:第一次运行可能需要下载一些library,依赖库,可能需要花一点时间,这个跟unit test本身没关。

第二种方法:maven地址指向 阿里云的地址。
build.gradle

allprojects {
        repositories {
            //依赖库,阿里云地址
            maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' }
            jcenter()
        }
    }

具体原理参考: http://www.jianshu.com/p/a01628c3ea16

五.Robolectric使用介绍

Mock

参考文章:
http://www.open-open.com/lib/view/open1470724287040.html

配置:

testCompile 'org.mockito:mockito-core:1.9.5'

说白了就是打桩(Stub)或则模拟,当你调用一个不好在测试中创建的对象时,Mock框架为你模拟一个和真实对象类似的替身来完成相应的行为。
mock对象就是在调试期间用来作为真实对象的替代品。Mockito是Java中常见的Mock框架。

Robolectric在文档中声称:“No Mocking Frameworks Required”:对于Robolectric的另一种可选方法是使用mock框架,比如Mockito;或者模拟出Android SDK。虽然这是个有效的方法,但基本上是应用代码的反向实现。

Mockito虽然不能模拟final类、匿名类和Java基本类型;对于final方法和static方法,不能对其 when(…).thenReturn(…) 操作。另外mock对象,大多都需要植入到应用代码中,从而进行verify(...)操作;但应用代码中不一定有相应的set方法,如果要植入,就需要为了测试添加应用代码。

但是, Mockito + Powermock可以解决上述的问题。

示例:

@Implements(HttpClient.class)
public class ShadowHttpClient {

    protected static boolean isHandleError = false;
    protected static boolean isRaiseException = false;
    public static String lastRequestPath;
    public static String lastRequestData;
    public static List<String> allExecutedAction = new ArrayList<String>();
    public static List<String> allRequestData = new ArrayList<String>();
    private static ResponseObjectConvert converter;
    private static List<HttpResponseResult> responseResultList;
    private static int position = 0;

    @RealObject
    HttpClient httpClient;

    public void __constructor__(String host, int port, boolean isEncryptionEnabled) {

    }

    @Implementation
    public HttpResponseResult sendRequestGetResponse(String path, String request) {
        lastRequestPath = path;
        lastRequestData = request;
        allExecutedAction.add(path);
        allRequestData.add(request);
        if (isRaiseException) {
            throw new RuntimeException();
        }

        if (converter != null) {
            if (isHandleError) {
                setResponseResultList(asList(new HttpResponseResult(FAILED, converter.convertResponse(), null)));
            } else {
                setResponseResultList(asList(new HttpResponseResult(SUCCEEDED, converter.convertResponse(), null)));
            }
        }

        return responseResultList.get(position++);
    }

    @Implementation
    public HttpResponseResult getResponse(String path) {
        return sendRequestGetResponse(path,"");
    }

    public static void reset() {
        lastRequestPath = null;
        lastRequestData = null;
        allExecutedAction.clear();
        allRequestData.clear();

        ShadowHttpClient.converter = null;
        ShadowHttpClient.responseResultList = null;
        ShadowHttpClient.isHandleError = false;
        ShadowHttpClient.isRaiseException = false;
    }

    public static void setRaiseException(boolean isRaiseException) {
        ShadowHttpClient.isRaiseException = isRaiseException;
    }

    public static void setConverter(ResponseObjectConvert converter) {
        ShadowHttpClient.converter = converter;
    }

    public static void setHandleError(boolean handleError) {
        ShadowHttpClient.isHandleError = handleError;
    }

    public static void setResponseResultList(List<HttpResponseResult> responseResultList) {
        position = 0;
        ShadowHttpClient.responseResultList = responseResultList;
    }

    public interface ResponseObjectConvert {
        public String convertResponse();
    }

Mock写法介绍

对于一些依赖关系复杂的测试对象,可以采用Mock框架解除依赖,常用的有Mockito。例如Mock一个List类型的对象实例,可以采用如下方式:

List list = mock(List.class);   //mock得到一个对象,也可以用@mock注入一个对象

所得到的list对象实例便是List类型的实例,如果不采用mock,List其实只是个接口,我们需要构造或者借助ArrayList才能进行实例化。与Shadow不同,Mock构造的是一个虚拟的对象,用于解耦真实对象所需要的依赖。Mock得到的对象仅仅是具备测试对象的类型,并不是真实的对象,也就是并没有执行过真实对象的逻辑。
Mock也具备一些补充JUnit的验证函数,比如设置函数的执行结果,示例如下:

When(sample.dosomething()).thenReturn(someAction);
//when(一个函数执行).thenReturn(一个可替代真实函数的结果的返回值);
//上述代码是设置sample.dosomething()的返回值,当执行了sample.dosomething()这个函数时,
//就会得到someAction,从而解除了对真实的sample.dosomething()函数的依赖

上述代码为被测函数定义一个可替代真实函数的结果的返回值。当使用这个函数后,这个可验证的结果便会产生影响,从而代替函数的真实结果,这样便解除了对真实函数的依赖。
同时Mock框架也可以验证函数的执行次数,代码如下:

List list = mock(List.class);   //Mock得到一个对象
list.add(1);                    //执行一个函数
verify(list).add(1);            //验证这个函数的执行
verify(list,time(3)).add(1);    //验证这个函数的执行次数

在一些需要解除网络依赖的场景中,多使用Mock。比如对retrofit框架的网络依赖解除如下:

public class MockClient implements Client {
    @Override
    public Response execute(Request request) throws IOException {
        Uri uri = Uri.parse(request.getUrl());
        String responseString = "";
        if(uri.getPath().equals("/path/of/interest")) {
            responseString = "返回的json1";//这里是设置返回值
        } else {
            responseString = "返回的json2";
        }
        return new Response(request.getUrl(), 200, "nothing", Collections.EMPTY_LIST, new TypedByteArray("application/json", responseString.getBytes()));
    }
}
//MockClient使用方式如下:
RestAdapter.Builder builder = new RestAdapter.Builder();
builder.setClient(new MockClient());

这种方式下retrofit的response可以由单元测试编写者设置,而不来源于网络,从而解除了对网络环境的依赖。

Shadow
Robolectric的本质是在Java运行环境下,采用Shadow的方式对Android中的组件进行模拟测试,从而实现Android单元测试。对于一些Robolectirc暂不支持的组件,可以采用自定义Shadow的方式扩展Robolectric的功能。

Robolectric定义了大量的Shadow类,修改或者扩展了Android OS类的行为。当一个Android OS类被实例化,Robolectric会搜索相应的Shadow类;如果找到了,将创建与之关联的Shadow对象。Android OS方法每次被调用时,Robolectirc确保:如果存在,Shadow类中的相应方法先被调用,这样就有机会做测试相关逻辑。这种策略可运用于所有的方法,包括static和final方法。

@Implements(Point.class)
public class ShadowPoint {
  @RealObject private Point realPoint;
  ...
  public void __constructor__(int x, int y) {
    realPoint.x = x;
    realPoint.y = y;
  }
}

上述实例中,@Implements是声明Shadow的对象,@RealObject是获取一个Android 对象,constructor则是该Shadow的构造函数,Shadow还可以修改一些函数的功能,只需要在重载该函数的时候添加@Implementation,这种方式可以有效扩展Robolectric的功能。
Shadow是通过对真实的Android对象进行函数重载、初始化等方式对Android对象进行扩展,Shadow出来的对象的功能接近Android对象,可以看成是对Android对象一种修复。自定义的Shadow需要在config中声明,声明写法是@Config(shadows=ShadowPoint.class)。

常见Robolectric用法

Robolectric支持单元测试范围从Activity的跳转、Activity展示View(包括菜单)和Fragment到View的点击触摸以及事件响应,同时Robolectric也能测试Toast和Dialog。对于需要网络请求数据的测试,Robolectric可以模拟网络请求的response。对于一些Robolectric不能测试的对象,比如ConcurrentTask,可以通过自定义Shadow的方式现实测试。下面将着重介绍Robolectric的常见用法。

  1. Activity展示测试与跳转测试
    创建网络请求后,便可以测试Activity了。测试代码如下:
@Test
public void testSampleActivity(){
    SampleActivity sampleActivity=Robolectric.buildActivity(SampleActivity.class).
                create().resume().get();
    assertNotNull(sampleActivity);
    assertEquals("Activity的标题", sampleActivity.getTitle());
}

Robolectric.buildActivity()用于构造Activity,create()函数执行后,该Activity会运行到onCreate周期,resume()则对应onResume周期。assertNotNull和assertEquals是JUnit中的断言,Robolectric只提供运行环境,逻辑判断还是需要依赖JUnit中的断言。
Activity跳转是Android开发的重要逻辑,其测试方法如下:

 @Test
    public void testMainActivity() {
        MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);
        mainActivity.findViewById(R.id.textView1).performClick();

        Intent expectedIntent = new Intent(mainActivity, SecondActivity.class);
        ShadowActivity shadowActivity = Shadows.shadowOf(mainActivity);
        Intent actualIntent = shadowActivity.getNextStartedActivity();
        Assert.assertEquals(expectedIntent, actualIntent);
    }
  1. Dialog和Toast测试
    测试Dialog和Toast的方法如下:
public void testDialog(){
    Dialog dialog = ShadowDialog.getLatestDialog();
    assertNotNull(dialog);
}
public void testToast(String toastContent){
    ShadowHandler.idleMainLooper();
    assertEquals(toastContent, ShadowToast.getTextOfLatestToast());
}

上述函数均需要在Dialog或Toast产生之后执行,能够测试Dialog和Toast是否弹出。

Fragment展示与切换
Fragment是Activity的一部分,在Robolectric模拟执行Activity过程中,如果触发了被测试的代码中的Fragment添加逻辑,Fragment会被添加到Activity中。
需要注意Fragment出现的时机,如果目标Activity中的Fragment的添加是执行在onResume阶段,在Activity被Robolectric执行resume()阶段前,该Activity中并不会出现该Fragment。采用Robolectric主动添加Fragment的方法如下:

@Test
public void addfragment(Activity activity, int fragmentContent){
    FragmentTestUtil.startFragment(activity.getSupportFragmentManager().findFragmentById(fragmentContent));
    Fragment fragment = activity.getSupportFragmentManager().findFragmentById(fragmentContent);
    assertNotNull(fragment);
}

startFragment()函数的主体便是常用的添加fragment的代码。切换一个Fragment往往由Activity中的代码逻辑完成,需要Activity的引用。
控件的点击以及可视验证

@Test
public void testButtonClick(int buttonID){
    Button submitButton = (Button) activity.findViewById(buttonID);
    assertTrue(submitButton.isEnabled());
    submitButton.performClick();
    //验证控件的行为
}

对控件的点击验证是调用performClick(),然后断言验证其行为。对于ListView这类涉及到Adapter的控件的点击验证,写法如下:

//listView被展示之后

listView.performItemClick(listView.getAdapter().getView(position, null, null), 0, 0);

与button等控件稍有不同。

六.Robolectric单元测试编写结构

如下实例:


未完待续......

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,271评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,275评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,151评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,550评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,553评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,559评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,924评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,580评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,826评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,578评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,661评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,363评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,940评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,926评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,156评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,872评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,391评论 2 342

推荐阅读更多精彩内容

  • Android单元测试介绍 处于高速迭代开发中的Android项目往往需要除黑盒测试外更加可靠的质量保障,这正是单...
    东经315度阅读 3,088评论 6 37
  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,389评论 25 707
  • 为什么要做单元测试 学习过或者了解软件工程的人一定对这个东西不陌生,很多人也知道这个东西很重要,但是总是以各种借口...
    DanieX阅读 621评论 0 3
  • 今天有客户问我:有没有听说过九型人格? 事实是听过,但没有太了解。大致就是将人的性格分成九种,然后让人对号入座等等...
    萍空间阅读 185评论 0 0
  • 三月二十五日,春分后第五天,气温回升、阴雨转晴。这日天缘茶室,有位新客到访——觅仙泉。 觅仙泉是源自大别山深层岩石...
    小金瓜阅读 831评论 0 3