可靠的功能測试--Espresso和Dagger2

欢迎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数据绑定支持.

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; // 天气client

    @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类, 延迟发送至监听接口.

/**
 * 模拟天气Apiclient
 */
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’.

OK, that’s all! Enjoy it!

时间: 2024-10-05 04:27:42

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

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

近期调试用到了安信可的ESP-01模块,最终打通了编译下载调试的整个通道,有一些细节须要记录,方便兴许的开发工作. 转载请注明:http://blog.csdn.net/sadshen/article/details/46776663 一.硬件准备 安信可的相关资料没有一个非常好的收集.费了非常大劲才从QQ群中下载到了測试板电路图,最终搞明确了拨码开关的含义.另外ESP-01的flash大小也没地方标明.问了QQ群里的人才知道手头的这个黑色版本号模块的flash大小是1M. 通过对电路的了解,大

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

GMGDC专訪戴亦斌:具体解释QAMAster全面測试服务6大功能 2014/10/10 · Testin · 业界资讯 在9月24-25日第三届全球移动游戏开发人员大会上,Testin云測COO戴亦斌受邀在GMGDC官方採訪中心接受多家媒体採訪,具体阐述和分析Testin云測全新推出的质量管家QAMAster全面測试服务的6大功能与服务. 下面为主要专訪内容: 问:Testin云測在Q3或者Q4有无新服务提供给开发人员? 戴亦斌:借着GMGDC的大会,我们把Testin两项服务正式推出来,事实

单元測试和白盒測试相关总结

一.  软件測试方法 1.        软件測试方法包含:白盒測试(White  Box  Testing).黑盒測试(Black  Box Testing).灰盒測试.静态測试.动态測试. 2.        白盒測试:是一种測试用例设计方法.在这里盒子指的是被測试的软件,白盒.顾名思义即盒子是可视的,你能够清晰盒子内部的东西以及里面是怎样运作的.因此白盒測试须要你对系统内部的结构和工作原理有一个清晰的了解,并且基于这个知识来设计你的用例. 白盒測试技术一般可被分为静态分析和动态分析两类技术

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

项目背景 广告SDK项目是为应用程序APP开发者提供移动广告平台接入的API程序集合,其形态就是一个植入宿主APP的jar包.提供的功能主要有以下几点: - 为APP请求广告内容 - 用户行为打点 - 错误日志打点 - 反作弊 团队现状 在项目推进的过程中.逐渐暴露了一些问题: 1. 项目团队分为上海团队(服务端)和北京团队(client),因为信息同步,人力资源等其它原因.服务端与client的开发进度非常难保持同步,经常出现client等着和服务端联调的情况 2. 接口文档不稳定,理解有偏差

关于迭代測试的一些思考

作者:朱金灿 来源:http://blog.csdn.net/clever101 一个软件的功能的越来越多,怎样建立一个规范的測试流程来保证对开发的功能进行充分的測试,是摆在我们面前的难题.在改动bug中经常会出现一种"按下葫芦浮起瓢"情形--改动了A模块的bug,却造成了原来測试没有问题的B模块出现了新的问题.这就促使我们思考:怎样保证測试的百分百的覆盖率.为此我设想一种迭代測试和迭代公布的流程.这个流程详细是这种:全部功能測试分为常规功能測试和新功能測试.所谓常规功能測试是指之前測

Selenium2 Python 自己主动化測试实战学习笔记(五)

7.1 自己主动化測试用例 无论是功能測试.性能測试和自己主动化測试时都须要编写測试用例,測试用例的好坏能准确的体现了測试人员的经验.能力以及对项目的深度理解. 7.1.1 手工測试用例与自己主动化測试用例 手工測试用例是针对手工測试人员.自己主动化測试用例是针对自己主动化測试框架.前者是手工測试用例人员应用手工方式进行用例解析,后者是应用脚本技术进行用例解析. 前者具有较好的异常处理能力,并且可以基于測试用例,制造各种不同的逻辑推断,并且人工測试步步跟踪,可以仔细定位问题.后者全然依照測试用例

软件測试基本方法(六)之集成測试和系统測试

在软件开发中.常常会遇到这种情况.单元測试时确认每一个模块都能单独工作,但这些模块集成在一起之后会出现有些模块不能正常工作.比如,在chrome环境下用js写了一个实时捕捉video中特定区域的模块,正常工作:利用worker线程进行webgl场景渲染,也正常.但是当两个运算合并时.出现一个模块不能正常执行,原因在于两个模块不适合在worker线程中结合.基于worker本身的局限性,仅仅能有一个模块正常工作. 所以,非常有必要进行集成測试. (1)集成測试定义: 集成測试是将软件集成起来,对模

android測试工具MonkeyRunner--google官网翻译

近期在复习之前的笔记,在回想MonkeyRunner时看了看google官网的内容,写得不错.就翻译出来分享下.事实上google官网真是一个学习的好地方. 基础知识 MonkeyRunner工具提供了一个API用于在Android代码之外控制Android设备和模拟器.通过MonkeyRunner.您能够写出一个Python程序去安装一个Android应用程序或測试包.执行它,向它发送模拟击键.截取它的用户界面图片.并将截图存储于工作站上.monkeyrunner工具的主要设计目的是用于測试功

软件測试系列之入门篇(一)

一.你知道软件測试有多重要吗? 在国际上.软件測试(软件质量控制)是一件很重要的project工作.測试也作为一个很独立的职业. 在IBM.Microsoft等开发大型系统软件公司,许多重要项目的开发測试人员的比例可以达到1:2甚至1:4. 在国内软件測试的地位还不够高.而且大多仅仅停留在软件单元測试.集成測试和功能測试上.软件測试从业人员的数量同实际需求有不小差距.国内软件企业中开发者与測试人员数量一般为5:1.因此.国内的软件測试产业化还有待开发和深掘. 讲到这里不知道你反应是高兴还是失望?