Bootstrap

Android单元测试框架Robolectric的学习使用

普通的AndroidJunit测试需要跑到设备或模拟器上去,需要打包apk运行,这样速度很慢,相当于每次运行app一样。而Robolectric通过实现一套能运行的Android代码的JVM环境,然后在运行unit test的时候去截取android相关的代码调用,然后转到自己实现的代码去执行这个调用的过程,从而达到能够脱离Android环境运行Android测试代码的目的。

添加配置和依赖

android {
    //使用robolectric必须配置
    testOptions {
        unitTests {
            includeAndroidResources = true
        }
    }
}
dependencies {
	testImplementation 'org.robolectric:robolectric:3.8'
}

在项目根目录下的gradle.properties 文件中添加下面的配置(在Android Studio 3.3+以上不需要):

android.enableUnitTestBinaryResources=true

我这里是依赖的Robolectric 3.8版本的,但是Robolectric最新版本已经是4.3版本的了,如果你打算使用4.0+版本的,则对Android Gradle Plugin / Android Studio 的要求是 3.2或者更新,需要参考官方的《配置迁移指南》,关于4.0版本的迁移,官方提供了一个自动迁移工具:automated migration tool,但很遗憾的是,我未尝试成功,可能是我的AS版本有点低,还有就是我的AS本身有点问题不能执行gradlew命令。

测试类配置:

@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 23)
public class MainActivityTest {
}
1. Config配置

在Robolectric当中你可以通过@Config注解来配置一些跟Android相关的系统配置

配置SDK版本
Robolectric会根据manifest文件配置的targetSdkVersion选择运行测试代码的SDK版本,如果你想指定sdk来运行测试用例,可以通过下面的方式配置:

@Config(sdk = Build.VERSION_CODES.M)
public class SandwichTest {

    @Config(sdk = Build.VERSION_CODES.KITKAT)
    public void getSandwich_shouldReturnHamSandwich() {
    }
}

配置Application类
Robolectric会根据manifest文件配置的Application配置去实例化一个Application类,如果你想在测试用例中重新指定,可以通过下面的方式配置:

@Config(application = CustomApplication.class)
public class SandwichTest {

    @Config(application = CustomApplicationOverride.class)
    public void getSandwich_shouldReturnHamSandwich() {
    }
}

指定Resource路径
Robolectric可以让你配置manifest、resource和assets路径,可以通过下面的方式配置:

@Config(manifest = "some/build/path/AndroidManifest.xml",
        assetDir = "some/build/path/assetDir",
        resourceDir = "some/build/path/resourceDir")
public class SandwichTest {

    @Config(manifest = "other/build/path/AndroidManifest.xml")
    public void getSandwich_shouldReturnHamSandwich() {
    }
}

使用第三方Library Resources
当Robolectric测试的时候,会尝试加载所有应用提供的资源,但如果你需要使用第三方库中提供的资源文件,你可能需要做一些特别的配置。不过如果你使用gradle来构建Android应用,这些配置就不需要做了,因为Gradle Plugin会在build的时候自动合并第三方库的资源,但如果你使用的是Maven,那么你需要配置libraries变量:

@RunWith(RobolectricTestRunner.class)
@Config(libraries = {
    "build/unpacked-libraries/library1",
    "build/unpacked-libraries/library2"
})
public class SandwichTest {
}

使用限定的资源文件
Android会在运行时加载特定的资源文件,如根据设备屏幕加载不同分辨率的图片资源、根据系统语言加载不同的string.xml,在Robolectric测试当中,你也可以进行一个限定,让测试程序加载特定资源.多个限定条件可以用破折号拼接在在一起。

    /**
     * 使用qualifiers加载对应的资源文件
     */
    @Config(qualifiers = "zh-rCN")
    @Test
    public void testString() throws Exception {
        final Context context = RuntimeEnvironment.application;
        assertThat(context.getString(R.string.app_name), is("单元测试Demo"));
    }

可参考:Using Qualified Resources

我们可以在Config类的源码中看到支持的哪些属性配置:
在这里插入图片描述

通过Properties文件配置
如果你嫌通过注解配置上面的东西麻烦,你也可以把以上配置放在一个Properties文件之中,然后通过@Config指定配置文件,比如,首先创建一个配置文件robolectric.properties:

# 放置Robolectric的配置选项:
sdk=21
manifest=some/build/path/AndroidManifest.xml
assetDir=some/build/path/assetDir
resourceDir=some/build/path/resourceDir

然后把robolectric.properties文件放到src/test/resources目录下,运行的时候,会自动加载里面的配置

系统属性配置

robolectric.offline:true代表关闭运行时获取jar包
robolectric.dependency.dir:当处于offline模式的时候,指定运行时的依赖目录
robolectric.dependency.repo.id:设置运行时获取依赖的Maven仓库ID,默认是sonatype
robolectric.dependency.repo.url:设置运行时依赖的Maven仓库地址,默认是https://oss.sonatype.org/content/groups/public/
robolectric.logging.enabled:设置是否打开调试开关

以上设置可以通过Gradle进行配置,如:

android {
    testOptions {
        unitTests.all {
            systemProperty 'robolectric.dependency.repo.url', 'https://local-mirror/repo'
            systemProperty 'robolectric.dependency.repo.id', 'local'
        }
    }
}

设备配置
可参考:Device Configuration

注意,从Roboelectric3.3开始,测试运行程序将在classpath中查找名为/com/android/tools/test_config.properties的文件。如果找到它,它将用于为测试提供默认manifest, resource, 和 asset 资源文件的位置,而无需在测试中指定@config(constants=buildconfig.class)@config(manifest=…“,res=…”,assets=…“)。另外,Roboelectric在运行单元测试方法时,必须确保构R.class已经构建生成。

2. 验证Activity页面跳转

现在新建一个Activity进行测试:

public class MainActivity extends Activity implements View.OnClickListener {
    private final static String TAG = MainActivity.class.getSimpleName();
    private Button mLoginBtn;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        initView();
    }

    private void initView() {
        mLoginBtn = (Button) findViewById(R.id.btn_login);
        mLoginBtn.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_login:
                Intent intent = new Intent(this, LoginActivity.class);
                startActivity(intent);
                break;
            default:
                break;
        }
    }
}

布局文件中只有一个按钮,代码中点击按钮后跳转到LoginActivity,布局xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:layout_width="match_parent"
              android:layout_height="match_parent"
              android:orientation="vertical"
    >
    <Button
        android:id="@+id/btn_login"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_margin="10dp"
        android:text="登录"
        android:textSize="20sp"
        android:textColor="@android:color/black"
        />
</LinearLayout>

然后在测试类中添加测试方法对点击事件进行测试:

@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowLog.class}, constants = BuildConfig.class, sdk = 23)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "org.json.*", "sun.security.*", "javax.net.*"})
@PrepareForTest({MainActivity.class})
public class MainActivityTest {

    @Before
    public void setUp(){
        //输出日志配置,用System.out代替Android的Log.x
        ShadowLog.stream = System.out;
    }

    @Test
    public void testOnClick() {
    	//创建Activity
        MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);
        Assert.assertNotNull(mainActivity);

        //模拟点击
        mainActivity.findViewById(R.id.btn_login).performClick();

        Intent expectedIntent = new Intent(mainActivity, LoginActivity.class);
        Intent actual = shadowOf(RuntimeEnvironment.application).getNextStartedActivity();
        //验证是否启动了期望的Activity
        Assert.assertEquals(expectedIntent.getComponent(), actual.getComponent());
    }
}

其中@RunWith(RobolectricTestRunner.class)指定Robolectric运行器,不用多说了。

@Config(shadows = {ShadowLog.class}, constants = BuildConfig.class, sdk = 23)通过配置shadows = {ShadowLog.class}和在@Before函数中指定ShadowLog.stream = System.out是为了用javaSystem.out代替AndroidLog输出,这样就能在run时的控制台看到Android的日志输出了。

@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "org.json.*", "sun.security.*", "javax.net.*"})通过PowerMockIgnore注解定义所忽略的package路径,防止所定义的package路径下的class类被PowerMockito测试框架mock。

在setUp()方法中调用MockitoAnnotations.initMocks(this);初始化PowerMockito注解,为@PrepareForTest(YourStaticClass.class)注解提供支持。

Robolectric.setupActivity是用来创建Activity, 当 Robolectric.setupActivity返回的时候,默认会调用Activity的生命周期: onCreate -> onStart -> onResume。

前面说过目标MainActivity中是点击按钮跳到一个LoginActivity, 为了测试这一点,我们可以检查当用户单击“登录”按钮时,是否启动了正确的Intent。因为Roboelectric是一个单元测试框架,实际上并不会真正的去启动MainActivity,但是我们可以检查MainActivity是否触发了正确的Intent,以达到验证目的。

然后我们右键运行testOnClick()方法,到这里如果你是第一次跑Robolectric的话,你会发现控制台显示Robolectric会开始下载东西,那么恭喜你已经成功开始入坑了。。。
在这里插入图片描述

3. 下载依赖jar

按照我的配置(sdk=23),它会下载一个名为android-all-6.0.1_r3-robolectric-r1.jar的jar包,这个包的体积很大大概有60多M,几乎很难下载下来,下载的地址是 https://oss.sonatype.org/content/groups/public, Robolectric默认会从这个地址去下载,但是真的很慢,即便你用浏览器下载速度也只有几kb。。。解决方法是直接到阿里云的maven仓库上去下载,在浏览器输入 https://maven.aliyun.com/mvn/search,
在这里插入图片描述
然后搜索框输入6.0.1_r3-robolectric-r1, 找到需要的jar包下载:
在这里插入图片描述
不得不说阿里云这个下载速度杠杠的,可以达到2MB/s, 然后将下载后的jar包复制到你的C盘:C:\Users\用户名.m2\repository\org\robolectric\android-all\6.0.1_r3-robolectric-r1这个目录下就可以了,如果这个目录下有temp文件(之前AS下载的部分临时文件),先删除掉就可以了。还有一个方法就是修改Robolectric的默认下载仓库地址,把oss.sonatype.org改成阿里云maven仓库,需要自定义一个RobolectricTestRunner 来覆盖设置默认的下载地址:

public class MyRobolectricTestRunner extends RobolectricTestRunner {
    public MyRobolectricTestRunner(Class<?> testClass) throws InitializationError {
        super(testClass);

        // 从源码知道MavenDependencyResolver默认以RoboSettings的repositoryUrl和repositoryId为默认值,因此只需要对RoboSetting进行赋值即可
        RoboSettings.setMavenRepositoryId("alimaven");
        RoboSettings.setMavenRepositoryUrl("http://maven.aliyun.com/nexus/content/groups/public/");
    }
}

然后测试类使用这个运行器就可以,@RunWith(MyRobolectricTestRunner.class),具体可以参考 加速Robolectric下载依赖库 这篇文章,不过这个方法我没有试过,我是直接下载的jar包,省事。但是我估计他这个方法应该只针对Robolectric 3.0+的版本有效,因为我发现Robolectric 4.0+的版本中已经没有RoboSettings的这两个方法了。另外你也可以在gradle中尝试开头提到的robolectric.dependency.repo.url配置,不过这个方法我也没有尝试成功。

好了,然后我们再次右键去运行testOnClick()方法:
在这里插入图片描述
可以看到运行成功了,而且Android的Log也可以在控制台看到了。在历经九九八十一难之后,终于测试成功了!不容易。。

4. 验证Toast显示

同样是上面的代码,我们点击按钮时弹出一个Toast然后去测试Toast是否已经显示:

 @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_login:
                Toast.makeText(this, "测试", Toast.LENGTH_SHORT).show();
                break;
            default:
                break;
        }
    }

测试类:

    @Test
    public void testToast() {
        MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);
        mainActivity.findViewById(R.id.btn_login).performClick();
        
        // 判断Toast已经弹出
        assertNotNull(ShadowToast.getLatestToast());
        //验证捕获的最近显示的Toast
        Assert.assertEquals("测试", ShadowToast.getTextOfLatestToast());

        //捕获所有已显示的Toast
        List<Toast> toasts = shadowOf(RuntimeEnvironment.application).getShownToasts();
        Assert.assertThat(toasts.size(), is(1));
        Assert.assertEquals(Toast.LENGTH_SHORT, toasts.get(0).getDuration());
    }

关于Shadow

前面的代码中都会出现一个shadow的关键词,Roboelectric通过一套测试API扩展了Android framework,这些API提供了额外的可配置性,并提供了对测试有用的Android组件的内部状态和历史的访问性。这种访问性就是通过Shadow类(影子类)来实现的,许多测试API都是对单个Android类的扩展,你可以使用Shadows.shadowOf()方法访问。

Roboelectric几乎针对所有的Android组件提供了一个Shadow开头的类,例如ShadowActivity、ShadowDialog、ShadowToast、ShadowApplication等等。Robolectric通过这些Shadow类来模拟Android系统的真实行为,当这些Android系统类被创建的时候,Robolectric会查找对应的Shadow类并创建一个Shadow类对象与原始类对象关联。每当系统类的方法被调用的时候,Robolectric会保证Shadow对应的方法会调用。每个Shadow对象都可以修改或扩展Android操作系统中相应类的行为。因此我们可以用Shadow类的相关方法对Android相关的对象进行测试。

更多关于Shadow的知识请参考官方介绍:Shadows

5. 验证Dialog显示
	@Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_login:
                showDialog();
                break;
            default:
                break;
        }
    }

    public void showDialog(){
        AlertDialog alertDialog = new AlertDialog.Builder(this)
                .setMessage("测试showDialog")
                .setTitle("提示")
                .create();
        alertDialog.show();
    }

测试类:

    @Test
    public void showDialog() {
        MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);

        // 捕获最近显示的Dialog
        AlertDialog dialog = ShadowAlertDialog.getLatestAlertDialog();
        // 判断Dialog尚未弹出
        Assert.assertNull(dialog);

        //点击按钮
        mainActivity.findViewById(R.id.btn_login).performClick();

        // 捕获最近显示的Dialog
        dialog = ShadowAlertDialog.getLatestAlertDialog();
        // 判断Dialog已经弹出
        Assert.assertNotNull(dialog);
        // 获取Shadow类进行验证
        ShadowAlertDialog shadowDialog = Shadows.shadowOf(dialog);
        Assert.assertEquals("测试showDialog", shadowDialog.getMessage());
    }
6. 验证UI组件状态

以CheckBox为例:

    @Test
    public void testCheckBoxState() throws Exception {
        MainActivity activity = Robolectric.setupActivity(MainActivity.class);
        CheckBox checkBox = activity.findViewById(R.id.checkbox);
        // 验证CheckBox初始状态
        Assert.assertFalse(checkBox.isChecked());

        // 点击按钮反转CheckBox状态
        checkBox.performClick();
        // 验证状态是否正确
        Assert.assertTrue(checkBox.isChecked());

        // 点击按钮反转CheckBox状态
        checkBox.performClick();
        // 验证状态是否正确
        Assert.assertFalse(checkBox.isChecked());
    }
7. 验证Fragment

目标类:

public class TestFragment extends Fragment {
    View content;

    public static TestFragment newInstance(Bundle arguments) {
        TestFragment fragment = new TestFragment();
        fragment.setArguments(arguments);
        return fragment;
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        content = inflater.inflate(R.layout.fragment_test, container, false);
        initView();
        return content;
    }

    private void initView() {
        TextView nameText = content.findViewById(R.id.tv_name);
        if (getArguments() != null) {
            nameText.setText(getArguments().getString("name"));
        }
    }
}

TestFragment 中做了一件很简单的事,将arguments参数中传来的name参数值显示到TextView上面。
测试方法:

    @Test
    public void testFragment() throws Exception {
        Bundle bundle = new Bundle();
        bundle.putString("name", "测试");
        TestFragment testFragment = TestFragment.newInstance(bundle);
        // 把Fragment添加到Activity中,这会调用Fragment的生命周期
        SupportFragmentTestUtil.startFragment(testFragment);
        
        // 验证Fragment是否显示布局
        Assert.assertNotNull(testFragment.getView());

        TextView textView = testFragment.getView().findViewById(R.id.tv_name);
        Assert.assertNotNull(textView);
        // 验证设置的参数值是否显示了
        Assert.assertEquals("测试", textView.getText());
    }

这里SupportFragmentTestUtil.startFragment()是会默认将Fragment添加到一个空的FragmentActivity当中,这样方便测试,当然你可以写一个Activity去添加Fragment,然后通过mActivity.getSupportFragmentManager().getFragments();去获取。

这里我写的TestFragment是继承的android.support.v4.app.Fragment,如果你是用android.app.Fragment那么这里使用FragmentTestUtil.startFragment();去启动。
如果使用supportv4支持库的话,gradle需要另外添加Robolectric对supportv4的依赖库:

testImplementation 'org.robolectric:shadows-supportv4:3.8'

Robolectric中除了主依赖库外,如果你想使用下面的测试库,需要单独添加依赖。

SDK PackageRobolectric Add-On Package
com.android.support.support-v4org.robolectric:shadows-supportv4
com.android.support.multidexorg.robolectric:shadows-multidex
com.google.android.gms:play-servicesorg.robolectric:shadows-playservices
org.apache.httpcomponents:httpclientorg.robolectric:shadows-httpclient
8. 访问资源文件

使用RuntimeEnvironment.application可以获取到Application对象,方便我们使用。比如访问资源文件。

    @Test
    public void testResource() throws Exception {
        Application application = RuntimeEnvironment.application;
        String appName = application.getString(R.string.app_name);
        Assert.assertEquals("AndroidUnitTestApplication", appName);
    }
9. 验证Activity生命周期

利用ActivityController我们可以让Activity执行相应的生命周期方法,如:

public class MainActivity extends Activity  {
    private final static String TAG = MainActivity.class.getSimpleName();
    private String lifecycle;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        lifecycle = "onCreate";
        Log.e(TAG, "onCreate: ");
    }

    public String getLifecycleState(){
        return lifecycle;
    }

    @Override
    protected void onStart() {
        super.onStart();
        Log.e(TAG, "onStart: ");
        lifecycle = "onStart";
    }

    @Override
    protected void onResume() {
        super.onResume();
        Log.e(TAG, "onResume: ");
        lifecycle = "onResume";
    }

    @Override
    protected void onPause() {
        super.onPause();
        Log.e(TAG, "onPause: ");
        lifecycle = "onPause";
    }

    @Override
    protected void onStop() {
        super.onStop();
        Log.e(TAG, "onStop: ");
        lifecycle = "onStop";
    }

    @Override
    protected void onRestart() {
        super.onRestart();
        Log.e(TAG, "onRestart: ");
        lifecycle = "onRestart";
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        Log.e(TAG, "onDestroy: ");
        lifecycle = "onDestroy";
    }
}

测试类:

    @Test
    public void testLifecycle() throws Exception {
        // 创建Activity控制器
        ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);
        MainActivity activity = controller.get();
        assertNull(activity.getLifecycleState());

        // 调用Activity的performCreate方法
        controller.create();
        assertEquals("onCreate", activity.getLifecycleState());

        // 调用Activity的performStart方法
        controller.start();
        assertEquals("onStart", activity.getLifecycleState());

        // 调用Activity的performResume方法
        controller.resume();
        assertEquals("onResume", activity.getLifecycleState());

        // 调用Activity的performPause方法
        controller.pause();
        assertEquals("onPause", activity.getLifecycleState());

        // 调用Activity的performStop方法
        controller.stop();
        assertEquals("onStop", activity.getLifecycleState());

        // 调用Activity的performRestart方法
        controller.restart();
        // 注意此处应该是onStart,因为performRestart不仅会调用restart,还会调用onStart
        assertEquals("onStart", activity.getLifecycleState());

        // 调用Activity的performDestroy方法
        controller.destroy();
        assertEquals("onDestroy", activity.getLifecycleState());
    }

通过ActivityController,我们可以模拟各种生命周期的变化。但是要注意,我们虽然可以随意调用Activity的生命周期,但是Activity生命周期切换有自己的检测机制,我们要遵循Activity的生命周期规律。比如,如果当前Activity并非处于stop状态,测试代码去调用了controller.restart方法,此时Activity是不会回调onRestart和onStart的。

