Android单元测试方案

@Author:彭海波

前言

单元测试(又称为模块测试, Unit Testing)是针对程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法。
单元测试不仅仅用来保证当前代码的正确性,更重要的是用来保证代码修复、改进或重构之后的正确性。但是单元测试并不一定保证程序功能是正确的,更不保证整体业务是准备的。
在现代软件工程中,单元测试已经是软件开发不可或缺的一部分。良好的单元测试技术对软件开发至关重要,可以说它是软件质量的第一关,是软件开发者对软件质量做出的承诺。敏捷开发中尤其强调单元测试的重要性。

单元测试框架

Junit框架

android中的测试框架是扩展的junit,所以在学习android中的单元测试签,可以先去Junit的官方网站熟悉Junit的使用。目前主流的有JUnit3和JUnit4。JUnit3中,测试用例需要继承TestCase类。JUnit4中,测试用例无需继承TestCase类,只需要使用@Test等注解。
使用之前要在工程中加入Junit的依赖,以Gradle build方式为例:

testCompile 'junit:junit:4.10'

下面是一个Junit4的实例:

import org.junit.After;  
import org.junit.AfterClass;  
import org.junit.Assert;  
import org.junit.Before;  
import org.junit.BeforeClass;  
import org.junit.Ignore;  
import org.junit.Test;  
   
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");  
    }  
   
} 

Android单元测试框架

Android单元测试的框架关系如下:
cmd-markdown-logo

从上图的类关系图中可以知道,通过android测试类可以实现对android中相关重要的组件进行测试(如Activity,Service,ContentProvider,甚至是application)。

JUnit TestCase类

继承自JUnit的TestCase,不能使用Instrumentation框架。但这些类包含访问系统对象(如Context)的方法。使用Context,你可以浏览资源,文件,数据库等等。基类是AndroidTestCase,一般常见的是它的子类,和特定组件关联。
子类有:

  • ApplicationTestCase——测试整个应用程序的类。它允许你注入一个模拟的Context到应用程序中,在应用程序启动之前初始化测试参数,并在应用程序结束之后销毁之前检查应用程序。
  • ProviderTestCase2——测试单个ContentProvider的类。因为它要求使用MockContentResolver,并注入一个IsolatedContext,因此Provider的测试是与OS孤立的。
  • ServiceTestCase——测试单个Service的类。你可以注入一个模拟的Context或模拟的Application(或者两者),或者让Android为你提供Context和MockApplication。

Instrumentation TestCase类

继承自JUnit TestCase类,并可以使用Instrumentation框架,用于测试Activity。使用Instrumentation,Android可以向程序发送事件来自动进行UI测试,并可以精确控制Activity的启动,监测Activity生命周期的状态。
基类是InstrumentationTestCase。它的所有子类都能发送按键或触摸事件给UI。子类还可以注入一个模拟的Intent。
子类有:

  • ActivityTestCase——Activity测试类的基类。
  • SingleLaunchActivityTestCase——测试单个Activity的类。它能触发一次setup()和tearDown(),而不是每个方法调用时都触发。如果你的测试方法都是针对同一个Activity的话,那就使用它吧。
  • SyncBaseInstrumentation——测试Content Provider同步性的类。它使用Instrumentation在启动测试同步性之前取消已经存在的同步对象。
  • ActivityUnitTestCase——对单个Activity进行单一测试的类。使用它,你可以注入模拟的Context或Application,或者两者。它用于对Activity进行单元测试。不同于其它的Instrumentation类,这个测试类不能注入模拟的Intent。
  • ActivityInstrumentationTestCase2——在正常的系统环境中测试单个Activity的类。你不能注入一个模拟的Context,但你可以注入一个模拟的Intent。另外,你还可以在UI线程(应用程序的主线程)运行测试方法,并且可以给应用程序UI发送按键及触摸事件。

测试代码示例

public class MainActivityTest extends ActivityInstrumentationTestCase2<MainActivity> {  
    private static final String TAG = "=== MainActivityTest";  
  
    private Instrumentation mInstrument;  
    private MainActivity mActivity;  
    private View mToLoginView;  
  
    public MainActivityTest() {  
        super("yuan.activity", MainActivity.class);  
    }  
  
