继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

使用 Espresso 和 Dagger 自动化测试网络服务

饮歌长啸
关注TA
已关注
手记 188
粉丝 46
获赞 152

可靠的功能测试, 意味着在任何时候, 获取的测试结果均相同, 这就需要模拟(Mock)数据. 测试框架可以使用Android推荐的Espresso. 模拟数据可以使用Dagger2, 一种依赖注入框架.

单元测试通常会模拟所有依赖, 避免出现不可靠的情况, 而功能测试也可以这样做. 一个经典的例子是如何模拟稳定的网络数据, 可以使用Dagger2处理这种情况.

本文源码的GitHub下载地址

Dagger2已经成为众多Android开发者的必备工具, 是一个快速的依赖注入框架,由Square开发,并针对Android做了特别优化, 已经被Google进行Fork开发. 不像其他的依赖注入器, Dagger2没有使用反射, 而是使用预生成代码, 提高执行速度.H

Talk is cheap! 我来讲解下如何实现.


配置依赖环境

主要:
(1) Lambda表达式支持.
(2) Dagger2依赖注入框架.
(3) RxAndroid响应式编程框架.
(4) Retrofit2网络库框架.
(5) Espresso测试框架.
(6) DataBinding数据绑定支持.

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
82
83
84
85
86
87
88
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表达式支持, 优雅整洁代码的关键.

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
16
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库.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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
}

数据绑定

1
2
3
4
5
6
android{
    // 数据绑定 
    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; // 订阅

    @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中设置数据.

1
2
3
4
5
<data>
    <variable
        name="weatherData"
        type="clwang.chunyu.me.wcl_espresso_dagger_demo.data.WeatherData"/>
</data>

在代码中绑定数据.

1
2
mBinding = 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
29
@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;
        }
    });
}

功能测试

这一部分, 我会重点讲解.

既然使用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.
 */
@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.

1
2
3
4
5
6
7
8
9
10
/**
 * 测试组件, 添加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).

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;

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

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

Mock数据类, 使用模拟数据创建Gson类, 延迟发送至监听接口.

1
2
3
4
5
6
7
8
9
10
/**
 * 模拟天气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.

1
2
3
4
5
6
7
8
9
10
11
/**
 * 更换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);
    }
}

测试主类

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.
 */
@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’.

原文链接:http://www.apkbus.com/blog-719059-60336.html

打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP