Robolectric介紹

簡介

在Android模擬器或設備運行測試很慢!構建,部署和啓動應用程序每每須要一分鐘或更長時間。
在Android直接從您的IDE內部運行測試豈不是很好?但容易碰到java.lang.RuntimeException: Stub!
Robolectric不依賴Android SDK jar的安卓單元測試框架,測試在JVM上運行,能夠在幾秒內完成。
示例:
html

@RunWith(RobolectricTestRunner.class)
public class MyActivityTest {

  @Test
  public void clickingButton_shouldChangeResultsViewText() throws Exception {
    MyActivity activity = Robolectric.setupActivity(MyActivity.class);

    Button button = (Button) activity.findViewById(R.id.button);
    TextView results = (TextView) activity.findViewById(R.id.results);

    button.performClick();
    assertThat(results.getText().toString()).isEqualTo("Robolectric Rocks!");
  }
}


Robolectric改寫Android SDK中的類,使他們可能在普通的JVM上運行。
Robolectric視圖和資源及本地C代碼實現等東東加載,很容易地提供咱們本身的特定的SDK方法實現,好比模擬錯誤狀況或現實世界的sensor行爲。

運行測試模擬器以外
Robolectric,您能夠在工做站上運行測試,或在常規JVM持續集成環境,沒有一個模擬器。正由於如此,德興,包裝和安裝 - 上的仿真器的步驟是沒有必要的,減小分鐘的測試周期秒鐘,這樣能夠快速迭代,並有信心重構代碼。
Robolectric的風格更接近黑盒測試,一般不須要Mockito等模擬框架,固然依舊能夠與Mockito配合使用。java


快速入門

Robolectric與Gradle或Maven配合比較好。新項目推薦使用Gradle。

build.gradle
python

testCompile「org.robolectric:robolectric:3.0」


測試使用註解便可:

android

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class SandwichTest {
}


注意必須指定constants指向由生成系統生成的BuildConfig.class。Robolectric使用類的常量來計算輸出路徑(Gradle使用), 不然Robolectric將沒法找到manifest,resources或asset。

使用Maven構建

pom.xml:

git

<dependency>
   <groupId>org.robolectric</groupId>
   <artifactId>robolectric</artifactId>
   <version>3.0</version>
   <scope>test</scope>
</dependency>

測試使用註解便可:
github

@RunWith(RobolectricTestRunner.class)
public class SandwichTest {
}


若是你引用項目之外的資源(即在AAR依賴)項目之外的資源,須要提供Robolectric一個指針指向AAR在構建系統。

Android Studioapache

Robolectric適用於Android Studio 1.1.0及之後版本。在"Build Variants" 中開啓 unit test支持便可。Linux和Mac中須要編輯「run configurations」,設置工做目錄爲$MODULE_DIR$。api

Android Enable Unit Tests

Android Studio Configure Defaults

Eclipse已經不推薦使用,可是能夠安裝m2e-android(不支持AAR)插件,導入Maven 工程, 點擊 "Plugin execution not covered by lifecycle configuration"。
示例工程參見:https://github.com/robolectric/robolectric-samples
app

第一個測試

activity的layout以下:
框架

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/login"
        android:text="Login"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"/>

</LinearLayout>



咱們要編寫測試斷言當用戶點擊按鈕時應用程序啓動LoginActivity。

public class WelcomeActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.welcome_activity);

        final View button = findViewById(R.id.login);
        button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                startActivity(new Intent(WelcomeActivity.this, LoginActivity.class));
            }
        });
    }
}



檢查啓動了對應的intent便可:

@RunWith(RobolectricTestRunner.class)
public class WelcomeActivityTest {

    @Test
    public void clickingLogin_shouldStartLoginActivity() {
        WelcomeActivity activity = Robolectric.setupActivity(WelcomeActivity.class);
        activity.findViewById(R.id.login).performClick();

        Intent expectedIntent = new Intent(activity, WelcomeActivity.class);
        assertThat(shadowOf(activity).getNextStartedActivity()).isEqualTo(expectedIntent);
    }
}

注意:目前Robolectric只有API 16/19/21的實例。貌似還不支持API 23等。上述代碼不能實際執行,實際執行的參見demo。

後期有空會把上面代碼串成實例。


配置Robolectric

  • 配置註解

定製Robolectric的主要方式是經過@Config註解。註釋能夠應用到的類和方法,同事也能夠在基類中定製。

  • 配置SDK級別

Robolectric基於manifest的targetSdkVersion運行代碼,可更改設置SDK版本:

@Config(sdk = Build.VERSION_CODES.JELLY_BEAN)
public class SandwichTest {

