• Android单元测试之Local unit tests(下)
      • 前言
        • Robolectric
          • 添加依赖
          • 测试例子-Robolectric基本使用
          • 无处不在的 shadowXXX 是什么?
          • 测试例子-Robolectric使用进阶
          • @Config(qualifiers =”“) 配置
          • 测试例子-Activity的生命周期
          • 自定义Shdow类
          • 附加模块
        • JUnit 4 & Mockito & Robolectric
      • 参考链接

Android单元测试之Local unit tests(下)

前言

接上篇 Android单元测试之Local unit tests(上)

前面介绍了这么多,接下来我们要介绍神器“Robolectric”就是为Android而生的单元测试框架

Robolectric

我们在前文介绍过Android中做本地单元测试是无法调用android.jar真正实现方法的,之前的解决方案是通过Mockito这类Mock框架来模拟调用。

而Robolectric 单元测试框架,通过在Android SDK类被加载时重写它们,并使它们能够在常规的JVM上运行成为可能。测试可以在几秒钟内在JVM上运行完成。

Robolectric与Mockito相比,使用上更接近“黑盒测试”,使得测试更有效地进行重构,并且无需Mock大量Android的方法,专注于应用程序的测试。当然它们也可以一起使用。

添加依赖
dependencies {    testCompile 'junit:junit:4.12'    testCompile "org.robolectric:robolectric:3.8"}
测试例子-Robolectric基本使用

我们通过测试例子来学习下如何使用Robolectric,我们新建个普通的Activity,比较简单,代码如下:

public class MyActivity extends Activity {    private static final String TAG = "MyActivity";    @Override    protected String getTag() {        return TAG;    }    @Override    protected void onCreate(Bundle savedInstanceState) {        super.onCreate(savedInstanceState);        setContentView(R.layout.activity_my);        init();    }    private void init() {        findViewById(R.id.btn).setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                Log.d(TAG, "onClick: ");            }        });        findViewById(R.id.btn_go).setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                Intent intent = new Intent();                intent.setClass(getApplication(), MainActivity.class);                startActivity(intent);            }        });        findViewById(R.id.btn_test_handler).setOnClickListener(new View.OnClickListener() {            @Override            public void onClick(View v) {                mHandler.postDelayed(new Runnable() {                    @Override                    public void run() {                        Toast.makeText(MyActivity.this, "hello", Toast.LENGTH_SHORT).show();                    }                },1000);            }        });    }}

布局文件 activity_my.xml:

<?xml version="1.0" encoding="utf-8"?><RelativeLayout    xmlns:android="http://schemas.android.com/apk/res/android"    xmlns:app="http://schemas.android.com/apk/res-auto"    xmlns:tools="http://schemas.android.com/tools"    android:layout_width="match_parent"    android:layout_height="match_parent"    tools:context="com.example.licheng.testapp.test.MyActivity">    <Button        android:id="@+id/btn"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_centerInParent="true"        android:text="log"/>    <Button        android:id="@+id/btn_go"        android:layout_below="@id/btn"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_centerInParent="true"        android:text="start new activity"/>        <Button        android:id="@+id/btn_test_handler"        android:layout_below="@id/btn_go"        android:layout_width="wrap_content"        android:layout_height="wrap_content"        android:layout_centerInParent="true"        android:text="post delay msg"/>RelativeLayout>

很简单,放了两个按钮分别注册了点击事件监听。

我们现在对这个MyActivity进行单元测试,在类名上CTRL+SHIFT+T,生成相应的测试类MyActivityTest

