可靠的功能測試--Espresso和Dagger2
原文轉載自:evget
可靠的功能測試, 意味著在任何時候, 獲取的測試結果均相同, 這就需要模擬(Mock)數據. 測試框架可以使用Android推薦的Espresso. 模擬數據可以使用Dagger2, 一種依賴注入框架。
單元測試通常會模擬所有依賴, 避免出現不可靠的情況, 而功能測試也可以這樣做. 一個經典的例子是如何模擬穩定的網絡數據, 可以使用Dagger2處理這種情況。
Talk is cheap! 我來講解下如何實現。
Github下載地址
1. 配置依賴環境
- Lambda表達式支持
- Dagger2依賴注入框架
- RxAndroid響應式編程框架
- Retrofit2網絡庫框架
- Espresso測試框架
- DataBinding數據綁定支持
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
}
// 數據綁定
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表達式支持, 優雅整潔代碼的關鍵。
// 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依賴注入框架, 實現依賴注入. 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
} 數據綁定
android{
// 數據綁定
dataBinding {
enabled = true
}
} 2. 設置項目
使用數據綁定, 實現了簡單的搜索天功能。
/**
* 實現簡單的查詢天氣的功能.
*
* @author wangchenlong
*/
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding mBinding; // 數據綁定
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); // 加載查詢數據
return true;
}
@Override public boolean onQueryTextChange(String newText) {
return false;
}
});
}
// 加載天氣數據
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);
}
// 綁定天氣數據
private void bindData(WeatherData weatherData) {
mBinding.progress.setVisibility(View.INVISIBLE);
mBinding.weatherLayout.setVisibility(View.VISIBLE);
mBinding.setWeatherData(weatherData);
}
// 綁定數據失敗
private void bindDataError(Throwable throwable) {
mBinding.progress.setVisibility(View.INVISIBLE);
}
@Override
protected void onDestroy() {
if (mSubscription != null) {
mSubscription.unsubscribe();
}
super.onDestroy();
}
} 數據綁定實現數據和顯示分離, 解耦項目, 易于管理, 非常適合數據展示頁面。
在layout中設置數據。
<data>
<variable
name="weatherData"
type="clwang.chunyu.me.wcl_espresso_dagger_demo.data.WeatherData"/>
</data> 在代碼中綁定數據。
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); // 加載查詢數據
return true;
}
@Override public boolean onQueryTextChange(String newText) {
return false;
}
});
} 3. 功能測試
這一部分, 我會重點講解。
既然使用Dagger2, 那么我們就來配置依賴注入。
三部曲: Module -> Component -> Application
Module, 使用模擬Api類, MockWeatherApiClient
/**
* 測試App的Module, 提供AppContext, WeatherApiClient的模擬數據.
* <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數據類, 使用模擬數據創建Gson類, 延遲發送至監聽接口。
/**
* 模擬天氣Api客戶端
*/
public class MockWeatherApiClient implements WeatherApiClient {
@Override public Observable<WeatherData> getWeatherForCity(String cityName) {
// 獲得模擬數據
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的數據, 模擬類的功能. 由于數據是預設的, 不論有無網絡, 都可以進行可靠的功能測試。
執行測試, 右鍵點擊MainActivityTest, 使用Run ‘MainActivityTest’。