Android单元测试方案

作者: 海波笔记 | 来源:发表于2016-08-24 16:27 被阅读1190次

    @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的组合方案。但技术和框架都只是一方面,真正需要推动的是培养开发人员单元测试的意识。对于一个单元测试做得足够好的项目,是不需要担心质量问题的,测试人员应该只需要做质量验收即可。

    相关文章

      网友评论

        本文标题:Android单元测试方案

        本文链接:https://www.haomeiwen.com/subject/zfnysttx.html