Android單元測試方案

@Author:彭海波

前言

單元測試(又稱為模塊測試, Unit Testing)是針對程序模塊(軟件設(shè)計(jì)的最小單位)來進(jìn)行正確性檢驗(yàn)的測試工作。程序單元是應(yīng)用的最小可測試部件。在過程化編程中,一個單元就是單個程序、函數(shù)、過程等;對于面向?qū)ο缶幊?,最小單元就是方法,包括基類(超類)、抽象類、或者派生類(子類)中的方法?br> 單元測試不僅僅用來保證當(dāng)前代碼的正確性,更重要的是用來保證代碼修復(fù)、改進(jìn)或重構(gòu)之后的正確性。但是單元測試并不一定保證程序功能是正確的,更不保證整體業(yè)務(wù)是準(zhǔn)備的。
在現(xiàn)代軟件工程中,單元測試已經(jīng)是軟件開發(fā)不可或缺的一部分。良好的單元測試技術(shù)對軟件開發(fā)至關(guān)重要,可以說它是軟件質(zhì)量的第一關(guān),是軟件開發(fā)者對軟件質(zhì)量做出的承諾。敏捷開發(fā)中尤其強(qiáng)調(diào)單元測試的重要性。

單元測試框架

Junit框架

android中的測試框架是擴(kuò)展的junit,所以在學(xué)習(xí)android中的單元測試簽,可以先去Junit的官方網(wǎng)站熟悉Junit的使用。目前主流的有JUnit3和JUnit4。JUnit3中,測試用例需要繼承TestCase類。JUnit4中,測試用例無需繼承TestCase類,只需要使用@Test等注解。
使用之前要在工程中加入Junit的依賴,以Gradle build方式為例:

testCompile 'junit:junit:4.10'

下面是一個Junit4的實(shí)例:

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("沒有實(shí)現(xiàn)");  
    }  
        // 使用“假設(shè)”來忽略測試方法  
    @Test  
    public void testAssume(){  
        System.out.println("Test assume");  
                // 當(dāng)假設(shè)失敗時(shí),則會停止運(yùn)行,但這并不會意味測試方法失敗。  
        Assume.assumeTrue(false);  
        Assert.fail("沒有實(shí)現(xiàn)");  
    }  
   
    @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單元測試的框架關(guān)系如下:
cmd-markdown-logo

從上圖的類關(guān)系圖中可以知道,通過android測試類可以實(shí)現(xiàn)對android中相關(guān)重要的組件進(jìn)行測試(如Activity,Service,ContentProvider,甚至是application)。

JUnit TestCase類

繼承自JUnit的TestCase,不能使用Instrumentation框架。但這些類包含訪問系統(tǒng)對象(如Context)的方法。使用Context,你可以瀏覽資源,文件,數(shù)據(jù)庫等等?;愂茿ndroidTestCase,一般常見的是它的子類,和特定組件關(guān)聯(lián)。
子類有:

  • ApplicationTestCase——測試整個應(yīng)用程序的類。它允許你注入一個模擬的Context到應(yīng)用程序中,在應(yīng)用程序啟動之前初始化測試參數(shù),并在應(yīng)用程序結(jié)束之后銷毀之前檢查應(yīng)用程序。
  • ProviderTestCase2——測試單個ContentProvider的類。因?yàn)樗笫褂肕ockContentResolver,并注入一個IsolatedContext,因此Provider的測試是與OS孤立的。
  • ServiceTestCase——測試單個Service的類。你可以注入一個模擬的Context或模擬的Application(或者兩者),或者讓Android為你提供Context和MockApplication。

Instrumentation TestCase類