10. 验证Intent参数传递
    @Test
    public void testStartActivityWithIntent() throws Exception {
        Intent intent = new Intent();
        intent.putExtra("test", "HelloWorld");
        Activity activity = Robolectric.buildActivity(MainActivity.class, intent).create().get();
        Bundle extras = activity.getIntent().getExtras();
        assertNotNull(extras);
        assertEquals("HelloWorld", extras.getString("test"));
    }
11. 验证savedInstanceState
public class MainActivity extends Activity {
    private final static String TAG = MainActivity.class.getSimpleName();
    Bundle savedInstanceState;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        this.savedInstanceState = savedInstanceState;
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        if (savedInstanceState != null) {
            Log.e(TAG, "onCreate: isCheckBoxChecked = "+savedInstanceState.getBoolean("isCheckBoxChecked"));
        }
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        this.savedInstanceState = savedInstanceState;
        super.onRestoreInstanceState(savedInstanceState);
        if (savedInstanceState != null) {
            Log.e(TAG, "onCreate: isCheckBoxChecked = "+savedInstanceState.getBoolean("isCheckBoxChecked"));
        }
    }

    public Bundle getSavedInstanceState() {
        return savedInstanceState;
    }
 }

有两种验证方法,一种是验证onRestoreInstanceState(Bundle savedInstanceState)的调用,还有一种是验证onCreate(Bundle savedInstanceState)的调用。

    @Test
    public void testActivityWithSavedState() throws Exception {
        Bundle savedState = new Bundle();
        savedState.putBoolean("isCheckBoxChecked", true);
        MainActivity activity = Robolectric.buildActivity(MainActivity.class).create(savedState).get();
        Bundle savedInstanceState = activity.getSavedInstanceState();
        assertNotNull(savedInstanceState);
        assertTrue(savedInstanceState.getBoolean("isCheckBoxChecked"));
    }
    @Test
    public void testActivityWithSavedState2() throws Exception {
        Bundle savedState = new Bundle();
        savedState.putBoolean("isCheckBoxChecked", true);
        MainActivity activity = Robolectric.buildActivity(MainActivity.class).create().restoreInstanceState(savedState).get();
        Bundle savedInstanceState = activity.getSavedInstanceState();
        assertNotNull(savedInstanceState);
        assertTrue(savedInstanceState.getBoolean("isCheckBoxChecked"));
    }

这两种方法分别是通过ActivityController.create(savedState)ActivityController.create().restoreInstanceState(savedState)来实现,这会分别回调Activity的onCreate(Bundle savedInstanceState)onRestoreInstanceState(Bundle savedInstanceState)方法。

12. 验证BroadcastReceiver

我们先在Activity中注册一个广播:

public class MainActivity extends Activity {
    private final static String TAG = MainActivity.class.getSimpleName();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    
    public static final String ACTION_TEST = "com.fly.unit.test";
    public static final String ACTION_TEST2 = "com.fly.unit.test2";
    private BroadcastReceiver mTestBroadcastReceiver = new BroadcastReceiver() {

        @Override
        public void onReceive(Context context, Intent intent) {
            Log.e(TAG, "mTestBroadcastReceiver onReceive: " + intent.getAction());
            if (ACTION_TEST.equals(intent.getAction())) {
                String name = intent.getStringExtra("name");
                PreferenceManager.getDefaultSharedPreferences(context)
                       .edit()
                       .putString("name", name)
                       .apply();
            }
        }
    };
    
    @Override
    protected void onResume() {
        super.onResume();
        IntentFilter intentFilter = new IntentFilter(ACTION_TEST);
        intentFilter.addAction(ACTION_TEST2);
        LocalBroadcastManager.getInstance(this)
                .registerReceiver(mTestBroadcastReceiver, intentFilter);
    }

    @Override
    protected void onPause() {
        super.onPause();
        LocalBroadcastManager.getInstance(this).unregisterReceiver(mTestBroadcastReceiver);
    }
}

我们使用LocalBroadcastManageronResume()方法中注册了拥有两个Action的广播,然后在onPause中反注册了这个广播。在onReceive方法中只针对ACTION_TEST这个action做了sp保存的操作。下面测试类进行验证:

    @Test
    public void testBroadcastReceive() throws Exception {
        MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);
        LocalBroadcastManager broadcastManager = ShadowLocalBroadcastManager.getInstance(mainActivity);
        Intent intent = new Intent(MainActivity.ACTION_TEST);
        intent.putExtra("name", "小明");
        //发送广播
        broadcastManager.sendBroadcast(intent);

        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(mainActivity);
        //通过验证sp保存的值验证广播是否收到
        Assert.assertEquals("小明", preferences.getString("name", null));

        intent = new Intent(MainActivity.ACTION_TEST2);
        intent.putExtra("name", "小红");
        //再次发送一个广播
        broadcastManager.sendBroadcast(intent);
        //验证新的参数值是否被保存(由于广播中我们没有对这个Action处理,因此sp中的name应该还是上次的)
        Assert.assertNotEquals("小红", preferences.getString("name", null));
    }

同时控制台也能看到log输出:
在这里插入图片描述
同样我们也可以通过控制生命周期验证广播是否被注销了,上面代码MainActivity是在onPause()方法中反注册了广播,如果我们忘记了反注册广播这一点,那么在onPause()方法之后发送广播应该还是会收到。

    @Test
    public void testBroadcastReceive2() throws Exception {
        ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);
        controller.setup();
        controller.pause();
        LocalBroadcastManager broadcastManager = ShadowLocalBroadcastManager.getInstance(RuntimeEnvironment.application);
        broadcastManager.sendBroadcast(new Intent(MainActivity.ACTION_TEST));
        //自行验证,例如可以看是否有log输出
    }

验证静态广播是否注册:

        <receiver android:name=".receiver.MyReceiver"
            android:exported="false">
            <intent-filter>
                <action android:name="com.test.receiver.MyReceiver"/>
            </intent-filter>
        </receiver>
    @Test
    public void testBroadcastReceive3() {
        Intent intent = new Intent("com.test.receiver.MyReceiver");
        PackageManager packageManager = RuntimeEnvironment.application.getPackageManager();
        List<ResolveInfo> resolveInfos = packageManager.queryBroadcastReceivers(intent, 0);
        assertNotNull(resolveInfos);
        assertThat(resolveInfos.size(), Matchers.greaterThan(0));
    }
13. 验证Service

跟Activity一样可以通过ServiceController验证Service的生命周期:

public class MyService extends Service {
    private final String TAG = MyService.class.getSimpleName();

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        Log.d(TAG, "onBind");
        return null;
    }

    @Override
    public void onCreate() {
        super.onCreate();
        Log.d(TAG, "onCreate");
    }

    @Override
    public boolean onUnbind(Intent intent) {
        Log.d(TAG, "onUnbind");
        return super.onUnbind(intent);
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        Log.d(TAG, "onDestroy");
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        Log.d(TAG, "onStartCommand");
        return super.onStartCommand(intent, flags, startId);
    }
}

测试代码:

    @Test
    public void testServiceLifecycle() throws Exception {
        //验证Service生命周期
        ServiceController<MyService> controller = Robolectric.buildService(MyService.class);
        controller.create();
        // verify something

        controller.startCommand(0, 0);
        // verify something

        controller.bind();
        // verify something

        controller.unbind();
        // verify something

        controller.destroy();
        // verify something
    }

控制台输出:
在这里插入图片描述
Robolectric.setupActivity一样,可以调用Robolectric.setupService直接创建一个Service实例:

MyService myService = Robolectric.setupService(MyService.class);
//verify somthing

但是Robolectric.setupService只会调用Service的onCreate()方法。

也可以像验证Activity的启动那样,验证在某个时刻是否启动了目标Service(如验证点击按钮启动一个Service):

    @Test
    public void testServiceCreate() {
        final MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);
        mainActivity.findViewById(R.id.btn_login).performClick();
        Intent intent = new Intent(mainActivity, MyService.class);
        Intent actual = ShadowApplication.getInstance().getNextStartedService();
        Assert.assertEquals(intent.getComponent(), actual.getComponent());
    }

验证在Activity的onCreate方法中启动了Service:

    @Test
    public void testServiceCreate() {
        ActivityController<MainActivity> controller = Robolectric.buildActivity(MainActivity.class);
        MainActivity mainActivity = controller.create().get();
        Intent intent = new Intent(mainActivity, MyService.class);
        Intent actual = ShadowApplication.getInstance().getNextStartedService();
        Assert.assertEquals(intent.getComponent(), actual.getComponent());
    }

测试IntentService:

public class SampleIntentService extends IntentService {
    public SampleIntentService() {
        super("SampleIntentService");
    }
    @Override
    protected void onHandleIntent(Intent intent) {
        SharedPreferences.Editor editor = getApplicationContext().getSharedPreferences(
                "example", Context.MODE_PRIVATE).edit();
        editor.putString("SAMPLE_DATA", "sample data");
        editor.apply();
    }
}
    @Test
    public void addsDataToSharedPreference() {
        Application application = RuntimeEnvironment.application;
        RoboSharedPreferences preferences = (RoboSharedPreferences) application
                .getSharedPreferences("example", Context.MODE_PRIVATE);
        Intent intent =  new Intent(application, SampleIntentService.class);
        SampleIntentService registrationService = new SampleIntentService();

        registrationService.onHandleIntent(intent);

        assertNotSame("", preferences.getString("SAMPLE_DATA", ""), "");
    }
14. 验证SharedPreferences
public class SharePreferManager {
    Context context;

    public SharePreferManager(Context context) {
        this.context = context;
    }

    public void saveName(String key, String name) {
        SharedPreferences sp = context.getSharedPreferences("test", Context.MODE_PRIVATE);
        sp.edit().putString(key, name).apply();
    }

    public String getName(String key) {
        SharedPreferences sp = context.getSharedPreferences("test", Context.MODE_PRIVATE);
        return sp.getString(key, null);
    }
}

测试类:

@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowLog.class}, constants = BuildConfig.class, sdk = Build.VERSION_CODES.M)
public class SharePreferManagerTest {
    SharePreferManager spManager;

    @Before
    public void setUp() throws Exception {
        spManager = new SharePreferManager(RuntimeEnvironment.application);
    }

    @Test
    public void testSp() {
        spManager.saveName("name", "小明");
        Assert.assertEquals("小明", spManager.getName("name"));
    }
}

需要在同一个方法里进行保存和读取的验证,如果在单独的方法中验证读取的话,最好在提前setUp中保存,否则可能读取的是null不是期望的值。

15. 验证File操作
public class FileManager {

    public void write(String path, String content) {
        File file = new File(path);
        BufferedOutputStream outputStream = null;
        try {
            outputStream = new BufferedOutputStream(new FileOutputStream(file));
            outputStream.write(content.getBytes());
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public String read(String path) {
        File file = new File(path);
        if (file.exists()) {
            BufferedInputStream inputStream = null;
            try {
                inputStream = new BufferedInputStream(new FileInputStream(file));
                byte[] bytes = new byte[2048];
                StringBuilder sb = new StringBuilder();
                int count = 0;
                while ((count =inputStream.read(bytes)) != -1) {
                    sb.append(new String(bytes, 0, count, "utf-8"));
                }
                return sb.toString();
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                if (inputStream != null) {
                    try {
                        inputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return null;
    }
}

测试类:

@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowLog.class}, constants = BuildConfig.class, sdk = Build.VERSION_CODES.M)
public class FlieManagerTest {
    private final static String TAG = FlieManagerTest.class.getSimpleName();
    FileManager fileManager;
    String SDCARD_ROOT;

    @Before
    public void setUp() throws Exception {
        ShadowLog.stream = System.out;
        fileManager = new FileManager();
        SDCARD_ROOT = Environment.getExternalStorageDirectory().getPath();
    }

    @Test
    public void testFile() {
        Log.e(TAG, "testFile: SDCARD_ROOT = " + SDCARD_ROOT );
        String dir = SDCARD_ROOT+"/test/aaa";
        //创建目录
        new File(dir).mkdirs();
        //验证目录是否创建成功
        Assert.assertTrue(new File(dir).exists());

        String fullName = dir+"/bbb.txt";
        fileManager.write(fullName, "测试testFile");

        File file = new File(fullName);
        //验证文件是否存在
        Assert.assertTrue(file.exists());
        Log.e(TAG, "testFile: file = " + file.getPath());

        //验证文件内容是否正确
        Assert.assertEquals("测试testFile", fileManager.read(fullName));
		
		//删除文件	
        file.delete();
        //验证是否删除成功
        Assert.assertFalse(file.exists());
    }

}

控制台输出:
在这里插入图片描述
运行测试方法后我们发现,其实在Robolectric中我们调用Environment.getExternalStorageDirectory()时返回的是一个本地C盘的临时缓存目录,然后后续的文件操作都是在这个目录下进行的。

16. 验证Assets文件读取
public class AssetsReader {

    AssetManager assetManager;

    public AssetsReader(Context context) {
        this.assetManager = context.getAssets();
    }

    public String read(String fileName) {
        try {
            InputStream inputStream = assetManager.open(fileName);
            StringBuilder sb = new StringBuilder();
            byte[] buffer = new byte[1024];
            int hasRead;
            while ((hasRead = inputStream.read(buffer, 0, buffer.length)) > -1) {
                sb.append(new String(buffer, 0, hasRead));
            }
            inputStream.close();
            return sb.toString();
        } catch (IOException e) {
            e.printStackTrace();
        }
        return "";
    }
}

src/main/assets/下新建测试文件:
在这里插入图片描述
测试类:

@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowLog.class}, constants = BuildConfig.class, sdk = Build.VERSION_CODES.M)
public class AssetsReaderTest {
    private final static String TAG = AssetsReaderTest.class.getSimpleName();
    AssetsReader assetsReader;

    @Before
    public void setUp() throws Exception {
        ShadowLog.stream = System.out;
        assetsReader = new AssetsReader(RuntimeEnvironment.application);
    }

    @Test
    public void testAsset() throws Exception {
        String json = assetsReader.read("test.json");
        Log.e(TAG, "read: " + json);
        JSONObject jsonObject = new JSONObject(json);
        Assert.assertThat(jsonObject.getString("name"), is("小明"));
        Assert.assertThat(jsonObject.getInt("id"), is(123));
    }
}

控制台输出:
在这里插入图片描述

17. 验证SQLite数据库操作
public class SQLiteHelper extends SQLiteOpenHelper {
    static String CREATE_TABLE_NOT_EXISTS = "CREATE TABLE IF NOT EXISTS ";

    public SQLiteHelper(Context context, String name, int version) {
        super(context, name, null, version);
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        String sql = CREATE_TABLE_NOT_EXISTS +
                "person(_id integer primary key autoincrement, name varchar, id varchar)";
        sqLiteDatabase.execSQL(sql);
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int i, int i1) {
    }
}
public class DbManager {
    private SQLiteHelper mDbHelper;
    private static DbManager INSTANCE = null;
    private final static String DB_NAME = "AppData";
    private final static int DB_VERSION = 1;

    private DbManager(Context context){
        init(context);
    }

    public static DbManager getInstance(Context context) {
        if (INSTANCE == null) {
            INSTANCE = new DbManager(context);
        }
        return INSTANCE;
    }

    public void init(Context context) {
        mDbHelper = new SQLiteHelper(context, DB_NAME, DB_VERSION);
    }

    public boolean savePerson(Person person) {
        SQLiteDatabase database = mDbHelper.getWritableDatabase();
        ContentValues contentValues = new ContentValues();
        contentValues.put("name", person.getName());
        contentValues.put("id", person.getId());
        if (judgeRepeat(person.getId())) {
            return database.update("person", contentValues, "id = ?", new String[]{person.getId()}) > 0;
        } else {
            return database.insert("person", null, contentValues) != -1;
        }
    }

    public Person getPerson(String id) {
        Person person = null;
        Cursor cursor = null;
        try {
            SQLiteDatabase database = mDbHelper.getWritableDatabase();
            cursor = database.query("person", null, "id = ?", new String[] {id}, null, null, null);
            if (cursor != null && cursor.moveToNext()) {
                person = new Person();
                person.setName(cursor.getString(cursor.getColumnIndex("name")));
                person.setId(cursor.getString(cursor.getColumnIndex("id")));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return person;
    }

    public boolean deletePerson(String id) {
        SQLiteDatabase database = mDbHelper.getWritableDatabase();
        return database.delete("person", "id = ?", new String[] {id}) > 0;
    }

    public  boolean judgeRepeat(String id) {
        boolean exist = false;
        Cursor cursor = null;
        try {
            SQLiteDatabase database = mDbHelper.getWritableDatabase();
            cursor = database.query("person", null, "id = ?", new String[] {id}, null, null, null);
            if (cursor != null && cursor.moveToNext()) {
                exist = true;
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
        return exist;
    }
    
	public boolean isTableExist(String tableName) {
        boolean result = false;
        String sql = "select count(*) as c from sqlite_master where type ='table' and name ='"+tableName+"'";
        try {
            SQLiteDatabase database = mDbHelper.getReadableDatabase();
            Cursor cursor = database.rawQuery(sql, null);
            if (cursor.moveToNext()) {
                int count = cursor.getInt(0);
                if (count > 0) {
                    result = true;
                }
            }
            cursor.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }
}

测试类:

@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowLog.class}, constants = BuildConfig.class, sdk = Build.VERSION_CODES.M)
public class DbManagerTest {
    private final static String TAG = DbManagerTest.class.getSimpleName();
    DbManager dbManager;

    @Before
    public void setUp() throws Exception {
        ShadowLog.stream = System.out;
        dbManager = DbManager.getInstance(RuntimeEnvironment.application);
    }

    @Test
    public void testDb() {
        File dbFile = RuntimeEnvironment.application.getDatabasePath(DbManager.DB_NAME);
        //验证数据库文件是否存在
        Assert.assertTrue(dbFile.exists());

        //验证person表是否存在
        Assert.assertTrue(dbManager.isTableExist("person"));

        Person person = new Person();
        person.setName("小明");
        person.setId("123");
        //验证是否保存成功
        Assert.assertTrue(dbManager.savePerson(person));

        //验证查询数据
        Person person2 = dbManager.getPerson("123");
        Assert.assertNotNull(person2);
        Assert.assertEquals("小明", person2.getName());

        //验证删除数据
        Assert.assertTrue(dbManager.deletePerson("123"));
        Assert.assertNull(dbManager.getPerson("123"));
    }
}
18. 验证RecyclerView
public class PersonListActivity extends Activity implements BaseRecyclerVewAdapter.OnItemClickListener {
    private RecyclerView mPersonRecyclerView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_person_list);
        initView();
    }

    private void initView() {
        mPersonRecyclerView = (RecyclerView) findViewById(R.id.rv_person);
        mPersonRecyclerView.setLayoutManager(new LinearLayoutManager(this));
        mPersonRecyclerView.addItemDecoration(new DividerDecoration(this));
        List<Person> list = new ArrayList<>(0);
        for (int i = 0; i < 20; i++) {
            list.add(new Person("姓名"+i));
        }
        PersonAdapter adapter = new PersonAdapter(list);
        mPersonRecyclerView.setAdapter(adapter);
        adapter.setOnItemClickListener(this);
    }

    @Override
    public void onItemClick(BaseRecyclerVewAdapter adapter, View view, int position) {
        Person item = ((PersonAdapter) adapter).getItem(position);
        if (item != null) {
            String name = item.getName();
            Toast.makeText(this, name, Toast.LENGTH_LONG).show();
        }
    }
}

在PersonListActivity中通过RecyclerView显示一个列表,这里Adapter是使用的BRVAH开源库:

public class PersonAdapter extends BaseRecyclerVewAdapter<Person, BaseViewHolder> {
    public PersonAdapter(@Nullable List<Person> data) {
        super(R.layout.item_person_list, data);
    }
    @Override
    protected void convert(BaseViewHolder helper, Person item) {
        helper.setText(R.id.tv_name, item.getName());
    }
}

测试方法:

    @Test
    public void testRecyclerView() {
        PersonListActivity activity = Robolectric.setupActivity(PersonListActivity.class);
        Assert.assertNotNull(activity);
        RecyclerView recyclerView = activity.findViewById(R.id.rv_person);
        //验证RecyclerView不为空
        Assert.assertNotNull(recyclerView);
        RecyclerView.Adapter adapter = recyclerView.getAdapter();
        //验证Adapter不为空
        Assert.assertNotNull(adapter);
        //验证Adapter类型
        Assert.assertTrue(adapter instanceof PersonAdapter);
        //验证Adapter数据量
        Assert.assertEquals(20, ((PersonAdapter) adapter).getData().size());
        Person item = ((PersonAdapter) adapter).getItem(5);
        //验证Adapter某一条数据
        Assert.assertNotNull(item);
        Assert.assertEquals("姓名5", item.getName());
        LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
        //验证RecyclerView点击某一个Item的响应
        layoutManager.findViewByPosition(5).performClick();
        Assert.assertEquals("姓名5", ShadowToast.getTextOfLatestToast());
    }

当然也可以不用Activity的数据,在测试方法中创建测试List数据做验证:

   @Test
    public void testRecyclerView() {
        PersonListActivity activity = Robolectric.setupActivity(PersonListActivity.class);
        Assert.assertNotNull(activity);
        RecyclerView recyclerView = activity.findViewById(R.id.rv_person);
        Assert.assertNotNull(recyclerView);

        //测试数据
        List<Person> list = new ArrayList<>(0);
        for (int i = 0; i < 20; i++) {
            list.add(new Person("姓名"+i));
        }
        PersonAdapter adapter = new PersonAdapter(list);
        adapter.setOnItemClickListener(activity);
        recyclerView.setAdapter(adapter);

        Person item = adapter.getItem(5);
        Assert.assertNotNull(item);
        Assert.assertEquals("姓名5", item.getName());
        LinearLayoutManager layoutManager = (LinearLayoutManager) recyclerView.getLayoutManager();
        layoutManager.findViewByPosition(5).performClick();
        Assert.assertEquals("姓名5", ShadowToast.getTextOfLatestToast());
    }

这时要先把Activity生命周期方法中设置的数据注释掉再运行。

19. 验证DelayedRunnable

我们在UI主线程有时会执行一些postDelayed Runnable操作,例如点击按钮时postDelayed一个Runnable来设置UI状态:

    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.btn_login:
                mLoginBtn.postDelayed(new Runnable() {
                    @Override
                    public void run() {
                        mLoginBtn.setText("测试");
                    }
                }, 500);
				//或者类似这样的:
//                new Handler().postDelayed(new Runnable() {
//                    @Override
//                    public void run() {
//                        mLoginBtn.setText("测试");
//                    }
//                }, 500);
                break;
            default:
                break;
        }
    }

这种操作虽然最终也会发生在UI主线程上进行,但是发生并不是即时的,如果你像之前一样使用下面的代码进行测试,则会验证失败:

    @Test
    public void testPostRunnable() {
        MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);
        Assert.assertNotNull(mainActivity);
        Button btn = mainActivity.findViewById(R.id.btn_login);
        btn.performClick();
        Assert.assertEquals("测试", btn.getText());
    }

这时可以通过 ShadowLooper.runUiThreadTasksIncludingDelayedTasks()或者ShadowLooper.runMainLooperOneTask()方法使所有UI线程上的延时任务即刻发生,我们在performClick()方法之后调这个方法,然后就可以正常断言了:

    @Test
    public void testPostRunnable() {
        MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);
        Assert.assertNotNull(mainActivity);

        //模拟点击
        Button btn = mainActivity.findViewById(R.id.btn_login);
        btn.performClick();

        ShadowLooper.runUiThreadTasksIncludingDelayedTasks();
        //使用下面这句也可以做到
        //ShadowLooper.runMainLooperOneTask();

        Assert.assertEquals("测试", btn.getText());
    }
20. 自定义Shadow类的使用

前面提到Robolectric中提供了好多Shadow开头的类,当然我们也可以自己定义某个类的Shadow类,自定义Shadow类需要注意一些点:

  • Shadow类需要通过@Implements注解与原始类关联在一起
  • 若原始类有有参构造方法,可以选择在Shadow类中定义public void类型的名为__constructor__的方法,且方法参数与原始类的构造方法参数一致
  • 定义与原始类方法签名一致的方法,在里面重写实现,Shadow方法需用@Implementation进行注解
  • Robolectric支持原始类上的所有方法生成Shadow方法,包括private、static、finalnative
  • Shadow方法必须实现最原始类的定义方法,如 ViewGroup extends View, 如果你想shadow setEnabled()方法那你应该去shadow View的而不是ViewGroup的,否则当你对ViewGroup调用setEnabled()将不起作用。

原始类:

public class Person {
    public String name;
    public int sex;
    public int age;

    public Person(String name) {
        this.name = name;
    }

    public Person() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getSex() {
        return sex;
    }

    public void setSex(int sex) {
        this.sex = sex;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String eat(String food){
        return food;
    }
}

对应的Shadow类:

/**
 * Person的影子类
 */
@Implements(Person.class)
public class ShadowPerson {
    /**
     * 通过@RealObject注解可以访问原始对象,但注意,通过@RealObject注解的变量调用方法,依然会调用Shadow类的方法,而不是原始类的方法
     * 只能用来访问原始类的field
     */
    @RealObject
    Person realBean;

    /**
     * 需要一个无参构造方法
     */
    public ShadowPerson() {
    }

    /**
     * 对应原始类的有参构造方法,必须保持参数一致
     */
    public void __constructor__(String name) {
        realBean.name = name;
    }

    /**
     * 可以选择覆写某个方法
     */
    @Implementation
    public int getAge(){
        return 18;
    }

    @Implementation
    public String getName() {
        return "小明";
    }

    @Implementation
    public int getSex() {
        return 1;
    }

    @Implementation
    public void setName(String name) {
        realBean.name = name;
    }

    @Implementation
    public void setSex(int sex) {
        realBean.sex = sex;
    }

    @Implementation
    public void setAge(int age) {
        realBean.age = age;
    }

    /**
     * 可以在原始类基础上添加而外的支持方法
     */
    public String descripOrigan() {
        return realBean.name +","+ realBean.sex +"," + realBean.age;
    }
}

注意,Shadow类中可以定义一个原始类成员对象的变量,通过@RealObject注解,这样Shadow类就能访问原始类的field了,但是通过@RealObject注解的变量调用方法时,依然会调用Shadow类的方法,而不是原始类的方法,如果想访问原始类字段只能通过field来访问。

即不能写下面这样的方法,因为realBean.getName()就会调用Shadow本身的getName()这会导致stackoverflow。

    @Implementation
    public String getName() {
        return realBean.getName();
    }

应用自定义的Shadow类:
通过@Config注解指定shadows ,可以添加多个:

@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowLog.class, ShadowPerson.class})
public class ShadowTest {
    private final static String TAG = ShadowTest.class.getSimpleName();

    @Before
    public void setUp(){
        //输出日志配置,用System.out代替Android的Log.x
        ShadowLog.stream = System.out;
    }

       @Test
    public void testShadowShadow(){
        Person person = new Person();
        //实际上调用的是ShadowPerson的方法
        assertEquals("小明", person.getName());
        assertEquals(18, person.getAge());
        assertEquals(1, person.getSex());

        //获取Person对象对应的Shadow对象
        ShadowPerson shadowPerson = Shadow.extract(person);
        assertEquals("小明", shadowPerson.getName());
        assertEquals(18, shadowPerson.getAge());
        assertEquals(1, shadowPerson.getSex());

        person.setName("小张");
        person.setAge(20);
        person.setSex(2);
        Log.d(TAG, shadowPerson.descripOrigan());
        assertEquals("小张,2,20", shadowPerson.descripOrigan());
    }

}

注意,系统的Shadow类是通过Shadows.shadowOf()获取,而自定义的Shadow类需要用ShadowExtractor.extract()来获取,获取之后进行类型转换。要记住一点,只要在原始类上调用的方法,都会对Shadow类进行调用,如果Shadow类覆写了该方法,那么一律以Shadow类覆写的方法返回值为最终结果(不管你对原始类对象如何操作)。

更多关于Shadow的知识请参考官方介绍:Shadows

21. 隔离Application

默认情况下,如果你的应用中Manifest文件中有指定Application类的话,Robolectric在运行时会创建该类的实例,Robolectric中有一个RuntimeEnvironment.application,你可以验证一下它跟你的Application类的实例是否一致:

public class App extends Application {
    private static App app;

    @Override
    public void onCreate() {
        super.onCreate();
        app = this;
        //第三方库的初始化操作
    }

    public static App getInstance() {
        return app;
    }
}
 @Test
 public void testApp() {
     Assert.assertEquals(App.getInstance(), RuntimeEnvironment.application);
 }

测试会发现RuntimeEnvironment.application跟App.getInstance()是同一个,也就是说在使用RuntimeEnvironment.application的实际上会去创建应用真实的Application类,而一般在实际的Application类的oncreate()方法中我们会去初始化第三方的库,这可能导致运行测试方法报错,比如有些第三方会调用static{ Library.load() }静态加载so库等。解决方法是为测试类配置一个空的Application类,通过@Config指定:

public class RobolectricApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
    }
}
@Config(application = RobolectricApp.class)

这时你会发现RuntimeEnvironment.application就是RobolectricApp 的实例。

另外Robolectric中有一个ShadowApplication类,你可以调用ShadowApplication.getInstance().getApplicationContext()得到的是和RuntimeEnvironment.application是相同的实例,ShadowApplication类中提供了大量的方法方便我们去做一些单元测试验证,如前面提到的验证是否启动了一个正确的Activity的Intent或者捕获最近的Toast等等。
在这里插入图片描述

22. 测试网络异步请求