    @Override  
    public void setUp() throws Exception {  
        super.setUp();  
        mInstrument = getInstrumentation();  
        // 启动被测试的Activity  
        mActivity = getActivity();  
        mToLoginView = mActivity.findViewById(yuan.activity.R.id.to_login);  
    }  
  
    public void testPreConditions() {  
        // 在执行测试之前,确保程序的重要对象已被初始化  
        assertTrue(mToLoginView != null);  
    }  
  
  
    //mInstrument.runOnMainSync(new Runnable() {  
    //  public void run() {  
    //      mToLoginView.requestFocus();  
    //      mToLoginView.performClick();  
    //  }  
    //});  
    @UiThreadTest  
    public void testToLogin() {  
        // @UiThreadTest注解使整个方法在UI线程上执行,等同于上面注解掉的代码  
        mToLoginView.requestFocus();  
        mToLoginView.performClick();  
    }  
  
    @Suppress  
    public void testNotCalled() {  
        // 使用了@Suppress注解的方法不会被测试  
        Log.i(TAG, "method 'testNotCalled' is called");  
    }  
  
    @Override  
    public void tearDown() throws Exception {  
        super.tearDown();  
    }  
}

Robolectric单元测试框架

Instrumentation 与 Roboletric 都是针对 Android 进行单元测试的框架,前者在执行 case 时候是以 Android JUnit 的方式运行,因此必须在真实的 Android 环境中运行(模拟器或者真机),而后者则是以 Java Junit 的方式运行,这里就脱离了对 Android 环境的依赖,而可以直接将 case 在 JVM 中运行,大赞~,因此很适合将 Roboletric 用于 Android 的测试驱动开发。
下面介绍用Robolectric框架进行单元测试的方法,假设我们有一个RobolectricDemo的Android工程,我们要对该工程进行单元测试。

配置Gradle

  • 配置 RoboletricDemo/build.gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:1.2.3'

        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
}

allprojects {
    repositories {
        maven { url "https://oss.sonatype.org/content/repositories/snapshots" }
        mavenLocal()
        mavenCentral()
    }
}

  • 配置 app/build.gradle
apply plugin: 'com.android.application'