繼承自JUnit TestCase類,并可以使用Instrumentation框架,用于測試Activity。使用Instrumentation,Android可以向程序發(fā)送事件來自動進(jìn)行UI測試,并可以精確控制Activity的啟動,監(jiān)測Activity生命周期的狀態(tài)。
基類是InstrumentationTestCase。它的所有子類都能發(fā)送按鍵或觸摸事件給UI。子類還可以注入一個模擬的Intent。
子類有:

  • ActivityTestCase——Activity測試類的基類。
  • SingleLaunchActivityTestCase——測試單個Activity的類。它能觸發(fā)一次setup()和tearDown(),而不是每個方法調(diào)用時(shí)都觸發(fā)。如果你的測試方法都是針對同一個Activity的話,那就使用它吧。
  • SyncBaseInstrumentation——測試Content Provider同步性的類。它使用Instrumentation在啟動測試同步性之前取消已經(jīng)存在的同步對象。
  • ActivityUnitTestCase——對單個Activity進(jìn)行單一測試的類。使用它,你可以注入模擬的Context或Application,或者兩者。它用于對Activity進(jìn)行單元測試。不同于其它的Instrumentation類,這個測試類不能注入模擬的Intent。
  • ActivityInstrumentationTestCase2——在正常的系統(tǒng)環(huán)境中測試單個Activity的類。你不能注入一個模擬的Context,但你可以注入一個模擬的Intent。另外,你還可以在UI線程(應(yīng)用程序的主線程)運(yùn)行測試方法,并且可以給應(yīng)用程序UI發(fā)送按鍵及觸摸事件。

測試代碼示例

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() {  
        // 在執(zhí)行測試之前,確保程序的重要對象已被初始化  
        assertTrue(mToLoginView != null);  
    }  
  
  
    //mInstrument.runOnMainSync(new Runnable() {  
    //  public void run() {  
    //      mToLoginView.requestFocus();  
    //      mToLoginView.performClick();  
    //  }  
    //});  
    @UiThreadTest  
    public void testToLogin() {  
        // @UiThreadTest注解使整個方法在UI線程上執(zhí)行,等同于上面注解掉的代碼  
        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 進(jìn)行單元測試的框架,前者在執(zhí)行 case 時(shí)候是以 Android JUnit 的方式運(yùn)行,因此必須在真實(shí)的 Android 環(huán)境中運(yùn)行(模擬器或者真機(jī)),而后者則是以 Java Junit 的方式運(yùn)行,這里就脫離了對 Android 環(huán)境的依賴,而可以直接將 case 在 JVM 中運(yùn)行,大贊~,因此很適合將 Roboletric 用于 Android 的測試驅(qū)動開發(fā)。
下面介紹用Robolectric框架進(jìn)行單元測試的方法,假設(shè)我們有一個RobolectricDemo的Android工程,我們要對該工程進(jìn)行單元測試。

配置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 面板,點(diǎn)擊刷新按鈕

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

完成 Test Case

  • 重命名 app/src/androidTest 為 test,并且刪除創(chuàng)建項(xiàng)目時(shí)自動生成的 ApplicationTest
  • 在 MainActivity 中快速創(chuàng)建測試類,選擇 JUnit 4,會自動創(chuàng)建 MainActivityTest 至之前修改的 test 目錄下
test case
  • 編寫 Test Case,這里直接貼上測試代碼,代碼都相當(dāng)簡單
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默認(rèn)顯示的內(nèi)容
        assertEquals("Hello world!", textView.getText().toString());
    }

    @Test
    public void testButton() throws Exception{
        //點(diǎn)擊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 面板,在面板中執(zhí)行測試
run case
  • 右鍵 MainActivityTest > Run 'MainActivityTest'
  • 在終端中運(yùn)行 ./gradlew test

查看報(bào)告

執(zhí)行完測試之后,會在 app/build/reports/tests/目錄下生成相應(yīng)地測試報(bào)告,使用瀏覽器打開

test report

Assert

  • Junit3和Junit4都提供了一個Assert類(雖然package不同,但是大致差不多)。Assert類中定義了很多靜態(tài)方法來進(jìn)行斷言。列表如下:
  • assertTrue(String message, boolean condition) 要求condition == true
  • assertFalse(String message, boolean condition) 要求condition == false
  • fail(String message) 必然失敗,同樣要求代碼不可達(dá)
  • 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測重于對功能的測試重現(xiàn)。比如對于List接口,Mock會直接對List進(jìn)行模擬,而Stub會新建一個實(shí)現(xiàn)了List的TestList,在其中編寫測試的代碼。
強(qiáng)烈建議優(yōu)先選擇Mock方式,因?yàn)镸ock方式下,模擬代碼與測試代碼放在一起,易讀性好,而且擴(kuò)展性、靈活性都比Stub好。
比較流行的Mock有:

