欢迎Follow我的GitHub, 关注我的CSDN.

可靠的功能測试, 意味着在不论什么时候, 获取的測试结果均同样, 这就须要模拟(Mock)数据. 測试框架能够使用Android推荐的Espresso. 模拟数据能够使用Dagger2, 一种依赖注入框架.

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

单元測试一般会模拟全部依赖, 避免出现不可靠的情况, 而功能測试也能够这样做. 一个经典的样例是怎样模拟稳定的网络数据, 能够使用Dagger2处理这样的情况.

Talk is cheap! 我来解说下怎样实现.

Github下载地址

1. 配置依赖环境

主要:

(1) Lambda表达式支持.

(2) Dagger2依赖注入框架.

(3) RxAndroid响应式编程框架.

(4) Retrofit2网络库框架.

(5) Espresso測试框架.

(6) DataBinding数据绑定支持.

  1. buildscript {
  2. repositories {
  3. jcenter()
  4. }
  5. dependencies {
  6. classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  7. }
  8. }
  9. // Lambda表达式
  10. plugins {
  11. id "me.tatarka.retrolambda" version "3.2.4"
  12. }
  13. apply plugin: 'com.android.application'
  14. apply plugin: 'com.neenbedankt.android-apt' // 凝视处理
  15. final BUILD_TOOLS_VERSION = '23.0.1'
  16. android {
  17. compileSdkVersion 23
  18. buildToolsVersion "${BUILD_TOOLS_VERSION}"
  19. defaultConfig {
  20. applicationId "clwang.chunyu.me.wcl_espresso_dagger_demo"
  21. minSdkVersion 16
  22. targetSdkVersion 23
  23. versionCode 1
  24. versionName "1.0"
  25. testInstrumentationRunner "clwang.chunyu.me.wcl_espresso_dagger_demo.runner.WeatherTestRunner"
  26. }
  27. buildTypes {
  28. release {
  29. minifyEnabled false
  30. proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
  31. }
  32. }
  33. // 凝视冲突
  34. packagingOptions {
  35. exclude 'META-INF/services/javax.annotation.processing.Processor'
  36. }
  37. // 使用Java1.8
  38. compileOptions {
  39. sourceCompatibility JavaVersion.VERSION_1_8
  40. targetCompatibility JavaVersion.VERSION_1_8
  41. }
  42. // 数据绑定
  43. dataBinding {
  44. enabled = true
  45. }
  46. }
  47. final DAGGER_VERSION = '2.0.2'
  48. final RETROFIT_VERSION = '2.0.0-beta2'
  49. dependencies {
  50. compile fileTree(dir: 'libs', include: ['*.jar'])
  51. testCompile 'junit:junit:4.12'
  52. // Warning:Conflict with dependency 'com.android.support:support-annotations'.
  53. // Resolved versions for app (23.1.1) and test app (23.0.1) differ.
  54. // See http://g.co/androidstudio/app-test-app-conflict for details.
  55. compile "com.android.support:appcompat-v7:${BUILD_TOOLS_VERSION}" // 须要与BuildTools保持一致
  56. compile 'com.jakewharton:butterknife:7.0.1' // 标注
  57. compile "com.google.dagger:dagger:${DAGGER_VERSION}" // dagger2
  58. compile "com.google.dagger:dagger-compiler:${DAGGER_VERSION}" // dagger2
  59. compile 'io.reactivex:rxandroid:1.1.0' // RxAndroid
  60. compile 'io.reactivex:rxjava:1.1.0' // 推荐同一时候载入RxJava
  61. compile "com.squareup.retrofit:retrofit:${RETROFIT_VERSION}" // Retrofit网络处理
  62. compile "com.squareup.retrofit:adapter-rxjava:${RETROFIT_VERSION}" // Retrofit的rx解析库
  63. compile "com.squareup.retrofit:converter-gson:${RETROFIT_VERSION}" // Retrofit的gson库
  64. compile 'com.squareup.okhttp:logging-interceptor:2.6.0' // 拦截器
  65. // 測试的编译
  66. androidTestCompile 'com.android.support.test:runner:0.4.1' // Android JUnit Runner
  67. androidTestCompile 'com.android.support.test:rules:0.4.1' // JUnit4 Rules
  68. androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1' // Espresso core
  69. provided 'javax.annotation:jsr250-api:1.0' // Java标注
  70. }