android {
    compileSdkVersion 22
    buildToolsVersion "23.0.0 rc2"

    defaultConfig {
        applicationId "com.pingan.robolectricdemo"
        minSdkVersion 15
        targetSdkVersion 19
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.android.support:appcompat-v7:22.2.0'
    testCompile 'junit:junit:4.10'
    testCompile 'org.assertj:assertj-core:1.7.0'
    testCompile 'org.robolectric:robolectric:3.0'
    compile files('libs/AndroidHyperion_1.0.0_release.jar')
    testCompile 'com.squareup.okhttp:mockwebserver:2.4.0'
}
  • 配置 Build Variants

在 Build Variants 面板中选择 Unit Tests

配置 Build Variants
  • 完成添加依赖

打开 Gradle 面板,点击刷新按钮

完成添加依赖
  • 完成之后可以在看到成功添加的所有依赖
查看依赖

完成 Test Case

  • 重命名 app/src/androidTest 为 test,并且删除创建项目时自动生成的 ApplicationTest
  • 在 MainActivity 中快速创建测试类,选择 JUnit 4,会自动创建 MainActivityTest 至之前修改的 test 目录下
test case
  • 编写 Test Case,这里直接贴上测试代码,代码都相当简单
package com.pingan.robolectricdemo;

import android.test.InstrumentationTestCase;
import android.widget.Button;
import android.widget.TextView;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.annotation.Config;

/**
 * Created by hyper on 15/8/13.
 */
@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class MainActivityTest extends InstrumentationTestCase {

    //引用待测Activity
    private MainActivity mainActivity;

    //引用待测Activity中的TextView和Button
    private TextView textView;
    private Button button;

    @Before
    public void setUp() throws Exception{
        //获取待测Activity
        mainActivity = Robolectric.setupActivity(MainActivity.class);
        //初始化textView和button
        textView = (TextView)mainActivity.findViewById(R.id.textView);
        button = (Button)mainActivity.findViewById(R.id.button);
    }

    @After
    public void tearDown() throws Exception{
        
    }

    @Test
    public void testInit() throws Exception{
        assertNotNull(mainActivity);
        assertNotNull(textView);
        assertNotNull(button);

        //判断包名
        assertEquals("com.pingan.robolectricdemo",mainActivity.getPackageName());

        //判断textView默认显示的内容
        assertEquals("Hello world!", textView.getText().toString());
    }

    @Test
    public void testButton() throws Exception{
        //点击Button
        button.performClick();
        assertEquals("Hyper",textView.getText().toString());
        //assertNotNull(textView.getText());
    }

    @Test
    public void testFail() throws Exception{
        fail("This case failed");
    }

}

Run Test Case

  • 打开 Gradle 面板,在面板中执行测试
run case
  • 右键 MainActivityTest > Run 'MainActivityTest'
  • 在终端中运行 ./gradlew test

查看报告

执行完测试之后,会在 app/build/reports/tests/目录下生成相应地测试报告,使用浏览器打开

test report

Assert

  • Junit3和Junit4都提供了一个Assert类(虽然package不同,但是大致差不多)。Assert类中定义了很多静态方法来进行断言。列表如下:
  • assertTrue(String message, boolean condition) 要求condition == true
  • assertFalse(String message, boolean condition) 要求condition == false
  • fail(String message) 必然失败,同样要求代码不可达
  • assertEquals(String message, XXX expected,XXX actual) 要求expected.equals(actual)
  • assertArrayEquals(String message, XXX[] expecteds,XXX [] actuals) 要求expected.equalsArray(actual)
  • assertNotNull(String message, Object object) 要求object!=null
  • assertNull(String message, Object object) 要求object==null
  • assertSame(String message, Object expected, Object actual) 要求expected == actual
  • assertNotSame(String message, Object unexpected,Object actual) 要求expected != actual
  • assertThat(String reason, T actual, Matcher matcher) 要求matcher.matches(actual) == true

单元测试方案

Mock/Stub

Mock和Stub是两种测试代码功能的方法。Mock测重于对功能的模拟。Stub测重于对功能的测试重现。比如对于List接口,Mock会直接对List进行模拟,而Stub会新建一个实现了List的TestList,在其中编写测试的代码。
强烈建议优先选择Mock方式,因为Mock方式下,模拟代码与测试代码放在一起,易读性好,而且扩展性、灵活性都比Stub好。
比较流行的Mock有:

其中EasyMock和Mockito对于Java接口使用接口代理的方式来模拟,对于Java类使用继承的方式来模拟(也即会创建一个新的Class类)。Mockito支持spy方式,可以对实例进行模拟。但它们都不能对静态方法和final类进行模拟,powermock通过修改字节码来支持了此功能。

使用Mockito进行单元测试

介绍

Mockito是Google Code上的一个开源项目,Api相对于EasyMock更好友好。与EasyMock不同的是,Mockito没有录制过程,只需要在“运行测试代码”之前对接口进行Stub,也即设置方法的返回值或抛出的异常,然后直接运行测试代码,运行期间调用Mock的方法,会返回预先设置的返回值或抛出异常,最后再对测试代码进行验证。可以查看此文章了解两者的不同。
官方提供了很多样例,基本上包括了所有功能,可以去看看。

引入方法

在你的Gradle文件中加入下面的依赖:

repositories { jcenter() }
dependencies { testCompile "org.mockito:mockito-core:1.9.5" } 

示例

这里从官方样例中摘录几个典型的:

  • 验证调用行为
import static org.mockito.Mockito.*;  
   
//创建Mock  
List mockedList = mock(List.class);  
   
//使用Mock对象  
mockedList.add("one");  
mockedList.clear();  
   
//验证行为  
verify(mockedList).add("one");  
verify(mockedList).clear();
  • 对Mock对象进行Stub
//也可以Mock具体的类,而不仅仅是接口  
LinkedList mockedList = mock(LinkedList.class);  
   
//Stub  
when(mockedList.get(0)).thenReturn("first"); // 设置返回值  
when(mockedList.get(1)).thenThrow(new RuntimeException()); // 抛出异常  
   
//第一个会打印 "first"  
System.out.println(mockedList.get(0));  
   
//接下来会抛出runtime异常  
System.out.println(mockedList.get(1));  
   
//接下来会打印"null",这是因为没有stub get(999)  
System.out.println(mockedList.get(999));  
    
// 可以选择性地验证行为,比如只关心是否调用过get(0),而不关心是否调用过get(1)  
verify(mockedList).get(0);  

使用Mockito测试异步方法

package com.paic.hyperion.core.hfasynchttp.http;

import android.app.Application;
import android.test.ApplicationTestCase;

import org.apache.http.Header;
import org.junit.Before;
import org.junit.Test;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;

import static org.mockito.Matchers.any;
import static org.mockito.Mockito.doAnswer;

/**
 * <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
 */

public class ApplicationTest extends ApplicationTestCase<Application> {
    public ApplicationTest() {
        super(Application.class);
    }

    @Mock
    private AsyncHttpClient mockClient;

    @Before
    public void setUp(){
        MockitoAnnotations.initMocks(this);
        mockClient = new AsyncHttpClient();
    }
    public class ResponseHandler extends AsyncHttpResponseHandler {
        private byte[] receive_data;
        @Override
        public void onSuccess(int statusCode, Header[] headers, byte[] binaryData) {
            System.out.println("success");
            receive_data = binaryData;
        }

        @Override
        public void onFailure(int statusCode, Header[] headers, byte[] binaryData, Throwable error) {
            System.out.println("fail");
        }

        public String getResult(){
            return receive_data.toString();
        }

    }
    @Test
    private void test(){
        //assertFalse(true);
        String result = "hello,world";
        final int statusCode = 200;
        final Header[] headers ={};
        final byte[] binaryData = "hello,world".getBytes();
        String url = "https://www.baidu.com";
        doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocationOnMock) throws Throwable {
                ( (ResponseHandler) invocationOnMock.getArguments()[0]).onSuccess(statusCode,headers,binaryData);
                return null;
            }
        }).when(mockClient.get(url,any(ResponseHandler.class)));
        ResponseHandler handler = new ResponseHandler();
        mockClient.get(url,handler);
        assertEquals(handler.getResult(),result);
    }
}

使用MockWebServer模拟服务端

介绍

我们的Android应用程序经常要从后端获取数据来进行相关交互,但前端和后台的开发往往是分开进行的,如果后台没有开发完成,前端甚至都无法进行调试,这是很浪费时间的。我们这里引入一个Mockwebserver的库,他可以模拟一个服务,对HTTP和HTTPS的请求返回指定的数据,从而用来验证我们的应用程序是否达到预期效果。你可以确定你正在进行的测试都走的是完整的HTTP协议栈。你甚至可以从真正的web服务器复制HTTP响应来创建你的测试案例。甚至,你还可以在代码中生成比较难以重现的类似500的错误或缓慢的加载响应的情况。详细内容可以参考Github上mockwebserver的介绍

使用方法

我们可以像使用Mockito一样来使用MockWebServer,使用步骤如下:

  • 在你的gradle文件中加入
testCompile 'com.squareup.okhttp:mockwebserver:2.4.0'
  • 设计Mock脚本
  • 运行你的应用程序
  • 验证返回结果

示例

public void test() throws Exception {
  // Create a MockWebServer. These are lean enough that you can create a new
  // instance for every unit test.
  MockWebServer server = new MockWebServer();

  // Schedule some responses.
  server.enqueue(new MockResponse().setBody("hello, world!"));
  server.enqueue(new MockResponse().setBody("sup, bra?"));
  server.enqueue(new MockResponse().setBody("yo dog"));

  // Start the server.
  server.start();

  // Ask the server for its URL. You'll need this to make HTTP requests.
  URL baseUrl = server.getUrl("/v1/chat/");

  // Exercise your application code, which should make those HTTP requests.
  // Responses are returned in the same order that they are enqueued.
  Chat chat = new Chat(baseUrl);

  chat.loadMore();
  assertEquals("hello, world!", chat.messages());

  chat.loadMore();
  chat.loadMore();
  assertEquals(""
      + "hello, world!\n"
      + "sup, bra?\n"
      + "yo dog", chat.messages());

  // Optional: confirm that your app made the HTTP requests you were expecting.
  RecordedRequest request1 = server.takeRequest();
  assertEquals("/v1/chat/messages/", request1.getPath());
  assertNotNull(request1.getHeader("Authorization"));

  RecordedRequest request2 = server.takeRequest();
  assertEquals("/v1/chat/messages/2", request2.getPath());

  RecordedRequest request3 = server.takeRequest();
  assertEquals("/v1/chat/messages/3", request3.getPath());

  // Shut down the server. Instances cannot be reused.
  server.shutdown();
}

MockResponse

Mock默认返回一个空的response body和一个200的状态码,你可以自定义body的内容(可以是字符串,数组,json等),你还可以通过fluent builder API来对你的响应添加headers