网络请求部分的测试主要有两个目的:

  • 一个是验证我们的业务代码是否针对接口的请求结果做出了正确的响应,如:正常结果的处理逻辑、结果为空的处理逻辑以及异常情况下的处理逻辑,不同响应结果下我们的UI是否展示正确等
  • 另一个主要是针对后端接口本身的验证,如:我们的请求参数是否正确是否符合后端的要求、接口请求的地址是否正确是否能够访问通、接口是否能够返回期望的返回值和类型等

接口请求一般又分为同步请求和异步请求,在Android中我们一般是使用流行的网络请求框架进行异步请求,然后返回的UI线程进行处理,这里先介绍异步请求的测试。

我们通过一个MVP的例子来进行测试:

public class MusicChannelActivity extends Activity implements IMusicChannelView {
    private final static String TAG = MusicChannelActivity.class.getSimpleName();
    private MusicChannelPresenter presenter;
    private TextView mResultTextView;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_music_channel);
        mResultTextView = (TextView) findViewById(R.id.tv_result);
        init();
    }

    private void init() {
        presenter = new MusicChannelPresenter(this);
        presenter.requestMusicChannels();
    }

    @Override
    public void showMusicChannelList(List<ChannellistBean> list) {
        Log.e(TAG, "showMusicChannelList: " + list.size());
        mResultTextView.setText("请求成功");
    }

    @Override
    public void showEmptyView() {
        Log.e(TAG, "showEmptyView: 数据为空");
        mResultTextView.setText("数据为空");
    }

    @Override
    public void showErrorView(String errorResult) {
        Log.e(TAG, "showErrorView: " + errorResult);
        mResultTextView.setText(errorResult);
    }
}
public interface IMusicChannelView {
    /**显示电台列表*/
    void showMusicChannelList(List<ChannellistBean> list);
    /**显示无数据页面*/
    void showEmptyView();
    /**显示请求出错页面*/
    void showErrorView(String errorResult);
}
public class MusicChannelPresenter {
    private IMusicChannelView mMusicChannelView;

    public MusicChannelPresenter(IMusicChannelView view) {
        mMusicChannelView = view;
    }

    public void requestMusicChannels() {
        String url = "https://api.apiopen.top/musicBroadcasting";
        RequestParams params = new RequestParams();
        HttpClient.getInstance().get(new BaseHttpRequest(url, params, new HttpCallback() {
            @Override
            public void onSuccess(String result, Object tag) {
                MusicChannel musicChannel = JsonUtils.jsonToObject(result, MusicChannel.class);
                if (musicChannel != null && musicChannel.getResult() != null
                        && musicChannel.getResult().size() > 0) {
                    MusicChannel.ResultBean resultBean = musicChannel.getResult().get(0);
                    List<ChannellistBean> channellist = resultBean.getChannellist();
                    if (channellist != null && channellist.size() > 0) {
                        mMusicChannelView.showMusicChannelList(channellist);
                        return;
                    }
                }
                mMusicChannelView.showEmptyView();
            }

            @Override
            public void onError(String result, Object tag) {
                if (StringCallback.ERROR_TIME_OUT.equals(result)) {
                    mMusicChannelView.showErrorView("请求超时");
                } else {
                    mMusicChannelView.showErrorView("请求失败");
                }
            }
        }, 100));
    }
}

MusicChannelActivity 中进入页面创建MusicChannelPresenter 并获取电台列表数据进行显示,IMusicChannelView 是View层的接口,MusicChannelPresenter中负责发起网络请求。MusicChannelPresenter获取数据之后,会调用IMusicChannelView 的接口方法设置Activity的数据的显示,这里本来要用一个RecyclerView进行展示,为了demo方便,Activity中先用一个TextView代替进行简要的展示。

直接验证真实的网络请求

首先我们直接验证启动Activity后能否显示正确的结果:

    @Test
    public void testGetMusicChannelsSuccess() throws Exception {
        MusicChannelActivity activity = Robolectric.setupActivity(MusicChannelActivity.class);
        //等待接口请求完毕 再往下执行
        waitAfterAsyncRequest();
        TextView resultTextView = activity.findViewById(R.id.tv_result);
        Assert.assertEquals("请求成功", resultTextView.getText().toString());
    }
    public void waitAfterAsyncRequest() throws Exception {
        //获取主线程的消息队列的调度者,通过它可以知道消息队列的情况并驱动主线程主动轮询消息队列
        Scheduler scheduler = Robolectric.getForegroundThreadScheduler();
        //因为网络请求是在异步线程中进行, 过一段时间请求完毕才会通知主线程
        //所以在这里进行等待,直到消息队列里存在消息
        while (!scheduler.areAnyRunnable()) {
            Thread.sleep(500);
        }
        //轮询消息队列,这样就会在主线程进行通知
        scheduler.runOneTask();
    }

由于网络是异步请求的,所以这个地方有个重要的地方就是需要通过Robolectric.getForegroundThreadScheduler()获取Scheduler来驱动主线程去轮询消息队列,这样使得我们最终可以在测试方法中调用网络请求之后进行断言操作,否则你的断言结果一定是不通过的,因为测试方法是同步的,跑完测试方法的时候,网络请求还没有结束。

运行测试方法,控制台输出:
在这里插入图片描述
可以看到测试方法通过,并且控制台我们能看到网络框架请求回来的log输出。
上面示例代码是进入Activity就在onCreate中进行了网络请求了,实际项目当中可能会是在任意时机进行请求,因此我们也可以在测试方法中创建Presenter类来进行测试:

    @Test
    public void testGetMusicChannelsSuccess() throws Exception {
        MusicChannelActivity activity = Robolectric.setupActivity(MusicChannelActivity.class);
        MusicChannelPresenter presenter = new MusicChannelPresenter(activity);
        presenter.requestMusicChannels();
        //等待接口请求完毕 再往下执行
        waitAfterAsyncRequest();
        TextView resultTextView = activity.findViewById(R.id.tv_result);
        Assert.assertEquals("请求成功", resultTextView.getText().toString());
        // TODO: 2019/4/29 presenter中增加public方法获取响应结果的实体类对象进行验证
    }

这时需要把Activity中生命周期方法onCreare中的调用先注释掉,否则会跑两遍。这时我们甚至可以在MusicChannelPresenter中增加公开的方法返回获取到的接口响应值或者对应的实体类对象来进行一些verify验证。

通过拦截器来模拟请求返回值

上面的demo代码是默认在后端接口完善并且网络可用的情况下直接进行真实的网络请求的,那如果后端接口现在还没有,不可用,或者想要验证异常的结果、为空的请求结果呢?这时可以通过网络框架的拦截器实现,项目中用的网络框架是基于OKHttp的, 所以可以很方便的设置拦截器:

public class TestInterceptor implements Interceptor {
    String emptyResult = "{}";
    String okResult = "{\"code\":200,\"message\":\"成功!\",\"result\":[{\"channellist\":[{\"thumb\":\"http://hiphotos.qianqian.com/ting/pic/item/838ba61ea8d3fd1fb4912e42354e251f95ca5f2a.jpg\",\"name\":\"漫步春天\",\"cate_name\":\"tuijian\",\"cate_sname\":\"推荐频道\",\"ch_name\":\"public_tuijian_spring\",\"value\":1000000,\"channelid\":\"62\"}],\"title\":\"公共频道\",\"cid\":1}]}";
    public static final int TEST_OK_RESULT = 0;
    public static final int TEST_EMPTY_RESULT = 1;
    public static final int TEST_ERROR_CALL_BACK = 2;
    /**拦截器测试模式:正常、空值、异常*/
    public static int TEST_MODE =  TEST_OK_RESULT;

    @Override
    public Response intercept(Interceptor.Chain chain) throws IOException {
        HttpUrl uri = chain.request().url();
        String path = uri.url().getPath();
        String responseString = null;

        //根据接口路径判断哪个接口返回什么样的业务模拟值
        if (path.equals("/musicBroadcasting")) {
            if (TEST_MODE == TEST_OK_RESULT) {
                responseString = okResult;
            } else if (TEST_MODE == TEST_EMPTY_RESULT) {
                responseString = emptyResult;
            }
        }
        Response response = null;
        if (TEST_MODE == TEST_ERROR_CALL_BACK) {
            response = new Response.Builder()
                    .code(500)
                    .message("Server Internal Error")
                    .request(chain.request())
                    .protocol(Protocol.HTTP_1_0)
                    .build();
            return response;
        } else {
            response = new Response.Builder()
                    .code(200)
                    .message(responseString)
                    .request(chain.request())
                    .protocol(Protocol.HTTP_1_0)
                    .body(ResponseBody.create(MediaType.parse("application/json"), responseString.getBytes()))
                    .addHeader("content-type", "application/json;charset=UTF-8")
                    .build();
            return response;
        }
    }
}

在拦截器TestInterceptor 中我定义了几个模式方便测试使用,当然这里你可以在intercept方法当中定义任何的返回结果。在定义好拦截器以后我们要设置给网络请求框架,很遗憾的是OkHttp目前好像不能支持动态的设置拦截器,我们需要在ApplicationonCreate方法中初始化网络框架的时候设置,而我们的拦截器中都是测试代码因此不会在业务代码(即src/main/下面)去添加,这时我们要使用隔离Applicaton的方式把它加到测试的Application当中:

public class RobolectricApp extends Application  {
    @Override
    public void onCreate() {
        super.onCreate();
        HttpClient.getInstance().addInterceptor(new TestInterceptor()).init(this);
    }
}
@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowLog.class}, application = RobolectricApp.class, constants = BuildConfig.class, sdk = Build.VERSION_CODES.M)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "org.json.*", "sun.security.*", "javax.net.*"})
public class MusicChannelActivityTest {
}

好了,当拦截器定义好以后我们就可以随心所欲、无法无天了,因为任何结果都可以自己设置了。

测试返回值为空:

   @Test
    public void testGetMusicChannelsEmpty() throws Exception {
        TestInterceptor.TEST_MODE = TestInterceptor.TEST_EMPTY_RESULT;
        MusicChannelActivity activity = Robolectric.setupActivity(MusicChannelActivity.class);
        new MusicChannelPresenter(activity).requestMusicChannels();
        waitAfterAsyncRequest();
        TextView resultTextView = activity.findViewById(R.id.tv_result);
        Assert.assertEquals("数据为空", resultTextView.getText().toString());
    }