其中EasyMock和Mockito對于Java接口使用接口代理的方式來模擬,對于Java類使用繼承的方式來模擬(也即會創(chuàng)建一個新的Class類)。Mockito支持spy方式,可以對實(shí)例進(jìn)行模擬。但它們都不能對靜態(tài)方法和final類進(jìn)行模擬,powermock通過修改字節(jié)碼來支持了此功能。

使用Mockito進(jìn)行單元測試

介紹

Mockito是Google Code上的一個開源項(xiàng)目,Api相對于EasyMock更好友好。與EasyMock不同的是,Mockito沒有錄制過程,只需要在“運(yùn)行測試代碼”之前對接口進(jìn)行Stub,也即設(shè)置方法的返回值或拋出的異常,然后直接運(yùn)行測試代碼,運(yùn)行期間調(diào)用Mock的方法,會返回預(yù)先設(shè)置的返回值或拋出異常,最后再對測試代碼進(jìn)行驗(yàn)證??梢圆榭?a target="_blank" rel="nofollow">此文章了解兩者的不同。
官方提供了很多樣例,基本上包括了所有功能,可以去看看。

引入方法

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

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

示例

這里從官方樣例中摘錄幾個典型的:

  • 驗(yàn)證調(diào)用行為
import static org.mockito.Mockito.*;  
   
//創(chuàng)建Mock  
List mockedList = mock(List.class);  
   
//使用Mock對象  
mockedList.add("one");  
mockedList.clear();  
   
//驗(yàn)證行為  
verify(mockedList).add("one");  
verify(mockedList).clear();
  • 對Mock對象進(jìn)行Stub
//也可以Mock具體的類,而不僅僅是接口  
LinkedList mockedList = mock(LinkedList.class);  
   
//Stub  
when(mockedList.get(0)).thenReturn("first"); // 設(shè)置返回值  
when(mockedList.get(1)).thenThrow(new RuntimeException()); // 拋出異常  
   
//第一個會打印 "first"  
System.out.println(mockedList.get(0));  
   
//接下來會拋出runtime異常  
System.out.println(mockedList.get(1));  
   
//接下來會打印"null",這是因?yàn)闆]有stub get(999)  
System.out.println(mockedList.get(999));  
    
// 可以選擇性地驗(yàn)證行為,比如只關(guān)心是否調(diào)用過get(0),而不關(guān)心是否調(diào)用過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 >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模擬服務(wù)端

介紹

我們的Android應(yīng)用程序經(jīng)常要從后端獲取數(shù)據(jù)來進(jìn)行相關(guān)交互,但前端和后臺的開發(fā)往往是分開進(jìn)行的,如果后臺沒有開發(fā)完成,前端甚至都無法進(jìn)行調(diào)試,這是很浪費(fèi)時(shí)間的。我們這里引入一個Mockwebserver的庫,他可以模擬一個服務(wù),對HTTP和HTTPS的請求返回指定的數(shù)據(jù),從而用來驗(yàn)證我們的應(yīng)用程序是否達(dá)到預(yù)期效果。你可以確定你正在進(jìn)行的測試都走的是完整的HTTP協(xié)議棧。你甚至可以從真正的web服務(wù)器復(fù)制HTTP響應(yīng)來創(chuàng)建你的測試案例。甚至,你還可以在代碼中生成比較難以重現(xiàn)的類似500的錯誤或緩慢的加載響應(yīng)的情況。詳細(xì)內(nèi)容可以參考Github上mockwebserver的介紹。