MockResponse response = new MockResponse()
    .addHeader("Content-Type", "application/json; charset=utf-8")
    .addHeader("Cache-Control", "no-cache")
    .setBody("{}");

MockResponse还可以用来模拟慢速网络,这样你能通过设置延迟来测试超时或者弱网

response.throttleBody(1024, 1, TimeUnit.SECONDS);

RecordedRequest

我们可以通过RecordedRequest来检查发送过来的请求的method, path, HTTP version, body, 和headers。

RecordedRequest request = server.takeRequest();
assertEquals("POST /v1/chat/send HTTP/1.1", request.getRequestLine());
assertEquals("application/json; charset=utf-8", request.getHeader("Content-Type"));
assertEquals("{}", request.getUtf8Body());

Dispatcher

默认情况下,MockWebServer使用队列的方式来处理请求,但我们还有另外一种方式来处理请求,就是使用Dispatcher,它根据请求路径来过滤并分发响应结果。

final Dispatcher dispatcher = new Dispatcher() {
    @Override
    public MockResponse dispatch(RecordedRequest request) throws InterruptedException {
        if (request.getPath().equals("/v1/login/auth/")){
            return new MockResponse().setResponseCode(200);
        } else if (request.getPath().equals("v1/check/version/")){
            return new MockResponse().setResponseCode(200).setBody("version=9");
        } else if (request.getPath().equals("/v1/profile/info")) {
            return new MockResponse().setResponseCode(200).setBody("{\\\"info\\\":{\\\"name\":\"Lucas Albuquerque\",\"age\":\"21\",\"gender\":\"male\"}}");
        }
        return new MockResponse().setResponseCode(404);
    }
};
server.setDispatcher(dispatcher);

使用DBUnit进行数据库单元测试

简介

DbUnit 是专门针对数据库测试的对JUnit的一个扩展,它可以将测试对象数据库置于一个测试轮回之间的状态。熟悉单元测试的开发人员都知道,在对数据库进行单元测试时候,通常采用的方案有运用模拟对象(mock objects) 和stubs 两种。通过隔离关联的数据库访问类,比如JDBC 的相关操作类,来达到对数据库操作的模拟测试。然而某些特殊的系统,比如利用了EJB 的CMP(container-managed persistence) 的系统,数据库的访问对象是在最底层而且很隐蔽的,那么这两种解决方案对这些系统就显得力不从心了。
DBUnit的设计理念就是在测试之前,备份数据库,然后给对象数据库植入我们需要的准备数据,最后,在测试完毕后,读入备份数据库,回溯到测试前的状态;而且又因为DBUnit 是对JUnit 的一种扩展,开发人员可以通过创建测试用例代码,在这些测试用例的生命周期内来对数据库的操作结果进行比较。

DbUnit 测试基本概念和流程

基于DbUnit 的测试的主要接口是IDataSet 。IDataSet 代表一个或多个表的数据。
可以将数据库模式的全部内容表示为单个IDataSet 实例。这些表本身由Itable 实例来表示。
IDataSet 的实现有很多,每一个都对应一个不同的数据源或加载机制。最常用的几种 IDataSet 实现为:

  • FlatXmlDataSet :数据的简单平面文件 XML 表示
  • QueryDataSet :用 SQL 查询获得的数据
  • DatabaseDataSet :数据库表本身内容的一种表示
  • XlsDataSet :数据的excel 表示
    一般而言,使用DbUnit 进行单元测试的流程如下:
  • 根据业务,做好测试用的准备数据和预想结果数据,通常准备成xml 格式文件。
  • 在setUp() 方法里边备份数据库中的关联表。
  • 在setUp() 方法里边读入准备数据。
  • 对测试类的对应测试方法进行实装: 执行对象方法,把数据库的实际执行结果和预想结果进行比较。
  • 在tearDown() 方法里边, 把数据库还原到测试前状态。

DbUnit 开发实例

下面通过一个实例来说明DbUnit 的实际运用。
比如有一个学生表[student] ,结构如下:

id char(4) pk 学号
name char(50) 姓名
sex char(1) 性别
birthday date 出生日期

1 准备数据如下:

id name sex birthday
0001 翁仔 m 1979-12-31
0002 王翠花 f 1982-08-09