测试请求失败:

    @Test
    public void testGetMusicChannelsFail() throws Exception {
        TestInterceptor.TEST_MODE = TestInterceptor.TEST_ERROR_CALL_BACK;
        MusicChannelActivity activity = Robolectric.setupActivity(MusicChannelActivity.class);
        new MusicChannelPresenter(activity).requestMusicChannels();
        waitAfterAsyncRequest();
        TextView resultTextView = activity.findViewById(R.id.tv_result);
        Assert.assertEquals("请求失败", resultTextView.getText().toString());
    }

测试返回值为指定数量或内容:

    @Test
    public void testGetMusicChannelsSuccess2() throws Exception {
        TestInterceptor.TEST_MODE = TestInterceptor.TEST_OK_RESULT;
        MusicChannelActivity activity = Robolectric.setupActivity(MusicChannelActivity.class);
        MusicChannelPresenter presenter = new MusicChannelPresenter(activity);
        presenter.requestMusicChannels();
        waitAfterAsyncRequest();
        MusicChannel musicChannelData = presenter.getMusicChannelData();
        Assert.assertNotNull(musicChannelData);
        Assert.assertEquals(200, musicChannelData.getCode());
        Assert.assertNotNull(musicChannelData.getResult());
        Assert.assertThat(musicChannelData.getResult().size(), is(1));
        Assert.assertNotNull(musicChannelData.getResult().get(0));
    }

在这个测试当中,我们可以在MusicChannelPresenter内部将接口请求后解析的对象保存为成员变量,暴露出get方法,然后在测试方法中获取这个对象进行断言操作,以验证是否符合预期。

使用Mockito模拟返回值

如果你的网络框架不支持设置拦截器,也没有可用的测试接口去测试真实的网络请求,那么可以选择使用mock框架来模拟一些返回值,如使用Mockito或者PowerMockito进行模拟。

首先需要对Presenter进行一点改造:

public class MusicChannelPresenter implements HttpCallback {
    private IMusicChannelView mMusicChannelView;

    public MusicChannelPresenter(IMusicChannelView view) {
        mMusicChannelView = view;
    }

    public void requestMusicChannels() {
        String url = "https://api.apiopen.top/musicBroadcasting";
        RequestParams params = new RequestParams();
        HttpClient.getInstance().get(new BaseHttpRequest(url, params, this, 100));
    }

    @Override
    public void onSuccess(String result, Object tag) {
        MusicChannel musicChannel = JsonUtils.jsonToObject(result, MusicChannel.class);
        if (musicChannel != null && musicChannel.getResult() != null
                && musicChannel.getResult().size() > 0) {
            MusicChannel.ResultBean resultBean = musicChannel.getResult().get(0);
            List<ChannellistBean> channellist = resultBean.getChannellist();
            if (channellist != null && channellist.size() > 0) {
                mMusicChannelView.showMusicChannelList(channellist);
                return;
            }
        }
        mMusicChannelView.showEmptyView();
    }

    @Override
    public void onError(String result, Object tag) {
        if (StringCallback.ERROR_TIME_OUT.equals(result)) {
            mMusicChannelView.showErrorView("请求超时");
        } else {
            mMusicChannelView.showErrorView("请求失败");
        }
    }
}

这里直接让MusicChannelPresenterl类实现网络请求的回调接口HttpCallback ,或者你可以选择创建一个实现HttpCallback接口的静态内部类, 然后在测试方法中针对回调接口实现类的onSuccessonError方法进行打桩。我们这里直接MusicChannelPresenterl本身实现回调接口,测试的时候会更加方便。当然在实际当中如何改造业务代码以便对测试友好,这需要由你自己去做判断和决定。

测试方法:

	@Test
    public void testGetMusicChannelsByMock() throws Exception {
        MusicChannelActivity activity = Robolectric.setupActivity(MusicChannelActivity.class);
        MusicChannelPresenter presenter = new MusicChannelPresenter(activity);
        final MusicChannelPresenter spy = Mockito.spy(presenter);
        //设置请求方法doNothing()
        Mockito.doNothing().when(spy).requestMusicChannels();
        //设置请求方法后执行指定的Answer
        Mockito.doAnswer(new Answer<Object>() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                String okResult = "{\"code\":200,\"message\":\"成功!\",\"result\":[{\"channellist\":[{\"thumb\":\"http://hiphotos.qianqian.com/ting/pic/item/838ba61ea8d3fd1fb4912e42354e251f95ca5f2a.jpg\",\"name\":\"漫步春天\",\"cate_name\":\"tuijian\",\"cate_sname\":\"推荐频道\",\"ch_name\":\"public_tuijian_spring\",\"value\":1000000,\"channelid\":\"62\"}],\"title\":\"公共频道\",\"cid\":1}]}";
                //测试正常返回结果处理
                spy.onSuccess(okResult, 100);
                return null;
            }
        }).doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                //测试空的返回结果处理
                spy.onSuccess("{}", 100);
                return null;
            }
        }).doAnswer(new Answer() {
            @Override
            public Object answer(InvocationOnMock invocation) throws Throwable {
                //测试请求失败的结果处理
                spy.onError("Server Internal error", 100);
                return null;
            }
        }).when(spy).requestMusicChannels();

        TextView resultTextView = activity.findViewById(R.id.tv_result);
        spy.requestMusicChannels();
        Assert.assertEquals("请求成功", resultTextView.getText().toString());
        spy.requestMusicChannels();
        Assert.assertEquals("数据为空", resultTextView.getText().toString());
        spy.requestMusicChannels();
        Assert.assertEquals("请求失败", resultTextView.getText().toString());
    }

在这个测试方法中,我们首先创建MusicChannelPresenter对象,然后通过spy生成对应的监控对象,我们希望屏蔽掉网络请求部分的代码,因此我们调用doNothing()设置MusicChannelPresenter的requestMusicChannels()方法什么也不做,这样调用监控对象的这个方法的时候它就不会去真实的请求网络,接着连续对spy对象进行了三次打桩,调用了三次doAnswer()方法在answer里面分别对spy对象设置了正常、空值、异常的返回结果,接下来就可以分别调用三次请求方法并对每次的请求结果针对UI的展示结果进行断言验证了。
在这里插入图片描述
使用Shadow大招解决异步网络问题

大招往往在最后祭出,除了上面的几种方式,我们还可以使用Robolectric提供的Shadow功能自定义Shadow影子类来解决异步网络问题。

首先我们看一下前面demo中MusicChannelPresenter调用的网络框架HttpClient的get方法:
在这里插入图片描述
这个其实是基于OkGo进行的封装,而OkGo也是基于OkHttp的,OkGo最终是调用execute方法执行请求的,因此我们需要找到这个execute方法最原始定义的地方:
在这里插入图片描述
最终在OkGo代码的Request.java类中找到了execute方法的定义,因此我们的目标现在就是要Shadow这个Request类,并且重新实现execute方法,将异步请求改为同步请求就可以啦。

我们在src/test目录下新建一个ShadowRequest类进行实现:

/**
 * 对应OkGo的Request类的影子类,异步转同步
 */
@Implements(Request.class)
public class ShadowRequest<String, R extends ShadowRequest> implements Serializable {
    @RealObject
    Request realRequest;

    /** 重写OkGo的异步请求方法,改为同步请求 */
    @Implementation
    public void execute(Callback<String> callback) {
        Log.e("ShadowRequest", "ShadowRequest execute: " );
        realRequest.converter(new StringConvert());
        Call call = realRequest.getRawCall();
        Response response = null;
        try {
            //执行OkHttp同步请求
            response = call.execute();
        } catch (IOException e) {
            e.printStackTrace();
            if (!call.isCanceled()) {
                com.lzy.okgo.model.Response<String> error = com.lzy.okgo.model.Response.error(false, call, null, e);
                callback.onError(error);
                callback.onFinish();
            }
            return;
        }
        int responseCode = response.code();
        //network error
        if (responseCode == 404 || responseCode >= 500) {
            com.lzy.okgo.model.Response<String> error = com.lzy.okgo.model.Response.error(false, call, response, HttpException.NET_ERROR());
            callback.onError(error);
            callback.onFinish();
            return;
        }
        try {
            String body = (String) realRequest.getConverter().convertResponse(response);
            com.lzy.okgo.model.Response<String> success = com.lzy.okgo.model.Response.success(false, body, call, response);
            callback.onSuccess(success);
            callback.onFinish();
        } catch (Throwable throwable) {
            com.lzy.okgo.model.Response<String> error = com.lzy.okgo.model.Response.error(false, call, response, throwable);
            callback.onError(error);
            callback.onFinish();
        }
    }
}

在这里我们除了要将异步请求换为同步请求代码外,最重要的是要将请求返回的response对象按照成功和失败的情况解析,然后分别调用execute方法的参数callback回调的onSuccessonError方法,这样我们在最终调用的时候这里实际上会变成同步请求但是结果还是会通过原来的callback回来。

接下来应用ShadowRequest类,只需要在测试类上添加注解即可:

@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowLog.class, ShadowRequest.class}, application = RobolectricApp.class, constants = BuildConfig.class, sdk = Build.VERSION_CODES.M)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "org.json.*", "sun.security.*", "javax.net.*"})
public class MusicChannelActivityTest {
}

然后在测试方法中我们就可以不需要再调用等待异步线程的那段代码,直接进行断言了,因为此时网络请求本身就是异步的啦:

    @Test
    public void testGetMusicChannelsSuccess() throws Exception {
        MusicChannelActivity activity = Robolectric.setupActivity(MusicChannelActivity.class);
        MusicChannelPresenter presenter = new MusicChannelPresenter(activity);
        presenter.requestMusicChannels();
        //这个等待的方法就不需要了
        //waitAfterAsyncRequest();
        TextView resultTextView = activity.findViewById(R.id.tv_result);
        Assert.assertEquals("请求成功", resultTextView.getText().toString());
    }