使用方法

我們可以像使用Mockito一樣來使用MockWebServer,使用步驟如下:

  • 在你的gradle文件中加入
testCompile 'com.squareup.okhttp:mockwebserver:2.4.0'
  • 設(shè)計(jì)Mock腳本
  • 運(yùn)行你的應(yīng)用程序
  • 驗(yàn)證返回結(jié)果

示例

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默認(rèn)返回一個空的response body和一個200的狀態(tài)碼,你可以自定義body的內(nèi)容(可以是字符串,數(shù)組,json等),你還可以通過fluent builder API來對你的響應(yīng)添加headers

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

MockResponse還可以用來模擬慢速網(wǎng)絡(luò),這樣你能通過設(shè)置延遲來測試超時(shí)或者弱網(wǎng)

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

RecordedRequest

我們可以通過RecordedRequest來檢查發(fā)送過來的請求的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

默認(rèn)情況下,MockWebServer使用隊(duì)列的方式來處理請求,但我們還有另外一種方式來處理請求,就是使用Dispatcher,它根據(jù)請求路徑來過濾并分發(fā)響應(yīng)結(jié)果。

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進(jìn)行數(shù)據(jù)庫單元測試

簡介

DbUnit 是專門針對數(shù)據(jù)庫測試的對JUnit的一個擴(kuò)展,它可以將測試對象數(shù)據(jù)庫置于一個測試輪回之間的狀態(tài)。熟悉單元測試的開發(fā)人員都知道,在對數(shù)據(jù)庫進(jìn)行單元測試時(shí)候,通常采用的方案有運(yùn)用模擬對象(mock objects) 和stubs 兩種。通過隔離關(guān)聯(lián)的數(shù)據(jù)庫訪問類,比如JDBC 的相關(guān)操作類,來達(dá)到對數(shù)據(jù)庫操作的模擬測試。然而某些特殊的系統(tǒng),比如利用了EJB 的CMP(container-managed persistence) 的系統(tǒng),數(shù)據(jù)庫的訪問對象是在最底層而且很隱蔽的,那么這兩種解決方案對這些系統(tǒng)就顯得力不從心了。
DBUnit的設(shè)計(jì)理念就是在測試之前,備份數(shù)據(jù)庫,然后給對象數(shù)據(jù)庫植入我們需要的準(zhǔn)備數(shù)據(jù),最后,在測試完畢后,讀入備份數(shù)據(jù)庫,回溯到測試前的狀態(tài);而且又因?yàn)镈BUnit 是對JUnit 的一種擴(kuò)展,開發(fā)人員可以通過創(chuàng)建測試用例代碼,在這些測試用例的生命周期內(nèi)來對數(shù)據(jù)庫的操作結(jié)果進(jìn)行比較。

DbUnit 測試基本概念和流程

基于DbUnit 的測試的主要接口是IDataSet 。IDataSet 代表一個或多個表的數(shù)據(jù)。
可以將數(shù)據(jù)庫模式的全部內(nèi)容表示為單個IDataSet 實(shí)例。這些表本身由Itable 實(shí)例來表示。
IDataSet 的實(shí)現(xiàn)有很多,每一個都對應(yīng)一個不同的數(shù)據(jù)源或加載機(jī)制。最常用的幾種 IDataSet 實(shí)現(xiàn)為:

  • FlatXmlDataSet :數(shù)據(jù)的簡單平面文件 XML 表示
  • QueryDataSet :用 SQL 查詢獲得的數(shù)據(jù)
  • DatabaseDataSet :數(shù)據(jù)庫表本身內(nèi)容的一種表示
  • XlsDataSet :數(shù)據(jù)的excel 表示
    一般而言,使用DbUnit 進(jìn)行單元測試的流程如下:
  • 根據(jù)業(yè)務(wù),做好測試用的準(zhǔn)備數(shù)據(jù)和預(yù)想結(jié)果數(shù)據(jù),通常準(zhǔn)備成xml 格式文件。
  • 在setUp() 方法里邊備份數(shù)據(jù)庫中的關(guān)聯(lián)表。
  • 在setUp() 方法里邊讀入準(zhǔn)備數(shù)據(jù)。
  • 對測試類的對應(yīng)測試方法進(jìn)行實(shí)裝: 執(zhí)行對象方法,把數(shù)據(jù)庫的實(shí)際執(zhí)行結(jié)果和預(yù)想結(jié)果進(jìn)行比較。
  • 在tearDown() 方法里邊, 把數(shù)據(jù)庫還原到測試前狀態(tài)。

