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

    • 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:


<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,则要进行以下操作:

Android单元测试之Local unit tests(下)_第1张图片
Android单元测试之Local unit tests(下)_第2张图片
Android单元测试之Local unit tests(下)_第3张图片
按图片配置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 happened
activityController.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使用教程

你可能感兴趣的:(android)