    @Config(sdk = Build.VERSION_CODES.KITKAT)
    public void getSandwich_shouldReturnHamSandwich() {
    }
}
  • 配置應用程序類

@Config(application = CustomApplication.class)
public class SandwichTest {

    @Config(application = CustomApplicationOverride.class)
    public void getSandwich_shouldReturnHamSandwich() {
    }
}
  • 配置資源路徑

Gradle和Maven有默認值,也可自定義:

@Config(manifest = "some/build/path/AndroidManifest.xml")
public class SandwichTest {

    @Config(manifest = "other/build/path/AndroidManifest.xml")
    public void getSandwich_shouldReturnHamSandwich() {
    }
}

默認資源目錄res和assets。經過添加resourceDir和assetDir選項到@Config可改變這些值。

  • 配置屬性

@Config註解的內容能夠在文件中定義robolectric.properties:

sdk=18
manifest=some/build/path/AndroidManifest.xml
shadows=my.package.ShadowFoo,my.package.ShadowBar
  • 系統屬性

robolectric.offline - Set to true to disable runtime fetching of jars.
robolectric.dependency.dir - When in offline mode, specifies a folder containing runtime dependencies.
robolectric.dependency.repo.id - Set the ID of the Maven repository to use for the runtime dependencies (default sonatype).
robolectric.dependency.repo.url - Set the URL of the Maven repository to use for the runtime dependencies (default https://oss.sonatype.org/content/groups/public/).
robolectric.logging.enabled - Set to true to enable debug loggin

Gradle可在all部分爲單元測試配置系統屬性。例如,覆蓋Maven倉庫URL和ID:

android {
  testOptions {
    unitTests.all {
      systemProperty 'robolectric.dependency.repo.url', 'https://local-mirror/repo'
      systemProperty 'robolectric.dependency.repo.id', 'local'
    }
  }
}



參考資料:
http://tools.android.com/tech-docs/unit-testing-support


Activity的生命週期

Robolectric2.2以前,大多數的測試直接調用構造函數 (new MyActivity()),而後手動調用的生命週期方法,如OnCreate()。ShadowActivity的方法(例如ShadowActivity.callOnCreate())也常常使用,他們是的ActivityController的前身。ActivityController在Robolectric2.0中引入的。

如今不會直接建立ActivityController,而是使用Robolectric.buildActivity()開始。一般能夠這樣:

Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).create().get();

上面建立MyAwesomeActivity實例並調用onCreate()。

要檢查onResume()也簡單:

ActivityController controller = Robolectric.buildActivity(MyAwesomeActivity.class).create().start();
Activity activity = controller.get();
// assert that something hasn't happened
activityController.resume();
// assert it happened!

相似的方法也包括start(), pause(), stop(), and destroy(),好比測試整個建立生命週期:

Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).create().start().resume().visible().get();

能夠帶intent啓動Activity:

Intent intent = new Intent(Intent.ACTION_VIEW);
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).withIntent(intent).create().get();

或恢復已保存實例的狀態:

Bundle savedInstanceState = new Bundle();
Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class)
    .create()
    .restoreInstanceState(savedInstanceState)
    .get();

    
更多資料參見http://robolectric.org/javadoc/latest/org/robolectric/util/ActivityController.html。


附加模塊


爲了減小對應用外部依賴,Robolectric的shadow被分紅各類附加包。主Robolectric模塊只提供基礎的Android SDK提供的shadow。

SDK Package Robolectric Add-On Package
com.android.support.support-v4 org.robolectric:shadows-support-v4
com.android.support.multidex org.robolectric:shadows-multidex
com.google.android.gms:play-services org.robolectric:shadows-play-services
com.google.android.maps:maps org.robolectric:shadows-maps
org.apache.httpcomponents:httpclient org.robolectric:shadows-httpclient


Acitvity 真實實例

開發代碼:

import android.app.Activity;
import android.os.*;
import android.view.View;
import android.widget.Toast;

import com.oppo.acs.st.STManager;
import com.oppo.acs.st.demo.R;

import java.util.HashMap;
import java.util.Map;

