本文是学习 Android 单元测试时的一些笔记。

Android 单元测试

采用 MVVM 作为 APP 架构的一个好处是在一定程度上分层剥离了 Android 层的代码, 使得基于 JAVA 层的单元测试 (Unit Test) 成为可能。

下面来介绍一些构建单元测试用到的 Library。

1. JUnit 4

单元测试使用的框架是 JUnit 4, JUnit 4 提供了 Java 层的测试基础。

一个简单的 JUnit 4 示例:

在 test 目录创建一个 TempTest.java 文件

@RunWith注解用于指定使用 JUnit 作为运行框架,@Before标示在测试开始前的操作,@Test标示测试本体。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RunWith(JUnit4.class)
public class TempTest {
private String mPackage;

@Before
public void init() {
mPackage = "jp.co.nintendo.supermario";
}

@Test
public void assertPackageName() {
assertTrue(mPackage.contains("nintendo"));
}
}

测试方法中的 assertTrue()是JUnit 原生提供的断言方法,于文件中直接右键Run 'TempTest'即可运行测试。

更多 JUnit 的介绍可见 JUnit单元测试框架的使用

更多 JUnit 的使用示例可见 Unit Testing with JUnit - Tutorial

2. Mockito

Mockito 是一个 JAVA 层的 Mocking 框架,通过 Mockito 可以生成一个模拟的桩对象,可供单元测试时替换真实对象以达到两个目的

  1. 验证这个对象的方法的调用情况,如调用次数和参数内容等
  2. 指定这个对象对特定调用方法的行为,如返回的值和额外执行其它操作等

Mockito 基于 JUnit 4 ,所以类的基本构造与 JUnit 4一样。

一个简单的 Mockito 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@RunWith(JUnit4.class)
public class UploadRepositoryTest {
private ApiService mService;
private UploadRepository mRepository;

@Before
public void init() {
// 通过 mock(clazz) 方法来模拟一个桩对象作为参数提供给需要被构造的对象
mService = mock(ApiService.class);
mRepository = new UploadRepository(mService);
}

@Test
// 测试上传图片成功后 LiveData 是否发出响应
public void testUploadImage() {
Observable<String> observable = Observable.just("Upload Success!");
MutableLiveData<String> liveData = mock(MutableLiveData.class);

// mService 是 mock (模拟)出来的空对象,这里设置了当调用 ApiService 的 postUploadImages
// 方法时直接返回上面生成的 observable PS: anyObject() 意为接受任何参数
when(mService.postUploadImages(anyObject())).thenReturn(observable);

mRepository.setLiveImageResponse(liveData);
mRepository.uploadImage(new File("/nintendo/supermario.png"));

// 校验 liveData 对象是否调用了1次 postValue() 方法
verify(liveData, times(1)).postValue(anyObject());
}
}

更多 Mockito 的介绍可见 Mock以及Mockito的使用

更多 Mockito 的使用示例可见 Mockito examples

3. Dagger 2

在实际应用中,使用 Mockito 来模拟对象很快就遇到了问题,当模拟对象不是以参数形式传入被测试对象而是在被测试对象内部自行生成的话,mock 出来的模拟对象就无能为力了,为了继续进行测试可能就需要在测试对象里设置 setXXX()方法来把模拟对象传递进去。

单独为单元测试而在原逻辑代码里新增setXXX()的方法并不优雅,其实除了创建setXXX()方法这种方式以外,还可以使用 Dagger 2 以注解的形式进行 依赖注入(Dependency Injection) 来构造对象。

使用 Dagger 2 除了代码优雅一点以外,还有以下优点

  • 减轻了代码维护的压力,修改依赖构造方法的时候只需要修改提供者,请求依赖方不需改动,而不使用 Dagger 2 的话则需要修改每个调用了这个构造方法的类
  • 可以松开一点数据和逻辑间的耦合

