使用Espresso和Dagger自動化測試服務

歡迎Follow我的GitHub, 關注我的簡書. 其余參考Android目錄.

Android Test

本文的合集已經編著成書,高級Android開發(fā)強化實戰(zhàn),歡迎各位讀友的建議和指導。在京東即可購買:https://item.jd.com/12385680.html

Android

可靠的功能測試, 意味著在任何時候, 獲取的測試結果均相同, 這就需要模擬(Mock)數(shù)據. 測試框架可以使用Android推薦的Espresso. 模擬數(shù)據可以使用Dagger2, 一種依賴注入框架.

Dagger2已經成為眾多Android開發(fā)者的必備工具, 是一個快速的依賴注入框架,由Square開發(fā),并針對Android做了特別優(yōu)化, 已經被Google進行Fork開發(fā). 不像其他的依賴注入器, Dagger2沒有使用反射, 而是使用預生成代碼, 提高執(zhí)行速度.

單元測試通常會模擬所有依賴, 避免出現(xiàn)不可靠的情況, 而功能測試也可以這樣做. 一個經典的例子是如何模擬穩(wěn)定的網絡數(shù)據, 可以使用Dagger2處理這種情況.

Talk is cheap! 我來講解下如何實現(xiàn).

本文源碼的GitHub下載地址

Test

1. 配置依賴環(huán)境

主要:
(1) Lambda表達式支持.
(2) Dagger2依賴注入框架.
(3) RxAndroid響應式編程框架.
(4) Retrofit2網絡庫框架.
(5) Espresso測試框架.
(6) DataBinding數(shù)據綁定支持.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

// Lambda表達式
plugins {
    id "me.tatarka.retrolambda" version "3.2.4"
}

apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt' // 注釋處理

final BUILD_TOOLS_VERSION = '23.0.1'

android {
    compileSdkVersion 23
    buildToolsVersion "${BUILD_TOOLS_VERSION}"

    defaultConfig {
        applicationId "clwang.chunyu.me.wcl_espresso_dagger_demo"
        minSdkVersion 16
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "clwang.chunyu.me.wcl_espresso_dagger_demo.runner.WeatherTestRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }

    // 注釋沖突
    packagingOptions {
        exclude 'META-INF/services/javax.annotation.processing.Processor'
    }

    // 使用Java1.8
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }

    // 數(shù)據綁定
    dataBinding {
        enabled = true
    }
}

final DAGGER_VERSION = '2.0.2'
final RETROFIT_VERSION = '2.0.0-beta2'

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    // Warning:Conflict with dependency 'com.android.support:support-annotations'.
    // Resolved versions for app (23.1.1) and test app (23.0.1) differ.
    // See http://g.co/androidstudio/app-test-app-conflict for details.
    compile "com.android.support:appcompat-v7:${BUILD_TOOLS_VERSION}" // 需要與BuildTools保持一致

    compile 'com.jakewharton:butterknife:7.0.1' // 標注

    compile "com.google.dagger:dagger:${DAGGER_VERSION}" // dagger2
    compile "com.google.dagger:dagger-compiler:${DAGGER_VERSION}" // dagger2

    compile 'io.reactivex:rxandroid:1.1.0' // RxAndroid
    compile 'io.reactivex:rxjava:1.1.0' // 推薦同時加載RxJava

    compile "com.squareup.retrofit:retrofit:${RETROFIT_VERSION}" // Retrofit網絡處理
    compile "com.squareup.retrofit:adapter-rxjava:${RETROFIT_VERSION}" // Retrofit的rx解析庫
    compile "com.squareup.retrofit:converter-gson:${RETROFIT_VERSION}" // Retrofit的gson庫
    compile 'com.squareup.okhttp:logging-interceptor:2.6.0' // 攔截器

    // 測試的編譯
    androidTestCompile 'com.android.support.test:runner:0.4.1' // Android JUnit Runner
    androidTestCompile 'com.android.support.test:rules:0.4.1' // JUnit4 Rules
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1' // Espresso core

    provided 'javax.annotation:jsr250-api:1.0' // Java標注
}

Lambda表達式支持, 優(yōu)雅整潔代碼的關鍵.

// Lambda表達式
plugins {
    id "me.tatarka.retrolambda" version "3.2.4"
}