public class MainActivity extends Activity {
    /**
    Those data is just for test.
     */
    private static final String DATA_TYPE_EXPOSE="cpd-app-expose";
    private static final String DATA_TYPE_CLICK="cpd-app-click";
    private static final String DATA_TYPE_DOWNLOAD="cpd-app-down";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_activity);
    }

    public void onBnExpose(View view){
        Toast.makeText(this, "expose", Toast.LENGTH_SHORT).show();
        /**
         *Record the expose event.
         */
        STManager.getInstance().onEvent(this, wrapKeyMap(view.getId()));
    }

    public void onBnClick(View view){
        Toast.makeText(this, "click", Toast.LENGTH_SHORT).show();
        /**
         *Record the click event.
         */
        STManager.getInstance().onEvent(this, wrapKeyMap(view.getId()));
    }

    public void onBnDownload(View view){
        Toast.makeText(this,"download", Toast.LENGTH_SHORT).show();
        /**
         *Record the download event.
         */
        STManager.getInstance().onEvent(this, wrapKeyMap(view.getId()));
    }

    public void onBnBatchExpose(View view){
        Toast.makeText(this,"batch expose.", Toast.LENGTH_SHORT).show();
        int i=0;
        while (i++<100){
            STManager.getInstance().onEvent(this,wrapKeyMap(view.getId()));
        }
    }

    private Map<String,String> wrapKeyMap(int viewId){
        Map<String,String> keyMap=new HashMap<String,String>();
        //those data is just for test.
        keyMap.put(STManager.KEY_ENTER_ID,"demo");
        keyMap.put(STManager.KEY_TAB_ID,"MainActivity");
        keyMap.put(STManager.KEY_AD_POS_ID,"1001");
        keyMap.put(STManager.KEY_CATEGORY_ID,"10001");
        keyMap.put(STManager.KEY_PAR_TAB_ID,"1001");
        keyMap.put(STManager.KEY_PAR_POS_ID,"10001");
        keyMap.put(STManager.KEY_AD_ID,"200");
        keyMap.put(STManager.KEY_AD_TYPE,"cpd");
        keyMap.put(STManager.KEY_AD_OWNER,"OPPO");
        keyMap.put(STManager.KEY_CONTENT_ID,"contentId");
        keyMap.put(STManager.KEY_CONTENT_CLS,"test");
        keyMap.put(STManager.KEY_CONTENT_SIZE,"20K");
        keyMap.put(STManager.KEY_PLAN_ID,"100");
        keyMap.put(STManager.KEY_PRICE,"200.00");
        keyMap.put(STManager.KEY_PAR_EVENT_ID,"001");
        keyMap.put(STManager.KEY_TRACE_ID,"001");
        keyMap.put(STManager.KEY_AB_TEST,"ATest");
        keyMap.put(STManager.KEY_GRADE,"good");
        keyMap.put(STManager.KEY_PROPERTY,"test demo");
        keyMap.put(STManager.KEY_DISPLAY_TIME, String.valueOf(System.currentTimeMillis()));
        //
        keyMap.put(STManager.KEY_MODULE_ID,"Test");
        keyMap.put(STManager.KEY_PAR_MODULE_ID,"Test_Par");
        keyMap.put(STManager.KEY_CHANNEL,"Test");
        keyMap.put(STManager.KEY_APP_ID,"1001");
        //
        switch (viewId){
            case R.id.expose_bn:
            case R.id.batch_expose_bn:
                /**
                 *Data type of expose event.
                 */
                keyMap.put(STManager.KEY_DATA_TYPE, DATA_TYPE_EXPOSE);
                break;
            case R.id.click_bn:
                /**
                 *Data type of click event.
                 */
                keyMap.put(STManager.KEY_DATA_TYPE,DATA_TYPE_CLICK);
                break;
            case R.id.download_bn:
                /**
                 *Data type of download event.
                 */
                keyMap.put(STManager.KEY_DATA_TYPE,DATA_TYPE_DOWNLOAD);
                break;
            default:
                break;
        }
        return keyMap;
    }

    @Override
    public void onBackPressed() {
        super.onBackPressed();
        /**
         *Report all statistics data before application exit.
         */
        STManager.getInstance().onExit(this, new STManager.ExitListener() {
            @Override
            public void onFinish(boolean result) {
                /**
                 * Exit the application when report statistics data finished.
                 */
                android.os.Process.killProcess(android.os.Process.myPid());
            }
        });
    }


}

測試代碼:

import android.app.Activity;
import android.view.Menu;
import android.widget.Button;

import com.oppo.acs.st.demo.BuildConfig;
import com.oppo.acs.st.demo.R;

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;
import org.robolectric.shadows.ShadowToast;

import junit.framework.Assert;
import static org.robolectric.Shadows.shadowOf;

@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
public class MainActivityTest {

    Activity activity;

    @Before
    public void setUp() throws Exception {
        activity = Robolectric.setupActivity(MainActivity.class);
        final Menu menu = shadowOf(activity).getOptionsMenu();
    }

    @After
    public void tearDown() throws Exception {

    }

    @Test
    public void testOnBnExpose() throws Exception {
        Button button = (Button) activity.findViewById(R.id.expose_bn);
        button.performClick();
        Assert.assertEquals("expose", ShadowToast.getTextOfLatestToast());
    }