DbUnit 開發(fā)實(shí)例

下面通過一個實(shí)例來說明DbUnit 的實(shí)際運(yùn)用。
比如有一個學(xué)生表[student] ,結(jié)構(gòu)如下:

id char(4) pk 學(xué)號
name char(50) 姓名
sex char(1) 性別
birthday date 出生日期

1 準(zhǔn)備數(shù)據(jù)如下:

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

測試對象類為StudentOpe.java ,里邊有2 個方法:
findStudent(String id) : 根據(jù)主鍵id 找記錄
addStudent(Student student) :添加一條記錄
在測試addStudent 方法時(shí)候,我們準(zhǔn)備添加如下一條數(shù)據(jù)

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

那么在執(zhí)行該方法后,數(shù)據(jù)庫的student 表里的數(shù)據(jù)是這樣的:

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

然后我們說明如何對這2 個方法進(jìn)行單元測試。
實(shí)例展開
1 把準(zhǔn)備數(shù)據(jù)和預(yù)想數(shù)據(jù)轉(zhuǎn)換成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 實(shí)裝setUp 方法,詳細(xì)見代碼注釋。

protected void setUp() {
    IDatabaseConnection connection =null;
    try{
        super.setUp();
        // 本例使用postgresql 數(shù)據(jù)庫
        Class.forName("org.postgresql.Driver");
        // 連接DB
        Connection conn=DriverManager.getConnection("jdbc:postgresql:testdb.test","postgres","postgres");
        // 獲得DB 連接
        connection =new DatabaseConnection(conn);
        // 對數(shù)據(jù)庫中的操作對象表student 進(jìn)行備份
        QueryDataSet backupDataSet = new QueryDataSet(connection);
        backupDataSet.addTable("student");
        file=File.createTempFile("student_back",".xml");// 備份文件
        FlatXmlDataSet.write(backupDataSet,new FileOutputStream(file));
        // 準(zhǔn)備數(shù)據(jù)的讀入
        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 實(shí)裝測試方法,詳細(xì)見代碼注釋。

  • 檢索類方法,可以利用assertEquals() 方法,拿表的字段進(jìn)行比較。
// findStudent 
public void testFindStudent() throws Exception{
// 執(zhí)行findStudent 方法
    StudentOpe studentOpe=new StudentOpe();
    Student result = studentOpe.findStudent("0001");
// 預(yù)想結(jié)果和實(shí)際結(jié)果的比較
    assertEquals(" 翁仔",result.getName());
    assertEquals("m",result.getSex());
    assertEquals("1979-12-31",result.getBirthDay());
}
  • 更新,添加,刪除等方法,可以利用Assertion.assertEquals() 方法,拿表的整體來比較。
public void testAddStudent() throws Exception{
// 執(zhí)行addStudent 方法
    StudentOpe studentOpe=new StudentOpe();
// 被追加的記錄
    Student newStudent = new Student("0088"," 王耳朵","m","1982-01-01");
// 執(zhí)行追加方法 
    Student result = studentOpe.addStudent(newStudent);
// 預(yù)想結(jié)果和實(shí)際結(jié)果的比較
    IDatabaseConnection connection=null;
    try{
// 預(yù)期結(jié)果取得
        IDataSet expectedDataSet = new FlatXmlDataSet(new FileInputStream("student_exp.xml"));
        ITable expectedTable = expectedDataSet.getTable("student");
// 實(shí)際結(jié)果取得
        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();
    }
}
  • 如果在整體比較表的時(shí)候,有個別字段不需要比較,可以用DefaultColumnFilter.excludedColumnsTable() 方法,
    將指定字段給排除在比較范圍之外。比如上例中不需要比較birthday 這個字段的話,那么可以如下代碼所示進(jìn)行處理:
ITable filteredExpectedTable = DefaultColumnFilter.excludedColumnsTable(expectedTable, new String[]{"birthday"});
ITable filteredActualTable = DefaultColumnFilter.excludedColumnsTable(actualTable,new String[]{"birthday"});
Assertion.assertEquals(filteredExpectedTable, filteredActualTable);

4 在tearDown() 方法里邊, 把數(shù)據(jù)庫還原到測試前狀態(tài)

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){}
    }
}