Lambda表达式支持, 优雅整洁代码的关键.

  1. // Lambda表达式
  2. plugins {
  3. id "me.tatarka.retrolambda" version "3.2.4"
  4. }
  5. android {
  6. // 使用Java1.8
  7. compileOptions {
  8. sourceCompatibility JavaVersion.VERSION_1_8
  9. targetCompatibility JavaVersion.VERSION_1_8
  10. }
  11. }

Dagger2依赖注入框架, 实现依赖注入. android-apt使用生成代码的插件.

  1. buildscript {
  2. repositories {
  3. jcenter()
  4. }
  5. dependencies {
  6. classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
  7. }
  8. }
  9. apply plugin: 'com.neenbedankt.android-apt' // 凝视处理
  10. dependencies {
  11. compile "com.google.dagger:dagger:${DAGGER_VERSION}" // dagger2
  12. compile "com.google.dagger:dagger-compiler:${DAGGER_VERSION}" // dagger2
  13. provided 'javax.annotation:jsr250-api:1.0' // Java标注
  14. }

測试, 在默认配置中加入Runner, 在依赖中加入espresso库.

  1. android{
  2. defaultConfig {
  3. testInstrumentationRunner "clwang.chunyu.me.wcl_espresso_dagger_demo.runner.WeatherTestRunner"
  4. }
  5. }
  6. dependencies {
  7. testCompile 'junit:junit:4.12'
  8. // 測试的编译
  9. androidTestCompile 'com.android.support.test:runner:0.4.1' // Android JUnit Runner
  10. androidTestCompile 'com.android.support.test:rules:0.4.1' // JUnit4 Rules
  11. androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2.1' // Espresso core
  12. }

数据绑定

  1. android{
  2. // 数据绑定
  3. dataBinding {
  4. enabled = true
  5. }
  6. }

2. 设置项目

使用数据绑定, 实现了简单的搜索天功能.

  1. /**
  2. * 实现简单的查询天气的功能.
  3. *
  4. * @author wangchenlong
  5. */
  6. public class MainActivity extends AppCompatActivity {
  7. private ActivityMainBinding mBinding; // 数据绑定
  8. private MenuItem mSearchItem; // 菜单项
  9. private Subscription mSubscription; // 订阅
  10. @Inject WeatherApiClient mWeatherApiClient; // 天气client
  11. @Override
  12. protected void onCreate(Bundle savedInstanceState) {
  13. super.onCreate(savedInstanceState);
  14. ((WeatherApplication) getApplication()).getAppComponent().inject(this);
  15. mBinding = DataBindingUtil.setContentView(this, R.layout.activity_main);
  16. }
  17. @Override public boolean onCreateOptionsMenu(Menu menu) {
  18. getMenuInflater().inflate(R.menu.menu_activity_main, menu); // 载入文件夹资源
  19. mSearchItem = menu.findItem(R.id.menu_action_search);
  20. tintSearchMenuItem();
  21. initSearchView();
  22. return true;
  23. }
  24. // 搜索项着色, 会覆盖基础颜色, 取交集.
  25. private void tintSearchMenuItem() {
  26. int color = ContextCompat.getColor(this, android.R.color.white); // 白色
  27. mSearchItem.getIcon().setColorFilter(color, PorterDuff.Mode.SRC_IN); // 交集
  28. }
  29. // 搜索项初始化
  30. private void initSearchView() {
  31. SearchView searchView = (SearchView) MenuItemCompat.getActionView(mSearchItem);
  32. searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
  33. @Override public boolean onQueryTextSubmit(String query) {
  34. MenuItemCompat.collapseActionView(mSearchItem);
  35. loadWeatherData(query); // 载入查询数据
  36. return true;
  37. }
  38. @Override public boolean onQueryTextChange(String newText) {
  39. return false;
  40. }
  41. });
  42. }
  43. // 载入天气数据
  44. private void loadWeatherData(String cityName) {
  45. mBinding.progress.setVisibility(View.VISIBLE);
  46. mSubscription = mWeatherApiClient
  47. .getWeatherForCity(cityName)
  48. .subscribeOn(Schedulers.io())
  49. .observeOn(AndroidSchedulers.mainThread())
  50. .subscribe(this::bindData, this::bindDataError);
  51. }
  52. // 绑定天气数据
  53. private void bindData(WeatherData weatherData) {
  54. mBinding.progress.setVisibility(View.INVISIBLE);
  55. mBinding.weatherLayout.setVisibility(View.VISIBLE);
  56. mBinding.setWeatherData(weatherData);
  57. }
  58. // 绑定数据失败
  59. private void bindDataError(Throwable throwable) {
  60. mBinding.progress.setVisibility(View.INVISIBLE);
  61. }
  62. @Override
  63. protected void onDestroy() {
  64. if (mSubscription != null) {
  65. mSubscription.unsubscribe();
  66. }
  67. super.onDestroy();
  68. }
  69. }