这样就可以一劳永逸,不需要在每个测试方法担心异步网络问题了,因为只要在测试类中指定了Shadow类,当实际调用到Request类的execute的时候,都会去调用ShadowRequest类的execute, 而我们Shadow的是一个Request基类,这将会对所有的Request子类起作用,包括GetRequestPostRequest类等。

23. 测试网络同步请求

其实在Android当中一般我们都会使用异步网络请求(因为从Android 4.0开始强制要求不能在主线程中进行网络请求),即便是有同步的请求也是在子线程中的业务代码中,最终会切换回调到主线程进行UI操作。所以关于网络同步请求的主要测试目的,即前面提到的另一个目的,可以用来测试接口本身的相关问题,验证请求参数、请求地址、响应内容格式等是否正确符合预期,也就是单纯针对接口访问进行真实的网络测试。

测试方法很简单,我们直接在测试类中写网络框架的同步请求代码即可:

    @Test
    public void testSyncRequest() throws Exception {
        String url = "https://api.apiopen.top/musicBroadcasting";
        RequestParams params = new RequestParams();
        params.set("name", "aaa");
        params.set("id", 123);
        String result = HttpClient.getInstance().getSync(new BaseHttpRequest(url, params, null, 0));
        Log.e(TAG, "testSyncRequest: " + result );
        Assert.assertNotNull(result);
        MusicChannel musicChannelData = JsonUtils.jsonToObject(result, MusicChannel.class);
        Assert.assertNotNull(musicChannelData);
        Assert.assertEquals(200, musicChannelData.getCode());
        Assert.assertNotNull(musicChannelData.getResult());
        Assert.assertThat(musicChannelData.getResult().size(), Matchers.greaterThan(0));
        Assert.assertNotNull(musicChannelData.getResult().get(0));
    }

控制台输出:
在这里插入图片描述
因为这时是测试真实的网络请求,所以这时需要把RobolectricApp中网络库初始化时设置的拦截器去掉,否则会返回我们设置的模拟值。

如果业务接口需要登录后才能请求,那也很简单,直接在@Before注解标注的setUp方法里调用登录接口就可以,因为跑每一个@Test方法之前会先执行@Before,因此能保证我们测试的每一个接口前都具备登录状态。

如果测试方法运行失败了,说明要么这个接口的请求参数、请求地址或者返回的json不符合预期,这时就需要跟后端接口负责人进行沟通了。

24. 其他异步线程问题

除了异步的网络请求问题以外,在Android开发当中我们还会经常遇到在子线程中去执行耗时任务,如数据库操作、IO文件操作等。

在Android业务代码中一般我们主要会用到两个类来处理异步任务:

  • Thread类
  • AsyncTask类

关于Thread的第一种不规范写法:

	public void doHandle() {
        mPerson.setName("测试");
    }

    public void startTask() {
        new Thread() {
            @Override
            public void run() {
                doHandle();
            }
        }.start();
    }

这时需要将Thread的run方法中的代码提取出来,然后只针对这个方法做测试。Thread的使用最好使用线程池来实现:

    public static final ThreadPoolExecutor SINGLE_THREAD_POOL;
    static {
        ThreadFactory threadFactory = Executors.defaultThreadFactory();
        //单线程池
        SINGLE_THREAD_POOL = new ThreadPoolExecutor(1, 1, 0L, MILLISECONDS,
                new LinkedBlockingQueue<Runnable>(1024), threadFactory);
    }

    /**
     * 在单线程池中执行一个任务
     * @param runnable
     */
    public static void executeInSingleThread(Runnable runnable) {
        SINGLE_THREAD_POOL.execute(runnable);
        SINGLE_THREAD_POOL.shutdown();
    }

   public void startTask() {
        final MyHandler myHandler = new MyHandler(this);
        executeInSingleThread(new Runnable() {
            @Override
            public void run() {
                mPerson.setName("测试");
                myHandler.sendEmptyMessage(100);
            }
        });
   }

这样可以通过Robolectric的Scheduler去等待:

@Test
    public void testSubTask() throws Exception {
        MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);
        Button btn = mainActivity.findViewById(R.id.btn_login);
        mainActivity.startTask();
        Scheduler scheduler = Robolectric.getForegroundThreadScheduler();
        if (!scheduler.areAnyRunnable()) {
            Thread.sleep(500);
        }
        scheduler.runOneTask();
        Assert.assertEquals("测试", mainActivity.mPerson.getName());
        Assert.assertEquals("测试", btn.getText());
    }

关于AsyncTask的使用,Robolectric默认就是在主线程执行的,所以可以不用担心异步问题,直接进行断言操作:

    public void startTask() {
        Log.e(TAG, "UIThreadId: " + Thread.currentThread().getId() );
        Log.e(TAG, "UIThreadName: " + Thread.currentThread().getName() );
        new MyAsyncTask().execute();
    }
    
    class MyAsyncTask extends AsyncTask<Void, Integer, String> {
        @Override
        protected String doInBackground(Void... voids) {
            Log.e(TAG, "AsyncTaskThreadId: " + Thread.currentThread().getId() );
            Log.e(TAG, "AsyncTaskThreadName: " + Thread.currentThread().getName() );
            //省略文件耗时操作
            return null;
        }

        @Override
        protected void onPostExecute(String s) {
            super.onPostExecute(s);
            Log.e(TAG, "onPostExecute: " );
            mLoginBtn.setText("测试");
        }
    }
 	@Test
    public void testSubTask() throws Exception {
        MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);
        Button btn = mainActivity.findViewById(R.id.btn_login);
        //如果是测试AsyncTask,doInBackground将会是执行在UI主线程的, 无需等待
        mainActivity.startTask();
        Assert.assertEquals("测试", btn.getText());
    }

运行测试方法,我们可以看到测试通过,并且doInBackground方法的线程id是跟UI线程一致的:
在这里插入图片描述

25. 创建测试基类

可以将常用的一些Robolectric配置放在一个基类当中,需要的时候直接继承这个基类,不用每次去添加配置了:

@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowLog.class}, constants = BuildConfig.class, sdk = 23)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "org.json.*", "sun.security.*", "javax.net.*"})
public abstract class BaseRobolectricTestCase {

    @Rule
    public PowerMockRule rule = new PowerMockRule();

    @Before
    public void setUp() {
        ShadowLog.stream = System.out;
        MockitoAnnotations.initMocks(this);
    }

    public Application getApplication() {
        return RuntimeEnvironment.application;
    }

    public Context getContext() {
        return RuntimeEnvironment.application;
    }

    public String getString(int id) {
        return RuntimeEnvironment.application.getString(id);
    }

    public String getPkgName() {
        return RuntimeEnvironment.application.getPackageName();
    }

    public <T extends Activity> T startActivity(Class<T> activityClass) {
        return Robolectric.setupActivity(activityClass);
    }

    public void startFragment(Fragment fragment) {
        SupportFragmentTestUtil.startFragment(fragment);
    }
}

其中@RunWith@Config注解都是可以继承的,所以子类可以不用再次添加这两个注解。

26. 支持Multidex

需要导入Robolectric对Multidex的支持库,否则会报java.lang.RuntimeException: MultiDex installation failed

testImplementation 'org.robolectric:shadows-multidex:3.8'
27. Flavor的支持

gradle添加falvor:

    productFlavors {
        flavor1 {
            applicationId "com.fly.unit.test.a"
            dimension "default"
        }
        flavor2 {
            applicationId "com.fly.unit.test.b"
            dimension "default"
        }
    }

在工程目录下跟src同级建立falvor1falvor2,同时针对falvor1falvor2的测试代码目录为testFlavor1testFlavor2
在这里插入图片描述
将Build Variant切换到对应的flavor就可以在对应的falvor测试文件夹下写代码了:
在这里插入图片描述
如在testFlavor1下对falvor1进行测试:

@RunWith(RobolectricTestRunner.class)
@Config(shadows = {ShadowLog.class}, constants = BuildConfig.class, sdk = 23)
@PowerMockIgnore({"org.mockito.*", "org.robolectric.*", "android.*", "org.json.*", "sun.security.*", "javax.net.*"})
@PrepareForTest({MainActivity.class})
public class MainActivityFlavorTest {
    @Test
    public void testFlavor() {
        MainActivity mainActivity = Robolectric.setupActivity(MainActivity.class);
        Button btn = mainActivity.findViewById(R.id.btn_login);
        Assert.assertEquals("登录", btn.getText());
        Assert.assertEquals("Flavor1", mainActivity.getString(R.string.app_name));
        Assert.assertEquals("com.fly.unit.test.a", BuildConfig.APPLICATION_ID);
    }
}
28. Robolectric给出的实践建议

Robolectric官方给了几点建议,可以看一下:

  • 不要mockspy那些会由Android系统类调用或操作的Android系统Class(如 ContextSharedPreferences等等),打桩行为会使得Robolectric 或 Android 平台的代码升级变得十分脆弱。尽量将这种操作应用在较小的范围内,如事件监听器。
  • 不要在Robolectric单元测试中去模拟layout的inflate操作,你应该直接测试你的Activity与布局的交互操作,以验证是否设置了正确的点击事件回调,而不是去mock LayoutInflater 或者模拟View类。
  • 在测试Android系统组件如ActivitiesServices时,尽量使用public的方法如Robolectric.buildActivity(),避免使用@VisibleForTesting标注的方法,否则后期升级依赖版本时将会给测试代码的重构工作带来困难。
  • 一定要限制在每个测试用例期间运行的线程数。恶意线程经常会导致测试环境污染,因为它们在测试之间不会自动清除。在使用第三方库(例如网络)或后台处理组件时,通常会无意中生成线程。测试期间额外线程的主要来源之一就是维护线程池的ExecutorServices。如果可能,尽量mock产生线程的依赖组件,或者使用DirectExecutor。如果需要在测试期间运行多个线程,请确保显式地停止所有线程和ExecutorServices,以避免测试污染。

在实际中推荐使用JUnit4+Mockito+PowerMockito+Robolectric的组合,通过PowerMockito弥补Mockito测试框架不能mock静态方法、final方法和private方法的不足,而通过Robolectric可以弥补不能在JVM中允许Android相关代码的缺陷,这样在写单元测试时可以解决任何问题。

;