android {
    // 使用Java1.8
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

Dagger2依賴注入框架, 實現(xiàn)依賴注入. android-apt使用生成代碼的插件.

buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

apply plugin: 'com.neenbedankt.android-apt' // 注釋處理

dependencies {
    compile "com.google.dagger:dagger:${DAGGER_VERSION}" // dagger2
    compile "com.google.dagger:dagger-compiler:${DAGGER_VERSION}" // dagger2
    provided 'javax.annotation:jsr250-api:1.0' // Java標注
}

測試, 在默認配置中添加Runner, 在依賴中添加espresso庫.

android{
    defaultConfig {
        testInstrumentationRunner "clwang.chunyu.me.wcl_espresso_dagger_demo.runner.WeatherTestRunner"
    }
}

dependencies {
    testCompile 'junit:junit:4.12'

    // 測試的編譯
    androidTestCompile 'com.android.support.test:runner:0.4.1' // Android JUnit Runner
    androidTestCompile 'com.android.support.test:rules:0.4.1' // JUnit4 Rules
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1' // Espresso core
}

數(shù)據綁定

android{
    // 數(shù)據綁定 
    dataBinding { 
        enabled = true 
    }
}

2. 設置項目

使用數(shù)據綁定, 實現(xiàn)了簡單的搜索天功能.

/**
 * 實現(xiàn)簡單的查詢天氣的功能.
 *
 * @author wangchenlong
 */
public class MainActivity extends AppCompatActivity {

    private ActivityMainBinding mBinding; // 數(shù)據綁定
    private MenuItem mSearchItem; // 菜單項
    private Subscription mSubscription; // 訂閱

    @Inject WeatherApiClient mWeatherApiClient; // 天氣客戶端

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        ((WeatherApplication) getApplication()).getAppComponent().inject(this);
        mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
    }


    @Override public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_activity_main, menu); // 加載目錄資源
        mSearchItem = menu.findItem(R.id.menu_action_search);
        tintSearchMenuItem();
        initSearchView();
        return true;
    }

    // 搜索項著色, 會覆蓋基礎顏色, 取交集.
    private void tintSearchMenuItem() {
        int color = ContextCompat.getColor(this, android.R.color.white); // 白色
        mSearchItem.getIcon().setColorFilter(color, PorterDuff.Mode.SRC_IN); // 交集
    }

    // 搜索項初始化
    private void initSearchView() {
        SearchView searchView = (SearchView) MenuItemCompat.getActionView(mSearchItem);
        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override public boolean onQueryTextSubmit(String query) {
                MenuItemCompat.collapseActionView(mSearchItem);
                loadWeatherData(query); // 加載查詢數(shù)據
                return true;
            }

            @Override public boolean onQueryTextChange(String newText) {
                return false;
            }
        });
    }

    // 加載天氣數(shù)據
    private void loadWeatherData(String cityName) {
        mBinding.progress.setVisibility(View.VISIBLE);
        mSubscription = mWeatherApiClient
                .getWeatherForCity(cityName)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(this::bindData, this::bindDataError);
    }

    // 綁定天氣數(shù)據
    private void bindData(WeatherData weatherData) {
        mBinding.progress.setVisibility(View.INVISIBLE);
        mBinding.weatherLayout.setVisibility(View.VISIBLE);
        mBinding.setWeatherData(weatherData);
    }

    // 綁定數(shù)據失敗
    private void bindDataError(Throwable throwable) {
        mBinding.progress.setVisibility(View.INVISIBLE);
    }

    @Override
    protected void onDestroy() {
        if (mSubscription != null) {
            mSubscription.unsubscribe();
        }
        super.onDestroy();
    }
}

數(shù)據綁定實現(xiàn)數(shù)據和顯示分離, 解耦項目, 易于管理, 非常適合數(shù)據展示頁面.

在layout中設置數(shù)據.

    <data>
        <variable
            name="weatherData"
            type="clwang.chunyu.me.wcl_espresso_dagger_demo.data.WeatherData"/>
    </data>

在代碼中綁定數(shù)據.

mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
mBinding.setWeatherData(weatherData);

搜索框的設置.

    @Override public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.menu_activity_main, menu); // 加載目錄資源
        mSearchItem = menu.findItem(R.id.menu_action_search);
        tintSearchMenuItem();
        initSearchView();
        return true;
    }

    // 搜索項著色, 會覆蓋基礎顏色, 取交集.
    private void tintSearchMenuItem() {
        int color = ContextCompat.getColor(this, android.R.color.white); // 白色
        mSearchItem.getIcon().setColorFilter(color, PorterDuff.Mode.SRC_IN); // 交集
    }

    // 搜索項初始化
    private void initSearchView() {
        SearchView searchView = (SearchView) MenuItemCompat.getActionView(mSearchItem);
        searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
            @Override public boolean onQueryTextSubmit(String query) {
                MenuItemCompat.collapseActionView(mSearchItem);
                loadWeatherData(query); // 加載查詢數(shù)據
                return true;
            }

            @Override public boolean onQueryTextChange(String newText) {
                return false;
            }
        });
    }

3. 功能測試

這一部分, 我會重點講解.

既然使用Dagger2, 那么我們就來配置依賴注入.
三部曲: Module -> Component -> Application

