Android 单元测试
本文是学习 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 | (JUnit4.class) |
测试方法中的 assertTrue()
是JUnit 原生提供的断言方法,于文件中直接右键Run 'TempTest'
即可运行测试。
更多 JUnit 的介绍可见 JUnit单元测试框架的使用
更多 JUnit 的使用示例可见 Unit Testing with JUnit - Tutorial
2. Mockito
Mockito 是一个 JAVA 层的 Mocking 框架,通过 Mockito 可以生成一个模拟的桩对象,可供单元测试时替换真实对象以达到两个目的
- 验证这个对象的方法的调用情况,如调用次数和参数内容等
- 指定这个对象对特定调用方法的行为,如返回的值和额外执行其它操作等
Mockito 基于 JUnit 4 ,所以类的基本构造与 JUnit 4一样。
一个简单的 Mockito 示例
1 | (JUnit4.class) |
更多 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
8public class LoginActivityPresenter {
private LoginActivity loginActivity;
public LoginActivityPresenter(LoginActivity loginActivity) {
this.loginActivity = loginActivity;
}
}使用
@Inject
注解构造器除了能从依赖图里获取参数依赖还能成为依赖图的一部分,即其亦可于在需要的时候被注入:1
2
3
4public class LoginActivity extends BaseActivity {
LoginActivityPresenter presenter;
}使用
@Inject
注解构造器的一个限制是在一个类中只能注解一个构造函数。注解变量
1
2
3
4public class LoginActivity extends BaseActivity {
LoginActivityPresenter presenter;
}@Inject
注解变量这种依赖注入方式需要手动调用注入,要在类中的某个地方执行:1
2
3
4
5
6
7
8
9
10public class LoginActivity extends BaseActivity {
LoginActivityPresenter presenter;
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
public class ApiModule {
// 让 Dagger 2 知道这个方法提供 OkHttpClient 依赖,方法名一般命名为 provideXXX()
OkHttpClient provideOkHttpClient() {
return new OkHttpClient();
}
}@Component
: 用于构建提供依赖的接口,可以在这里定义需要获得的依赖来自于哪个被@Module
修饰的类(或者其它被@Components
修饰的类)。@Component
可以理解为提供依赖的@Module
与待注入依赖的@Inject
间的桥梁。1
2
3
4(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(RetentionPolicy.RUNTIME)
public ApiRetrofit {}
------- // Class divider
(RetentionPolicy.RUNTIME)
public UserRetrofit {}
------- // Class divider
public Retrofit provideUserRetrofit(OkHttpClient okHttpClient) {
return new Retrofit("User");
}
public Retrofit provideApiRetrofit(OkHttpClient okHttpClient) {
return new Retrofit("Api");
}
public ApiService provideApiService(@ApiRetrofit Retrofit retrofit) {
return retrofit.create(ApiService.class);
}
public UserService provideUserService(@UserRetrofit Retrofit retrofit) {
return retrofit.create(UserService.class);
}
简单介绍了常用 API ,接下来演示简单的用法
首先是提供依赖类 @Module
1 |
|
接着是桥梁 @Component
1 | (modules = {ApiModule.class}) |
最后是需要被注入依赖的 LoginActivity
1 | public class LoginActivity extends BaseActivity { |
这样 presenter 对象就由 Dagger 2 实现了注入,可供使用。
更多相关的 Dagger 2 使用技巧可见 用 Dagger 2 实现依赖注入
虽然 Dagger 2 自动生成代码完成依赖注入很棒,但因为 Activity
和Fragment
等控件是由安卓系统生成,导致许多成员注入都写在它们的生命周期回调里,许多类最后会变成这样:
1 | public class FrombulationActivity extends Activity { |
不断地重复这样的代码会导致这样的问题:
- 不利于重构,拷来拷去,可能到最后会忘了这段代码的实际用途
- 往更深一层去看,这些代码显式地去指明了它的注入者,即便指明的是接口但也违背了依赖注入时类本身不知道注入方式的基本原则
这时候应用 dagger.android 库就可以使代码更优雅。
下面来介绍一下 dagger.android 这个库的简单使用方式
在 Application 级的 Component 里添加 AndroidInjectionModule
1
2
3
4
5
(modules = {AndroidInjectionModule.class, AppModule.class})
public interface AppComponent {
// For brevity.
}为需要注入依赖的 Activity 书写继承了 AndroidInjector\
的接口,并带有继承了AndroidInjector\ 一个抽象的 Builder类 1
2
3
4
5
6
7// Subcomponent 可以理解为子 Component,在拥有自己的 Mod概念概念ule 的同时亦可以使用父 Component 的
// Module,不同 Subcomponent 之间的 Module 不能直接互用 (类似于命名空间的概念)
(modules = ...)
public interface YourActivitySubcomponent extends AndroidInjector<YourActivity> {
.Builder
public abstract class Builder extends AndroidInjector.Builder<YourActivity> {}
}简化简化定义了上述 SubComponent 以后再写一个 Module 来绑定它
1
2
3
4
5
6
7
8(subcomponents = YourActivitySubcomponent.class)
abstract class YourActivityModule {
(YourActivity.class)
abstract AndroidInjector.Factory<? extends Activity>
bindYourActivityInjectorFactory(YourActivitySubcomponent.Builder builder);
}最后在 Application 级的 Component 里将 Module 添加进去
1
2
3
4(modules = {AndroidInjectionModule.class,, YourActivityModule.class})
public interface AppComponent {
// For brevity.
}若 SubComponent 及其 Builder 没有其它方法或者是超类的话,可以用 @ContributesAndroidInjector 注解来替换掉上面的步骤2和步骤3 ,具体如下
1
2
3
4
5
public abstract class ActivityBuilder {
/* modules to install into the subcomponent */ }) (modules = {
abstract YourActivity contributeYourActivityInjector();
}将其添加至 Application 级的 Component 即可
Application 实现 HasActivityInjector 接口以及
@Inject
注入一个 DispatchingAndroidInjector\供 activityInjector()
作为返回值1
2
3
4
5
6
7
8
9
10
11
12
13
14
15public class YourApplication extends Application implements HasActivityInjector {
DispatchingAndroidInjector<Activity> dispatchingActivityInjector;
public void onCreate() {
super.onCreate();
DaggerAppComponent.create()
.inject(this);
}
public AndroidInjector<Activity> activityInjector() {
return dispatchingActivityInjector;
}
}最后在 Activity 调用
super.onCreate()
前调用AndroidInjector.inject()
即可完成依赖注入1
2
3
4
5
6public 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 | // 与JUnit 4的内容略有不同 (AndroidJUnit4.class) |
更多 Espresso 的介绍可见 UI测试(Espresso)
更多 Espresso 的使用示例可见 Espresso Tutorial
参考资料
上文所有可跳转链接以及下列文章