下面来介绍 Dagger 2 的一些常用的注解 API

  • @Inject: 标记需要被 Dagger2 注入的依赖,常用的有两种注解方式

    • 注解构造器

      1
      2
      3
      4
      5
      6
      7
      8
      public class LoginActivityPresenter {
      private LoginActivity loginActivity;

      @Inject
      public LoginActivityPresenter(LoginActivity loginActivity) {
      this.loginActivity = loginActivity;
      }
      }

      使用@Inject注解构造器除了能从依赖图里获取参数依赖还能成为依赖图的一部分,即其亦可于在需要的时候被注入:

      1
      2
      3
      4
      public class LoginActivity extends BaseActivity {
      @Inject
      LoginActivityPresenter presenter;
      }

      使用@Inject注解构造器的一个限制是在一个类中只能注解一个构造函数。

    • 注解变量

      1
      2
      3
      4
      public class LoginActivity extends BaseActivity {
      @Inject
      LoginActivityPresenter presenter;
      }

      @Inject注解变量这种依赖注入方式需要手动调用注入,要在类中的某个地方执行:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      public class LoginActivity extends BaseActivity {
      @Inject
      LoginActivityPresenter presenter;

      @Override
      protected void onCreate(Bundle bundle) {
      super.onCreate(bundle);
      getAppComponent().inject(this); // 请求依赖注入
      }
      }

      inject()方法调用前,变量的值均为空。

      注意,@Inject注解的变量的修饰符不能为 private,原因是 Dagger 2 自动生成的代码需要显式访问该变量(确切地说是为该变量赋值),与 Butterknife 同理。

  • @Module:用于标记提供依赖的类,Dagger 2 在此寻找依赖

  • @Provides: 在含有@Module注解的类中使用,用于标记提供依赖的方法:

    1
    2
    3
    4
    5
    6
    7
    8
    @Module
    public class ApiModule {

    @Provides // 让 Dagger 2 知道这个方法提供 OkHttpClient 依赖,方法名一般命名为 provideXXX()
    OkHttpClient provideOkHttpClient() {
    return new OkHttpClient();
    }
    }
  • @Component: 用于构建提供依赖的接口,可以在这里定义需要获得的依赖来自于哪个被@Module修饰的类(或者其它被@Components修饰的类)。@Component可以理解为提供依赖的@Module与待注入依赖的@Inject间的桥梁。

    1
    2
    3
    4
    @Component(modules = {ApiModule.class})
    public interface AppComponent {
    void inject(LoginActivity activity);
    }
  • @Scope:用于自定义依赖单例的生命周期,更多细节可参考Scope注解的使用及源码分析

  • @Singleton: 用于标记单例,若已存在实例不再生成直接返回

  • @Qualifier: 用于区分相同类型的依赖:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    @Retention(RetentionPolicy.RUNTIME)
    @Qualifier
    public @interface ApiRetrofit {}
    ------- // Class divider
    @Retention(RetentionPolicy.RUNTIME)
    @Qualifier
    public @interface UserRetrofit {}
    ------- // Class divider
    @Provides
    @UserRetrofit
    public Retrofit provideUserRetrofit(OkHttpClient okHttpClient) {
    return new Retrofit("User");
    }

    @Provides
    @ApiRetrofit
    public Retrofit provideApiRetrofit(OkHttpClient okHttpClient) {
    return new Retrofit("Api");
    }

    @Provides
    public ApiService provideApiService(@ApiRetrofit Retrofit retrofit) {
    return retrofit.create(ApiService.class);
    }

    @Provides
    public UserService provideUserService(@UserRetrofit Retrofit retrofit) {
    return retrofit.create(UserService.class);
    }

简单介绍了常用 API ,接下来演示简单的用法

首先是提供依赖类 @Module

1
2
3
4
5
6
7
8
9
10
11
12
13
@Module
public class ApiModule {

@Provides
OkHttpClient provideOkHttpClient() {
return new OkHttpClient();
}

@Provides
LoginActivityPresenter provideLoginPresenter(OkhttpClient client) {
return new LoginActivityPresenter(client)
}
}

接着是桥梁 @Component

1
2
3
4
@Component(modules = {ApiModule.class})
public interface ApiComponent {
void inject(LoginActivity activity);
}