数据绑定实现数据和显示分离, 解耦项目, 易于管理, 很适合数据展示页面.

在layout中设置数据.

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

在代码中绑定数据.

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

搜索框的设置.

  1. @Override public boolean onCreateOptionsMenu(Menu menu) {
  2. getMenuInflater().inflate(R.menu.menu_activity_main, menu); // 载入文件夹资源
  3. mSearchItem = menu.findItem(R.id.menu_action_search);
  4. tintSearchMenuItem();
  5. initSearchView();
  6. return true;
  7. }
  8. // 搜索项着色, 会覆盖基础颜色, 取交集.
  9. private void tintSearchMenuItem() {
  10. int color = ContextCompat.getColor(this, android.R.color.white); // 白色
  11. mSearchItem.getIcon().setColorFilter(color, PorterDuff.Mode.SRC_IN); // 交集
  12. }
  13. // 搜索项初始化
  14. private void initSearchView() {
  15. SearchView searchView = (SearchView) MenuItemCompat.getActionView(mSearchItem);
  16. searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
  17. @Override public boolean onQueryTextSubmit(String query) {
  18. MenuItemCompat.collapseActionView(mSearchItem);
  19. loadWeatherData(query); // 载入查询数据
  20. return true;
  21. }
  22. @Override public boolean onQueryTextChange(String newText) {
  23. return false;
  24. }
  25. });
  26. }

3. 功能測试

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

既然使用Dagger2, 那么我们就来配置依赖注入.

三部曲: Module -> Component -> Application

Module, 使用模拟Api类, MockWeatherApiClient.

  1. /**
  2. * 測试App的Module, 提供AppContext, WeatherApiClient的模拟数据.
  3. * <p>
  4. * Created by wangchenlong on 16/1/16.
  5. */
  6. @Module
  7. public class TestAppModule {
  8. private final Context mContext;
  9. public TestAppModule(Context context) {
  10. mContext = context.getApplicationContext();
  11. }
  12. @AppScope
  13. @Provides
  14. public Context provideAppContext() {
  15. return mContext;
  16. }
  17. @Provides
  18. public WeatherApiClient provideWeatherApiClient() {
  19. return new MockWeatherApiClient();
  20. }
  21. }

Component, 注入MainActivityTest.

  1. /**
  2. * 測试组件, 加入TestAppModule
  3. * <p>
  4. * Created by wangchenlong on 16/1/16.
  5. */
  6. @AppScope
  7. @Component(modules = TestAppModule.class)
  8. public interface TestAppComponent extends AppComponent {
  9. void inject(MainActivityTest test);
  10. }