    @Test
    public void testOnBnClick() throws Exception {
        Button button = (Button) activity.findViewById(R.id.click_bn);
        button.performClick();
        Assert.assertEquals("click", ShadowToast.getTextOfLatestToast());
    }

    @Test
    public void testOnBnDownload() throws Exception {
        Button button = (Button) activity.findViewById(R.id.download_bn);
        button.performClick();
        Assert.assertEquals("download", ShadowToast.getTextOfLatestToast());

    }

    @Test
    public void testOnBnBatchExpose() throws Exception {
        Button button = (Button) activity.findViewById(R.id.batch_expose_bn);
        button.performClick();
        Assert.assertEquals("batch expose.", ShadowToast.getTextOfLatestToast());
    }
}package com.oppo.acs.st;

import android.app.Application;

import java.util.List;
import com.oppo.acs.st.demo.BuildConfig;

import org.junit.runner.RunWith;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLog;
import static org.junit.Assert.*;



@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class)
@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*" })
@PrepareForTest({STManager.class})
public class STManagerTest {

    private STManager sTManager;
    private Application application;

    @Before
    public void setUp() throws Exception {
        sTManager = STManager.getInstance();
        sTManager.enableDebugLog();
        application = RuntimeEnvironment.application;
        ShadowLog.stream = System.out;
    }

    @After
    public void tearDown() throws Exception {

    }

    @Test
    public void init() throws Exception {
        sTManager.init(application);
        List logs = ShadowLog.getLogs();
        System.out.println(logs.size());
        String result = readFile();
        assertTrue(result.contains("has no initted.init!!"));
    }

    @Test (expected = Exception.class)
    public void initWithNullRaiseException() throws Exception {
        sTManager.init(null);
    }

    public String readFile() throws Exception {
        String result = "";
        List logs = ShadowLog.getLogs();
        for (int i = 0; i < logs.size(); i++) {
            // System.out.println(logs.get(i).toString());
            result = result + ((ShadowLog.LogItem) logs.get(i)).msg;
        }
        return result;
    }

}

安卓端基於日誌的測試方法

package com.oppo.acs.st;

import android.app.Application;

import java.util.List;
import com.oppo.acs.st.demo.BuildConfig;
import com.oppo.acs.st.utils.Utils;

import org.junit.Rule;
import org.junit.runner.RunWith;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import org.powermock.api.mockito.PowerMockito;
import org.powermock.core.classloader.annotations.PowerMockIgnore;
import org.powermock.core.classloader.annotations.PrepareForTest;
import org.powermock.modules.junit4.rule.PowerMockRule;
import org.robolectric.RobolectricGradleTestRunner;
import org.robolectric.RuntimeEnvironment;
import org.robolectric.annotation.Config;
import org.robolectric.shadows.ShadowLog;
import static org.junit.Assert.*;



@RunWith(RobolectricGradleTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
@PowerMockIgnore({ "org.mockito.*", "org.robolectric.*", "android.*" })
@PrepareForTest(Utils.class)
public class STManagerTest {

    private STManager sTManager;
    private Application application;

    @Rule
    public PowerMockRule rule = new PowerMockRule();

    @Before
    public void setUp() throws Exception {
        sTManager = STManager.getInstance();
        sTManager.enableDebugLog();
        application = RuntimeEnvironment.application;
        ShadowLog.stream = System.out;
    }

    @After
    public void tearDown() throws Exception {

    }

    @Test
    public void init() throws Exception {
        sTManager.init(application);
        List logs = ShadowLog.getLogs();
        System.out.println(logs.size());
        String result = readFile();
        assertTrue(result.contains("has no initted.init!!"));
    }

    @Test
    public void initWithoutNetwork() throws Exception {
        PowerMockito.mockStatic(Utils.class);
        PowerMockito.when(Utils.isNetworkAvailable(application)).thenReturn(false);
        assertFalse(Utils.isNetworkAvailable(application));
        sTManager.init(application);
        String result = readFile();
        assertTrue(result.contains("no net!"));
    }

    @Test (expected = Exception.class)
    public void initWithNullRaiseException() throws Exception {
        sTManager.init(null);
    }

    public String readFile() throws Exception {
        String result = "";
        List logs = ShadowLog.getLogs();
        for (int i = 0; i < logs.size(); i++) {
            // System.out.println(logs.get(i).toString());
            result = result + ((ShadowLog.LogItem) logs.get(i)).msg;
        }
        return result;
    }
}

上面例子要特別注意必需要添加Rule才行。

相關文章
相關標籤/搜索