测试对象类为StudentOpe.java ,里边有2 个方法:
findStudent(String id) : 根据主键id 找记录
addStudent(Student student) :添加一条记录
在测试addStudent 方法时候,我们准备添加如下一条数据

id name sex birthday
0088 王耳朵 m 1982-01-01

那么在执行该方法后,数据库的student 表里的数据是这样的:

id name sex birthday
0001 翁仔 m 1979-12-31
0002 王翠花 f 1982-08-09
0088 王耳朵 m 1982-01-01

然后我们说明如何对这2 个方法进行单元测试。
实例展开
1 把准备数据和预想数据转换成xml 文件
student_pre.xml

<?xml version='1.0' encoding="gb2312"?>
<dataset>
<student id="0001" name=" 翁仔" sex="m" birthday="1979-12-31"/>
<student id="0002" name=" 王翠花" sex="f" birthday="1982-08-09"/>
</dataset>

student_exp.xml

<?xml version='1.0' encoding="gb2312"?>
<dataset>
<student id="0001" name=" 翁仔" sex="m" birthday="1979-12-31"/>
<student id="0002" name=" 王翠花" sex="f" birthday="1982-08-09"/>
<student id="0088" name=" 王耳朵" sex="m" birthday="1982-01-01"/>
</dataset

2 实装setUp 方法,详细见代码注释。

protected void setUp() {
    IDatabaseConnection connection =null;
    try{
        super.setUp();
        // 本例使用postgresql 数据库
        Class.forName("org.postgresql.Driver");
        // 连接DB
        Connection conn=DriverManager.getConnection("jdbc:postgresql:testdb.test","postgres","postgres");
        // 获得DB 连接
        connection =new DatabaseConnection(conn);
        // 对数据库中的操作对象表student 进行备份
        QueryDataSet backupDataSet = new QueryDataSet(connection);
        backupDataSet.addTable("student");
        file=File.createTempFile("student_back",".xml");// 备份文件
        FlatXmlDataSet.write(backupDataSet,new FileOutputStream(file));
        // 准备数据的读入
        IDataSet dataSet = new FlatXmlDataSet( new FileInputStream("student_pre.xml"));
        DatabaseOperation.CLEAN_INSERT.execute(connection,dataSet);
    }catch(Exception e){
        e.printStackTrace();
    }finally{
        try{
            if(connection!=null) connection.close();
        }catch(SQLException e){}
    }
}

3 实装测试方法,详细见代码注释。

  • 检索类方法,可以利用assertEquals() 方法,拿表的字段进行比较。
// findStudent 
public void testFindStudent() throws Exception{
// 执行findStudent 方法
    StudentOpe studentOpe=new StudentOpe();
    Student result = studentOpe.findStudent("0001");
// 预想结果和实际结果的比较
    assertEquals(" 翁仔",result.getName());
    assertEquals("m",result.getSex());
    assertEquals("1979-12-31",result.getBirthDay());
}
  • 更新,添加,删除等方法,可以利用Assertion.assertEquals() 方法,拿表的整体来比较。
public void testAddStudent() throws Exception{
// 执行addStudent 方法
    StudentOpe studentOpe=new StudentOpe();
// 被追加的记录
    Student newStudent = new Student("0088"," 王耳朵","m","1982-01-01");
// 执行追加方法 
    Student result = studentOpe.addStudent(newStudent);
// 预想结果和实际结果的比较
    IDatabaseConnection connection=null;
    try{
// 预期结果取得
        IDataSet expectedDataSet = new FlatXmlDataSet(new FileInputStream("student_exp.xml"));
        ITable expectedTable = expectedDataSet.getTable("student");
// 实际结果取得
        Connection conn=getConnection();
        connection =new DatabaseConnection(conn);
        IDataSet databaseDataSet = connection.createDataSet();
        ITable actualTable = databaseDataSet.getTable("student");
// 比较
        Assertion.assertEquals(expectedTable, actualTable);
    }finally{
        if(connection!=null) connection.close();
    }
}
  • 如果在整体比较表的时候,有个别字段不需要比较,可以用DefaultColumnFilter.excludedColumnsTable() 方法,
    将指定字段给排除在比较范围之外。比如上例中不需要比较birthday 这个字段的话,那么可以如下代码所示进行处理:
ITable filteredExpectedTable = DefaultColumnFilter.excludedColumnsTable(expectedTable, new String[]{"birthday"});
ITable filteredActualTable = DefaultColumnFilter.excludedColumnsTable(actualTable,new String[]{"birthday"});
Assertion.assertEquals(filteredExpectedTable, filteredActualTable);

4 在tearDown() 方法里边, 把数据库还原到测试前状态

protected void tearDown() throws Exception{
    IDatabaseConnection connection =null;
    try{
        super.tearDown();
        Connection conn=getConnection();
        connection =new DatabaseConnection(conn);
        IDataSet dataSet = new FlatXmlDataSet(file);
        DatabaseOperation.CLEAN_INSERT.execute(connection,dataSet);
    }catch(Exception e){
        e.printStackTrace();
    }finally{
        try{
            if(connection!=null) connection.close();
        }catch(SQLException e){}
    }
}

关于Android-async-http的单元测试

如果我们直接在AndroidTestCase中对异步请求的方法进行测试,会发现根本没有返回结果,测试就结束了。这是因为Android的单元测试根本不是在主线程跑的,但我们的异步请求创建的Handler并不是绑定到主线程,而是绑定到创建它的线程,即测试线程。这样,测试一结束,Handler也就释放,异步返回就的消息就找不到Handler了。为了解决这个问题,我們發現 ActivityTestCase 擁有一個可以在主線程運行的測試API:runTestOnUiThread,只要将测试代码放进去即可,示例如下:

public class ApplicationTest extends InstrumentationTestCase {

    private MockWebServer mServer;

    @Override
    public void setUp() throws Exception{
        mServer = new MockWebServer();
        mServer.play();
    }
    @Override
    public void tearDown() throws Exception{
        mServer.shutdown();
    }

    public void testHttp(){
        mServer.enqueue(new MockResponse().setResponseCode(200).setBody("hyper"));
        final StringBuilder strBuilder = new StringBuilder();
        final AsyncHttpClient client = new AsyncHttpClient();
        final CountDownLatch signal = new CountDownLatch(1);
        final String url = mServer.getUrl("/").toString();
        try {
            this.runTestOnUiThread(new Runnable() {
                @Override
                public void run() {
                    client.get(url, new AsyncHttpResponseHandler() {
                        @Override
                        public void onSuccess(int i, Header[] headers, byte[] bytes) {
                            strBuilder.append(new String(bytes));
                        }

                        @Override
                        public void onFailure(int i, Header[] headers, byte[] bytes, Throwable throwable) {

                        }

                        @Override
                        public void onFinish() {
                            signal.countDown();
                        }
                    });
                }
            });
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }

        try {
            signal.await(3000, TimeUnit.MILLISECONDS);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        assertEquals(strBuilder.toString(),"hyper");
    }
}

代码覆盖率

代码覆盖率的作用主要是用来查看执行完毕后,有哪些代码尚未覆盖到,未覆盖到的代码通常意味着未覆盖到的功能或场景,目前主流的Android覆盖率工具有开源软件Emma和Jacoco。

Emma

  • 第一步:把被测工程生成Ant build文件,andriod-app就是工程名
    android update project -p android-app
  • 第二步:将andriod测试工程也转换成ant工程,-m选项指定了测试工程对应的主andriod工程的位置,而android-test就是测试工程名:
android update test-project -m ../android-app -p android-test
  • 第三步:执行
    下面的命令,编译、执行单元测试、收集覆盖率:
ant clean emma debug install

Jacoco

JaCoCo(Java Code Coverage)是一种分析单元测试覆盖率的工具,使用它运行单元测试后,可以给出代码中哪些部分被单元测试测到,哪些部分没有没测到,并且给出整个项目的单元测试覆盖情况百分比,看上去一目了然。下面介绍一下如何在Android studio中配置Jacoco为单元测试执行覆盖率

在Gradle中加入Jacoco

在build.gradle文件中加入下面的配置项

apply plugin: 'jacoco'

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

执行Jacoco

先执行单元测试用例,然后执行Jacoco

./gradlew clean createDebugCoverageReport

查看覆盖率结果

查看结果
结果

总结

关于Android单元测试,个人还是比较推荐Robolectric+Mockito的组合方案。但技术和框架都只是一方面,真正需要推动的是培养开发人员单元测试的意识。对于一个单元测试做得足够好的项目,是不需要担心质量问题的,测试人员应该只需要做质量验收即可。

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

推荐阅读更多精彩内容