可靠的功能测试, 意味着在任何时候, 获取的测试结果均相同, 这就需要模拟(Mock)数据. 测试框架可以使用Android推荐的Espresso. 模拟数据可以使用Dagger2, 一种依赖注入框架.
单元测试通常会模拟所有依赖, 避免出现不可靠的情况, 而功能测试也可以这样做. 一个经典的例子是如何模拟稳定的网络数据, 可以使用Dagger2处理这种情况.
本文源码的GitHub下载地址
欢迎Follow我的GitHub: https://github.com/SpikeKing
Dagger2已经成为众多Android开发者的必备工具, 是一个快速的依赖注入框架,由Square开发,并针对Android做了特别优化, 已经被Google进行Fork开发. 不像其他的依赖注入器, Dagger2没有使用反射, 而是使用预生成代码, 提高执行速度.H
Talk is cheap! 我来讲解下如何实现.
配置依赖环境
主要:
(1) Lambda表达式支持.
(2) Dagger2依赖注入框架.
(3) RxAndroid响应式编程框架.
(4) Retrofit2网络库框架.
(5) Espresso测试框架.
(6) DataBinding数据绑定支持.
1 | buildscript { |
Lambda表达式支持, 优雅整洁代码的关键.1
2
3
4
5
6
7
8
9
10
11
12// 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使用生成代码的插件.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16buildscript {
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库.1
2
3
4
5
6
7
8
9
10
11
12
13
14android{
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
}
数据绑定1
2
3
4
5
6android{
// 数据绑定
dataBinding {
enabled = true
}
}
设置项目
使用数据绑定, 实现了简单的搜索天功能.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81/**
* 实现简单的查询天气的功能.
*
* @author wangchenlong
*/
public class MainActivity extends AppCompatActivity {
private ActivityMainBinding mBinding; // 数据绑定
private MenuItem mSearchItem; // 菜单项
private Subscription mSubscription; // 订阅
// 天气客户端 WeatherApiClient mWeatherApiClient;
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
((WeatherApplication) getApplication()).getAppComponent().inject(this);
mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
}
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() {
public boolean onQueryTextSubmit(String query) {
MenuItemCompat.collapseActionView(mSearchItem);
loadWeatherData(query); // 加载查询数据
return true;
}
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);
}
protected void onDestroy() {
if (mSubscription != null) {
mSubscription.unsubscribe();
}
super.onDestroy();
}
}
数据绑定实现数据和显示分离, 解耦项目, 易于管理, 非常适合数据展示页面.
在layout中设置数据.1
2
3
4
5<data>
<variable
name="weatherData"
type="clwang.chunyu.me.wcl_espresso_dagger_demo.data.WeatherData"/>
</data>
在代码中绑定数据.1
2mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
mBinding.setWeatherData(weatherData);
搜索框的设置.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29public 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() {
public boolean onQueryTextSubmit(String query) {
MenuItemCompat.collapseActionView(mSearchItem);
loadWeatherData(query); // 加载查询数据
return true;
}
public boolean onQueryTextChange(String newText) {
return false;
}
});
}
功能测试
这一部分, 我会重点讲解.
既然使用Dagger2, 那么我们就来配置依赖注入.
三部曲: Module -> Component -> Application
Module, 使用模拟Api类, MockWeatherApiClient.1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24/**
* 测试App的Module, 提供AppContext, WeatherApiClient的模拟数据.
* <p>
* Created by wangchenlong on 16/1/16.
*/
public class TestAppModule {
private final Context mContext;
public TestAppModule(Context context) {
mContext = context.getApplicationContext();
}
public Context provideAppContext() {
return mContext;
}
public WeatherApiClient provideWeatherApiClient() {
return new MockWeatherApiClient();
}
}
Component, 注入MainActivityTest.1
2
3
4
5
6
7
8
9
10/**
* 测试组件, 添加TestAppModule
* <p>
* Created by wangchenlong on 16/1/16.
*/
(modules = TestAppModule.class)
public interface TestAppComponent extends AppComponent {
void inject(MainActivityTest test);
}
Application, 继承非测试的Application(WeatherApplication), 设置测试组件, 重写获取组件的方法(getAppComponent).1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21/**
* 测试天气应用
* <p>
* Created by wangchenlong on 16/1/16.
*/
public class TestWeatherApplication extends WeatherApplication {
private TestAppComponent mTestAppComponent;
public void onCreate() {
super.onCreate();
mTestAppComponent = DaggerTestAppComponent.builder()
.testAppModule(new TestAppModule(this))
.build();
}
// 组件
public TestAppComponent getAppComponent() {
return mTestAppComponent;
}
}
Mock数据类, 使用模拟数据创建Gson类, 延迟发送至监听接口.1
2
3
4
5
6
7
8
9
10/**
* 模拟天气Api客户端
*/
public class MockWeatherApiClient implements WeatherApiClient {
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.1
2
3
4
5
6
7
8
9
10
11/**
* 更换Application, 设置TestRunner
*/
public class WeatherTestRunner extends AndroidJUnitRunner {
public Application newApplication(ClassLoader cl, String className, Context context) throws InstantiationException,
IllegalAccessException, ClassNotFoundException {
String testApplicationClassName = TestWeatherApplication.class.getCanonicalName();
return super.newApplication(cl, testApplicationClassName, context);
}
}
测试主类1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36/**
* 测试的Activity
* <p>
* Created by wangchenlong on 16/1/16.
*/
(AndroidJUnit4.class)
public class MainActivityTest {
private static final String CITY_NAME = "Beijing"; // 因为我们使用测试接口, 设置任何都可以.
public ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(MainActivity.class);
WeatherApiClient weatherApiClient;
public void setUp() {
((TestWeatherApplication) activityTestRule.getActivity().getApplication()).getAppComponent().inject(this);
}
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’.
OK, that’s all! Enjoy it!
原始地址:
http://www.wangchenlong.org/2016/03/13/1602/use-dagger-espresso-test-network/
欢迎Follow我的GitHub, 关注我的简书, 微博, CSDN, 掘金, Slides.
我已委托“维权骑士”为我的文章进行维权行动. 未经授权, 禁止转载, 授权或合作请留言.