Module, 使用模擬Api類, MockWeatherApiClient.

/**
 * 測試App的Module, 提供AppContext, WeatherApiClient的模擬數(shù)據.
 * <p>
 * Created by wangchenlong on 16/1/16.
 */
@Module
public class TestAppModule {
    private final Context mContext;

    public TestAppModule(Context context) {
        mContext = context.getApplicationContext();
    }

    @AppScope
    @Provides
    public Context provideAppContext() {
        return mContext;
    }

    @Provides
    public WeatherApiClient provideWeatherApiClient() {
        return new MockWeatherApiClient();
    }
}

Component, 注入MainActivityTest.

/**
 * 測試組件, 添加TestAppModule
 * <p>
 * Created by wangchenlong on 16/1/16.
 */
@AppScope
@Component(modules = TestAppModule.class)
public interface TestAppComponent extends AppComponent {
    void inject(MainActivityTest test);
}

Application, 繼承非測試的Application(WeatherApplication), 設置測試組件, 重寫獲取組件的方法(getAppComponent).

/**
 * 測試天氣應用
 * <p>
 * Created by wangchenlong on 16/1/16.
 */
public class TestWeatherApplication extends WeatherApplication {
    private TestAppComponent mTestAppComponent;

    @Override public void onCreate() {
        super.onCreate();
        mTestAppComponent = DaggerTestAppComponent.builder()
                .testAppModule(new TestAppModule(this))
                .build();
    }

    // 組件
    @Override
    public TestAppComponent getAppComponent() {
        return mTestAppComponent;
    }
}

Mock數(shù)據類, 使用模擬數(shù)據創(chuàng)建Gson類, 延遲發(fā)送至監(jiān)聽接口.

/**
 * 模擬天氣Api客戶端
 */
public class MockWeatherApiClient implements WeatherApiClient {
    @Override public Observable<WeatherData> getWeatherForCity(String cityName) {
        // 獲得模擬數(shù)據
        WeatherData weatherData = new Gson().fromJson(TestData.MUNICH_WEATHER_DATA_JSON, WeatherData.class);
        return Observable.just(weatherData).delay(1, TimeUnit.SECONDS); // 延遲時間
    }
}

注冊Application至TestRunner.

/**
 * 更換Application, 設置TestRunner
 */
public class WeatherTestRunner extends AndroidJUnitRunner {
    @Override
    public Application newApplication(ClassLoader cl, String className, Context context) throws InstantiationException,
            IllegalAccessException, ClassNotFoundException {
        String testApplicationClassName = TestWeatherApplication.class.getCanonicalName();
        return super.newApplication(cl, testApplicationClassName, context);
    }
}

測試主類

/**
 * 測試的Activity
 * <p>
 * Created by wangchenlong on 16/1/16.
 */
@LargeTest
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {

    private static final String CITY_NAME = "Beijing"; // 因為我們使用測試接口, 設置任何都可以.

    @Rule public ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(MainActivity.class);

    @Inject WeatherApiClient weatherApiClient;

    @Before
    public void setUp() {
        ((TestWeatherApplication) activityTestRule.getActivity().getApplication()).getAppComponent().inject(this);
    }

    @Test
    public void correctWeatherDataDisplayed() {
        WeatherData weatherData = weatherApiClient.getWeatherForCity(CITY_NAME).toBlocking().first();

        onView(withId(R.id.menu_action_search)).perform(click());
        onView(withId(android.support.v7.appcompat.R.id.search_src_text)).perform(replaceText(CITY_NAME));
        onView(withId(android.support.v7.appcompat.R.id.search_src_text)).perform(pressKey(KeyEvent.KEYCODE_ENTER));

        onView(withId(R.id.city_name)).check(matches(withText(weatherData.getCityName())));
        onView(withId(R.id.weather_date)).check(matches(withText(weatherData.getWeatherDate())));
        onView(withId(R.id.weather_state)).check(matches(withText(weatherData.getWeatherState())));
        onView(withId(R.id.weather_description)).check(matches(withText(weatherData.getWeatherDescription())));
        onView(withId(R.id.temperature)).check(matches(withText(weatherData.getTemperatureCelsius())));
        onView(withId(R.id.humidity)).check(matches(withText(weatherData.getHumidity())));
    }
}

ActivityTestRule設置MainActivity.class測試類.
setup設置依賴注入, 注入TestWeatherApplication的組件.

使用WeatherApiClient的數(shù)據, 模擬類的功能. 由于數(shù)據是預設的, 不論有無網絡, 都可以進行可靠的功能測試.

執(zhí)行測試, 右鍵點擊MainActivityTest, 使用Run 'MainActivityTest'.

OK, that's all! Enjoy it!

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容