關(guān)于Android-async-http的單元測試

如果我們直接在AndroidTestCase中對異步請求的方法進(jìn)行測試,會發(fā)現(xiàn)根本沒有返回結(jié)果,測試就結(jié)束了。這是因?yàn)锳ndroid的單元測試根本不是在主線程跑的,但我們的異步請求創(chuàng)建的Handler并不是綁定到主線程,而是綁定到創(chuàng)建它的線程,即測試線程。這樣,測試一結(jié)束,Handler也就釋放,異步返回就的消息就找不到Handler了。為了解決這個問題,我們發(fā)現(xiàn) ActivityTestCase 擁有一個可以在主線程運(yùn)行的測試API:runTestOnUiThread,只要將測試代碼放進(jìn)去即可,示例如下:

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");
    }
}

代碼覆蓋率

代碼覆蓋率的作用主要是用來查看執(zhí)行完畢后,有哪些代碼尚未覆蓋到,未覆蓋到的代碼通常意味著未覆蓋到的功能或場景,目前主流的Android覆蓋率工具有開源軟件Emma和Jacoco。

Emma

  • 第一步:把被測工程生成Ant build文件,andriod-app就是工程名
    android update project -p android-app
  • 第二步:將andriod測試工程也轉(zhuǎn)換成ant工程,-m選項(xiàng)指定了測試工程對應(yīng)的主andriod工程的位置,而android-test就是測試工程名:
android update test-project -m ../android-app -p android-test
  • 第三步:執(zhí)行
    下面的命令,編譯、執(zhí)行單元測試、收集覆蓋率:
ant clean emma debug install

Jacoco

JaCoCo(Java Code Coverage)是一種分析單元測試覆蓋率的工具,使用它運(yùn)行單元測試后,可以給出代碼中哪些部分被單元測試測到,哪些部分沒有沒測到,并且給出整個項(xiàng)目的單元測試覆蓋情況百分比,看上去一目了然。下面介紹一下如何在Android studio中配置Jacoco為單元測試執(zhí)行覆蓋率

在Gradle中加入Jacoco

在build.gradle文件中加入下面的配置項(xiàng)

apply plugin: 'jacoco'

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

執(zhí)行Jacoco

先執(zhí)行單元測試用例,然后執(zhí)行Jacoco

./gradlew clean createDebugCoverageReport

查看覆蓋率結(jié)果

查看結(jié)果
結(jié)果

總結(jié)

關(guān)于Android單元測試,個人還是比較推薦Robolectric+Mockito的組合方案。但技術(shù)和框架都只是一方面,真正需要推動的是培養(yǎng)開發(fā)人員單元測試的意識。對于一個單元測試做得足夠好的項(xiàng)目,是不需要擔(dān)心質(zhì)量問題的,測試人員應(yīng)該只需要做質(zhì)量驗(yàn)收即可。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容