最后是需要被注入依赖的 LoginActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class LoginActivity extends BaseActivity {
@Inject
LoginActivityPresenter presenter;

@Override
protected void onCreate(Bundle bundle) {
super.onCreate(bundle);
// DaggerApiComponent 是由 Dagger2 实现 ApiComponent 接口自动生成的类
DaggerApiComponent.builder()
.build()
.inject(this);

presenter.login();
}
}

这样 presenter 对象就由 Dagger 2 实现了注入,可供使用。

更多相关的 Dagger 2 使用技巧可见 用 Dagger 2 实现依赖注入

虽然 Dagger 2 自动生成代码完成依赖注入很棒,但因为 ActivityFragment等控件是由安卓系统生成,导致许多成员注入都写在它们的生命周期回调里,许多类最后会变成这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class FrombulationActivity extends Activity {
@Inject Frombulator frombulator;

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// DO THIS FIRST. Otherwise frombulator might be null!
((SomeApplicationBaseType) getContext().getApplicationContext())
.getApplicationComponent()
.newActivityComponentBuilder()
.activity(this)
.build()
.inject(this);
// ... now you can write the exciting code
}
}

不断地重复这样的代码会导致这样的问题:

  1. 不利于重构,拷来拷去,可能到最后会忘了这段代码的实际用途
  2. 往更深一层去看,这些代码显式地去指明了它的注入者,即便指明的是接口但也违背了依赖注入时类本身不知道注入方式的基本原则

这时候应用 dagger.android 库就可以使代码更优雅。

下面来介绍一下 dagger.android 这个库的简单使用方式

  1. 在 Application 级的 Component 里添加 AndroidInjectionModule

    1
    2
    3
    4
    5
    @Singleton
    @Component(modules = {AndroidInjectionModule.class, AppModule.class})
    public interface AppComponent {
    // For brevity.
    }
  2. 为需要注入依赖的 Activity 书写继承了 AndroidInjector\ 的接口,并带有继承了AndroidInjector\ 一个抽象的 Builder类

    1
    2
    3
    4
    5
    6
    7
    // Subcomponent 可以理解为子 Component,在拥有自己的 Mod概念概念ule 的同时亦可以使用父 Component 的
    // Module,不同 Subcomponent 之间的 Module 不能直接互用 (类似于命名空间的概念)
    @Subcomponent(modules = ...)
    public interface YourActivitySubcomponent extends AndroidInjector<YourActivity> {
    @Subcomponent.Builder
    public abstract class Builder extends AndroidInjector.Builder<YourActivity> {}
    }
  3. 简化简化定义了上述 SubComponent 以后再写一个 Module 来绑定它

    1
    2
    3
    4
    5
    6
    7
    8
    @Module(subcomponents = YourActivitySubcomponent.class)
    abstract class YourActivityModule {
    @Binds
    @IntoMap
    @ActivityKey(YourActivity.class)
    abstract AndroidInjector.Factory<? extends Activity>
    bindYourActivityInjectorFactory(YourActivitySubcomponent.Builder builder);
    }

    最后在 Application 级的 Component 里将 Module 添加进去

    1
    2
    3
    4
    @Component(modules = {AndroidInjectionModule.class,, YourActivityModule.class})
    public interface AppComponent {
    // For brevity.
    }

    若 SubComponent 及其 Builder 没有其它方法或者是超类的话,可以用 @ContributesAndroidInjector 注解来替换掉上面的步骤2和步骤3 ,具体如下

    1
    2
    3
    4
    5
    @Module
    public abstract class ActivityBuilder {
    @ContributesAndroidInjector(modules = { /* modules to install into the subcomponent */ })
    abstract YourActivity contributeYourActivityInjector();
    }

    将其添加至 Application 级的 Component 即可

  4. Application 实现 HasActivityInjector 接口以及 @Inject注入一个 DispatchingAndroidInjector\activityInjector() 作为返回值

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public class YourApplication extends Application implements HasActivityInjector {
    @Inject DispatchingAndroidInjector<Activity> dispatchingActivityInjector;

    @Override
    public void onCreate() {
    super.onCreate();
    DaggerAppComponent.create()
    .inject(this);
    }

    @Override
    public AndroidInjector<Activity> activityInjector() {
    return dispatchingActivityInjector;
    }
    }
  5. 最后在 Activity 调用 super.onCreate()前调用AndroidInjector.inject()即可完成依赖注入

    1
    2
    3
    4
    5
    6
    public class YourActivity extends Activity {
    public void onCreate(Bundle savedInstanceState) {
    AndroidInjection.inject(this);
    super.onCreate(savedInstanceState);
    }
    }