Application, 继承非測试的Application(WeatherApplication), 设置測试组件, 重写获取组件的方法(getAppComponent).

  1. /**
  2. * 測试天气应用
  3. * <p>
  4. * Created by wangchenlong on 16/1/16.
  5. */
  6. public class TestWeatherApplication extends WeatherApplication {
  7. private TestAppComponent mTestAppComponent;
  8. @Override public void onCreate() {
  9. super.onCreate();
  10. mTestAppComponent = DaggerTestAppComponent.builder()
  11. .testAppModule(new TestAppModule(this))
  12. .build();
  13. }
  14. // 组件
  15. @Override
  16. public TestAppComponent getAppComponent() {
  17. return mTestAppComponent;
  18. }
  19. }

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

  1. /**
  2. * 模拟天气Apiclient
  3. */
  4. public class MockWeatherApiClient implements WeatherApiClient {
  5. @Override public Observable<WeatherData> getWeatherForCity(String cityName) {
  6. // 获得模拟数据
  7. WeatherData weatherData = new Gson().fromJson(TestData.MUNICH_WEATHER_DATA_JSON, WeatherData.class);
  8. return Observable.just(weatherData).delay(1, TimeUnit.SECONDS); // 延迟时间
  9. }
  10. }

注冊Application至TestRunner.

  1. /**
  2. * 更换Application, 设置TestRunner
  3. */
  4. public class WeatherTestRunner extends AndroidJUnitRunner {
  5. @Override
  6. public Application newApplication(ClassLoader cl, String className, Context context) throws InstantiationException,
  7. IllegalAccessException, ClassNotFoundException {
  8. String testApplicationClassName = TestWeatherApplication.class.getCanonicalName();
  9. return super.newApplication(cl, testApplicationClassName, context);
  10. }
  11. }

測试主类

  1. /**
  2. * 測试的Activity
  3. * <p>
  4. * Created by wangchenlong on 16/1/16.
  5. */
  6. @LargeTest
  7. @RunWith(AndroidJUnit4.class)
  8. public class MainActivityTest {
  9. private static final String CITY_NAME = "Beijing"; // 由于我们使用測试接口, 设置不论什么都能够.
  10. @Rule public ActivityTestRule<MainActivity> activityTestRule = new ActivityTestRule<>(MainActivity.class);
  11. @Inject WeatherApiClient weatherApiClient;
  12. @Before
  13. public void setUp() {
  14. ((TestWeatherApplication) activityTestRule.getActivity().getApplication()).getAppComponent().inject(this);
  15. }
  16. @Test
  17. public void correctWeatherDataDisplayed() {
  18. WeatherData weatherData = weatherApiClient.getWeatherForCity(CITY_NAME).toBlocking().first();
  19. onView(withId(R.id.menu_action_search)).perform(click());
  20. onView(withId(android.support.v7.appcompat.R.id.search_src_text)).perform(replaceText(CITY_NAME));
  21. onView(withId(android.support.v7.appcompat.R.id.search_src_text)).perform(pressKey(KeyEvent.KEYCODE_ENTER));
  22. onView(withId(R.id.city_name)).check(matches(withText(weatherData.getCityName())));
  23. onView(withId(R.id.weather_date)).check(matches(withText(weatherData.getWeatherDate())));
  24. onView(withId(R.id.weather_state)).check(matches(withText(weatherData.getWeatherState())));
  25. onView(withId(R.id.weather_description)).check(matches(withText(weatherData.getWeatherDescription())));
  26. onView(withId(R.id.temperature)).check(matches(withText(weatherData.getTemperatureCelsius())));
  27. onView(withId(R.id.humidity)).check(matches(withText(weatherData.getHumidity())));
  28. }
  29. }

ActivityTestRule设置MainActivity.class測试类.

setup设置依赖注入, 注入TestWeatherApplication的组件.

使用WeatherApiClient的数据, 模拟类的功能. 由于数据是预设的, 不论有无网络, 都能够进行可靠的功能測试.

运行測试, 右键点击MainActivityTest, 使用Run ‘MainActivityTest’.

OK, that’s all! Enjoy it!

可靠的功能測试--Espresso和Dagger2的更多相关文章

  1. ESP8266学习笔记1:怎样在安信可全功能測试板上实现ESP-01的编译下载和调试

    近期调试用到了安信可的ESP-01模块,最终打通了编译下载调试的整个通道,有一些细节须要记录,方便兴许的开发工作. 转载请注明:http://blog.csdn.net/sadshen/article ...

  2. GMGDC专訪戴亦斌:具体解释QAMAster全面測试服务6大功能

    GMGDC专訪戴亦斌:具体解释QAMAster全面測试服务6大功能 2014/10/10 · Testin · 业界资讯 在9月24-25日第三届全球移动游戏开发人员大会上,Testin云測COO戴亦 ...

  3. Java web測试分为6个部分

    1.功能測试 2.性能測试(包含负载/压力測试)3.用户界面測试 4. 兼容性測试 5.  安全測试  6.接口測试   1 功能測试 1.1 链接測试 链接測试可分为三个方面. 首先,測试全部链接是 ...

  4. Mock+Proxy在SDK项目的自己主动化測试实战

    项目背景 广告SDK项目是为应用程序APP开发者提供移动广告平台接入的API程序集合,其形态就是一个植入宿主APP的jar包.提供的功能主要有以下几点: - 为APP请求广告内容 - 用户行为打点 - ...

  5. Android自己主动化測试解决方式

    如今,已经有大量的Android自己主动化測试架构或工具可供我们使用,当中包含:Activity Instrumentation, MonkeyRunner, Robotium, 以及Robolect ...

  6. Android单元測试之JUnit

    随着近期几年測试方面的工作慢慢火热起来.常常看见有招聘測试project师的招聘信息.在Java中有单元測试这么一个JUnit 方式,Android眼下主要编写的语言是Java,所以在Android开 ...

  7. Android自己主动化測试之Monkeyrunner用法及实例

    眼下android SDK里自带的现成的測试工具有monkey 和 monkeyrunner两个.大家别看这俩兄弟名字相像,但事实上是完全然全不同的两个工具,应用在不同的測试领域.总的来说,monke ...

  8. Web安全測试二步走

    Web安全測试时一个比較复杂的过程,软件測试人员能够在当中做一些简单的測试,例如以下: Web安全測试也应该遵循尽早測试的原则,在进行功能測试的时候(就应该运行以下的測试Checklist安全測试场景 ...

  9. 移动App測试实战:顶级互联网企业软件測试和质量提升最佳实践

    这篇是计算机类的优质预售推荐>>>><移动App測试实战:顶级互联网企业软件測试和质量提升最佳实践> 国内顶级互联网公司測试实战经验总结.阿里.腾讯.京东.携程.百 ...

随机推荐

  1. Asp.net 生成静态页面

    http://www.cnblogs.com/tonycall/archive/2009/07/18/1526079.html Asp.net 生成静态页面(简单用法) 第一次发表,有什么错误,请大家 ...

  2. PHP 表单 - 5(完整表单实例)

    PHP 完整表单实例 本章节将介绍如何让用户在点击"提交(submit)"按钮提交数据前保证所有字段正确输入. PHP - 在表单中确保输入值 在用户点击提交按钮后,为确保字段值是 ...

  3. MySQL主从常见的架构

    Master-Slave  级联  双Master互为主备

  4. Best Time to Buy and Sell Stock I &amp;&amp; II &amp;&amp; III

    题目1:Best Time to Buy and Sell Stock Say you have an array for which the ith element is the price of ...

  5. 1038. Recover the Smallest Number (30)

    题目链接:http://www.patest.cn/contests/pat-a-practise/1038 题目: 1038. Recover the Smallest Number (30) 时间 ...

  6. FCT需求分析

    1. 系统组成 系统从硬件角度看是由芯片.电源,时钟,总线组成, 当中总线分为控制总线和数据总线. 芯片是单个的硬件单元,可实现多种功能.有些功能有性能需求,在计算机系统中大部分功能都须要软件配合. ...

  7. TP3.2批量上传文件(图片),解决同名冲突问题

    1.html <form action="{:U('Upload/index')}" enctype="multipart/form-data" meth ...

  8. TP3.2实例化复杂模型类

    1.表名:xxf_witkey_member_oauth M方法,直接实例化对象:M('member_oauth','xxf_witkey_'[,'db_config']); 具体解析:M方法三个参数 ...

  9. Apache-一个IP多个主机域名

    #配置虚拟主机名 NameVirtualHost 192.168.209.128 <VirtualHost 192.168.209.128> DocumentRoot /htdocs/wi ...

  10. js firstChild 、nextSibling、lastChild、previousSibling、parentNode

    nextSibling下一个兄弟节点 previousSibling上一个兄弟 parentNode父亲节点 <select><option value="zs" ...