@RunWith(RobolectricTestRunner.class)@Config(constants = BuildConfig.class, shadows = {ShadowLog.class}, sdk = 23)public class MyActivityTest {    @Before    public void setUp() throws Exception {        // 设置log输出模式,配置后控制台可见日志        ShadowLog.stream = System.out;    }    @Test    public void clickingButton_shouldPrintLog() throws Exception {        MyActivity activity = Robolectric.setupActivity(MyActivity.class);        Button btn = (Button) activity.findViewById(R.id.btn);        // 控制台打印出"onClick: ",验证按钮点击事件成功        btn.performClick();    }}
  1. 测试类必须添加@RunWith(RobolectricTestRunner.class)
  2. @Config()用于配置测试环境,可以对类或者方法进行设置。配置参数很多,常见的有:sdk,manifest,qualifiers。

基本配置完成后,我们就可以开始运行测试了。首次运行时间较长,因为Robolectric会远程下载相应sdk版本jar包。(如长时间无反应,建议重试)

注意:
如果运行过程爆出java.io.FileNotFoundException: build\intermediates\bundles\debug\AndroidManifest.xml,则要进行以下操作:




按图片配置Working directory即可。

我们运行完此次的单元测试后,控制台会输出”onClick”,验证了按钮点击事件的相关逻辑。

无处不在的 shadowXXX 是什么?

你可能注意到了例子中我们配置了一个shadows = {ShadowLog.class},这个ShadowLog是什么呢?

其实ShadowLog是Robolectric为JVM重写的android sdk的android.util.Log.class,Robolectric中有大量的ShadowXXX类,当一个Android OS类被实例化,Robolectric会搜索相应的Shadow类;如果找到了,将创建与之关联的Shadow对象。Robolectirc确保:如果存在,Shadow类中的相应方法先被调用,这样就有机会做测试相关逻辑。

常见的有:ShadowActivity,ShadowViewGroup,ShadowView……等等无需配置会自动替换。使用者也可以自定义Shadow类。

测试例子-Robolectric使用进阶

我们在刚刚的例子只验证了一个按钮的点击事件,还有另外两个按钮的点击事件我们来进行测试:

public Context getContext() {        return RuntimeEnvironment.application;    }    @Test    public void clickBtn_shouldStartNewActivity() throws Exception {        Button button = (Button) activity.findViewById(R.id.btn_go);        // 验证按钮点击跳转Activity        button.performClick();        // 对Activity进行Shadow        ShadowActivity shadowActivity = Shadow.extract(activity);        // 获取实际跳转intent        String actualIntentName = shadowActivity.getNextStartedActivity().getComponent().getClassName();        // 构造预期跳转intent        Intent expectedIntent = new Intent(getContext(), MainActivity.class);        String expectedIntentName = expectedIntent.getComponent().getClassName();        // 验证实际跳转与预期跳转是否一致,比对跳转Activity类名        Assert.assertThat(expectedIntentName, Matchers.is(actualIntentName));    }        @Test    public void clickBtn_shouldPostDelayMsg() {        MyActivity activity = Robolectric.setupActivity(MyActivity.class);        Button button = (Button) activity.findViewById(R.id.btn_test_handler);        // 验证按钮点击跳转Activity        button.performClick();        /**         * java.lang.AssertionError:         *  Expected: is "hello"         *  but: was null         *         *  测试不通过,因为是延迟发送消息         */        // Assert.assertThat(ShadowToast.getTextOfLatestToast(), Matchers.is("hello"));        // 通过ShadowLooper 完成所有task的执行,包括延迟task        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();        Assert.assertThat(ShadowToast.getTextOfLatestToast(), Matchers.is("hello"));    }

结论:
1. 可以通过RuntimeEnvironment.application方式获取Context
2. 通过对Activity进行Shadow,可以调用getNextStartedActivity()来验证跳转。
3. 通过ShadowLooper.runUiThreadTasksIncludingDelayedTasks();完成所有task的执行,包括延迟task

Robolectric可以类似的对Fragment,Service,Broadcast,Application等进行测试

@Config(qualifiers =”“) 配置

qualifiers配置可以模拟真实机器运行环境,包括语言,地区,屏幕,横竖屏等,之前我们使用过@Config但一直没用过这个特性,接下来我们介绍下:

@Test @Config(qualifiers = "fr-rFR-w360dp-h640dp-xhdpi")public void testItOnFrenchNexus5() { ... }

Robolectric测试会通过使用Android资源限定符解析规则,自动选择正确的资源:

values/strings.xml

<string name="not_overridden">Not Overriddenstring><string name="overridden">Unqualified valuestring><string name="overridden_twice">Unqualified valuestring>

values-en/strings.xml

<string name="overridden">English qualified valuestring><string name="overridden_twice">English qualified valuestring>

values-en-port/strings.xml

<string name="overridden_twice">English portrait qualified valuestring>
@Test@Config(qualifiers="en-port")public void shouldUseEnglishAndPortraitResources() {  final Context context = RuntimeEnvironment.application;  assertThat(context.getString(R.id.not_overridden)).isEqualTo("Not Overridden");  assertThat(context.getString(R.id.overridden)).isEqualTo("English qualified value");  assertThat(context.getString(R.id.overridden_twice)).isEqualTo("English portrait qualified value");}

更多支持的配置请前往 Configuring Robolectric .

测试例子-Activity的生命周期

我们都知道Activity相比于普通java类最大的特点就是生命周期,正常的生命周期回调是由AMS完成的。单元测试肯定不会有AMS,而我们很多情况是需要对生命周期方法进行测试的,Robolectric提供了相应的解决方案。

// 使用Robolectric.buildActivity() 能够完成一个Activity的构建,并会返回ActivityController对象操作生命周期方法ActivityController controller = Robolectric.buildActivity(MyAwesomeActivity.class).create();

比如我们想验证onResume调用的影响

ActivityController controller = Robolectric.buildActivity(MyAwesomeActivity.class).create().start();Activity activity = controller.get();// assert that something hasn't happenedactivityController.resume();// assert it happened!

还有很多相似的生命周期回调方法

Activity activity = Robolectric.buildActivity(MyAwesomeActivity.class).create().start().resume().visible().get();

当你不关心生命周期时,通过Robolectric.setupActivity构建Activity,这会帮我们完成了一次完整的生命周期调用

public static  T setupActivity(Class activityClass) {    return buildActivity(activityClass).setup().get();  }public ActivityController setup() {    return create().start().postCreate(null).resume().visible();  }

注意:
我们知道在真正的Android应用程序中,onCreate之后的某个时机Activity的view hierarchy才会attach到Window完成可见。Robolectric将Activity的可见时机交给测试人员去决定。当你需要进行交互前应该完成visible()的调用。

自定义Shdow类

除了Robolectric中大量的ShdowXXX类外,我们还可以自定义Shdow类替换和丰富原有方法,帮助我们完成测试。自定义Shadow类规则如下:

  1. Shadow类须要一个public的无參构造方法以方便Robolectric框架能够实例化它。通过@Implements注解与原始类关联在一起
  2. 若原始类有有參构造方法。在Shadow类中定义public void类型的名为constructor的方法,且方法參数与原始类的构造方法參数一致
  3. 定义与原始类方法签名一致的方法,在里面重写实现,Shadow方法需用@Implementation进行注解

原始类:

public class LoginApi {    public Context mContext;    public LoginApi(Context context) {        mContext = context;    }    public void login(String name, String password, LoginCallback callback) {        if (name.equals("lee") && password.equals("123")) {            callback.onSuccess();        } else {            callback.onFailed();        }    }}

Shadow类:

@Implements(LoginApi.class)public class ShadowLoginApi {    /**     * 通过@RealObject注解能够訪问原始对象,但注意,通过@RealObject注解的变量调用方法,依旧会调用Shadow类的方法。而不是原始类的方法     * 仅仅能用来訪问原始类的field     */    @RealObject    LoginApi mLoginApi;    /**     * 须要一个无參构造方法     */    public ShadowLoginApi() {    }    /**     * 相应原始类的构造方法     *     * @param context 相应原始类构造方法的传入參数     */    public void __constructor__(Context context) {        mLoginApi.mContext = context;    }    /**     * 原始对象的方法被调用的时候,Robolectric会依据方法签名查找相应的Shadow方法并调用     */    @Implementation    public void login(String name, String password, LoginCallback callback){        // 直接调用成功,替换原有逻辑        callback.onSuccess();    }}

我们验证下登录:

@RunWith(RobolectricTestRunner.class)@Config(constants = BuildConfig.class,shadows = {ShadowLoginApi.class}, sdk = 23)public class LoginActivityTest {    @Before    public void setUp() throws Exception {    }    @Test    public void login() throws Exception {        LoginActivity loginActivity = Robolectric.setupActivity(LoginActivity.class);        EditText etAccount = (EditText) loginActivity.findViewById(R.id.et_account);        etAccount.setText("lee");        EditText etPassword = (EditText) loginActivity.findViewById(R.id.et_password);        etPassword.setText("1234");        loginActivity.findViewById(R.id.btn_login).performClick();        /**         * 验证成功,LoginActivity中对LoginApi.class的调用被替换为ShadowLoginApi.class的方法,默认成功         * 我们可以通过修改ShadowLoginApi.class的方法验证不同流程         */        Assert.assertThat(ShadowToast.getTextOfLatestToast(), Matchers.equalTo("success!"));    }}

我们在测试环境配置了shadows = {ShadowLoginApi.class},单元测试时ShadowLoginApi会覆盖LoginApi.class中方法,其实这里有点像我们上一节介绍的Mockito,但是有个好处就是静态方法,final方法都能进行覆盖。(可以避免mockito和powermockito混合使用)

附加模块

为了减少被测试应用程序的外部依赖数量,Robolectric的Shdow被分成各种附加包
,主Robolectric模块仅提供基础Android SDK中提供的类的Shdow类。附加模块提供了诸如appcompat或支持库之类的其他Shdow类。

SDK Package Robolectric Add-On Package
com.android.support.support-v4 org.robolectric:shadows-supportv4
com.android.support.multidex org.robolectric:shadows-multidex
com.google.android.gms:play-services org.robolectric:shadows-playservices
org.apache.httpcomponents:httpclient org.robolectric:shadows-httpclient
dependencies {    ......    testCompile "org.robolectric:shadows-supportv4:3.8"    testCompile "org.robolectric:shadows-multidex:3.8"    testCompile "org.robolectric:shadows-playservices"    testCompile "org.robolectric:shadows-httpclient:3.8"}

JUnit 4 & Mockito & Robolectric

我们以上介绍了这么多用于单元测试的框架,各有优缺点。实际应用中更是结合起来应用才会给开发者带来最大的效率。
1. 对pure java类或工具类方法测试使用JUnit 4。
2. 验证方法是否调用/流程对测试结果影响可以使用Mockito。
2. 涉及Android view及四大组件测试用Robolectric。

参考链接

Fundamentals of Testing
Android单元测试之JUnit4
robolectric
使用Robolectric进行Android单元测试
Android 集成Robolectric下的一些坑
Robolectric使用教程

更多相关文章

  1. Android(安卓)Pitfall - Fragment.startActivityForResult(), re
  2. android笔记
  3. 让TextView 自带滚动条
  4. 不可或缺 Windows Native (25) - C++: windows app native, andr
  5. Ted Mosby - 一个MVP框架的软件架构
  6. android开发每日汇总【2011-11-26】
  7. Android(安卓)Market google play store帐号注册方法流程 及发布
  8. 不可或缺 Windows Native (25) - C++: windows app native, andr
  9. Android面试题精选:讲一讲 Android(安卓)的事件分发机制

随机推荐

  1. WebAssembly入门课
  2. MySQL 8.0 安装教程 步骤 (windows 64位)
  3. 演示flex container 容器中的4个属性
  4. CSS中flex布局的属性及应用
  5. Vue自学之路1-vue概述
  6. 我可以搞定这个需求,你行吗
  7. 在数据中查找异常值的5种方法总结及示例
  8. 第三卷.Stata最新且急需的程序系列汇编
  9. 美国621位经济学家关于支持就业和企业应
  10. 我是如何从一个诗人变成一个计量实证高手