上述是注入依赖到 Activity 的流程,注入依赖到 Fragment 中的方式也类似,具体差异可参考官方文档 Injecting Fragment objects

通过查看 Dagger 自动生成的 DaggerAppComponent.java文件不难发现, dagger.android 库通过分析提供的注解把提供依赖的 Module 添加到 Map 里,当在需要注入的控件调用 AndroidInjection.inject(this)其逻辑就是从该 Map 或其子 Map 里去取控件相对应的 Module 来实现依赖注入。

参照 Google Archtitecture Sample 通过使用registerActivityLifecycleCallbacks()以及registerFragmentLifecycleCallbacks()在控件相应的生命周期回调里调用 AndroidInjection.inject()方法的话,代码能达到进一步的简化。

4. Espresso

Google 官方将 APP 测试分为三种,小型、中型和大型测试,其分配的比例分为70% 、20%和10%。

上面介绍的单元测试就是占比最大的 70% 测试,而 Espresso 则是构成 20% 和 10% 所使用的库。

与Mockito 不一样 Espresso 是一个 UI 测试框架,运行时需要用到 Android 的代码,所以需要编译运行,时间会长很多。

下面是一个简单的 Espresso 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
@RunWith(AndroidJUnit4.class) // 与JUnit 4的内容略有不同
public class UploadDetailFragmentTest {
@Rule // 使用一个空的Activity来承载Fragment
public ActivityTestRule<SingleFragmentActivity> mActivityRule =
new ActivityTestRule<>(SingleFragmentActivity.class, true, true);

private UploadViewModel mViewModel;
private ApiService mApiService;
private MutableLiveData mMutableLiveData = new MutableLiveData();

@Before
public void init() {
UploadDetailFragment fragment = new UploadDetailFragment();

mViewModel = mock(UploadViewModel.class);
mApiService = new Retrofit.Builder()
.baseUrl(Config.API_HOST)
.client(new OkHttpClient())
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.build().create(ApiService.class);

when(mViewModel.getLiveData()).thenReturn(mMutableLiveData);

fragment.setMApiService(mApiService);
fragment.setMFactory(new ViewModelProviderFactory<>(mViewModel));

mActivityRule.getActivity().setFragment(fragment);
}

// 测试图片上传成功与失败的文字显示状态
@Test
public void uploadCover() throws Throwable {
// 检测view是否处于显示状态
onView(withId(R.id.tv_upload_detail_cover_select))
.perform(ViewActions.scrollTo())
.check(matches(isDisplayed()));

UploadObservableDataWrapper successWrapper = new UploadObservableDataWrapper();
successWrapper.isSuccess = true;
successWrapper.dataType = DataType.APP_COVER_URL;
successWrapper.url = "https://pic.google.com/";
mMutableLiveData.postValue(successWrapper);
onView(withId(R.id.tv_upload_detail_cover_select))
.check(matches(not(isDisplayed())));

UploadObservableDataWrapper failedWrapper = new UploadObservableDataWrapper();
failedWrapper.isSuccess = false;
failedWrapper.dataType = DataType.APP_COVER_URL;
failedWrapper.url = "https://pic.google.com/";
mMutableLiveData.postValue(failedWrapper);
onView(withId(R.id.tv_upload_detail_cover_select))
.check(matches(isDisplayed()));
}
}

更多 Espresso 的介绍可见 UI测试(Espresso)

更多 Espresso 的使用示例可见 Espresso Tutorial

参考资料

上文所有可跳转链接以及下列文章