在本文中,我想向你展示一个使用 RxAndroid、Retrofit、Mockito 和 Model View Presenter (MVP) 架构测试 API 请求层的教程。我们将构建一个使用免费的 Star Wars API 显示电影角色数据的 Android 应用程序。
本教程需要具备 Android 开发、单元测试和响应式编程的基础知识。如果你想查看完整的代码,可以访问这个 github 仓库。
项目设置
在 Android Studio 中(我目前使用的是 2.2.3 版本),开始一个空的活动的基本项目。本教程中,我将选择 Android 4.0.3 (15) 的最小 API。
下面是 build.gradle 文件中所需的所有外部依赖项及其简要描述:
compile 'io.reactivex:rxandroid:1.2.1'
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'testCompile 'junit:junit:4.12'
testCompile 'org.mockito:mockito-core:2.6.8'
- RxAndroid:将 Reactive Extensions 带入 Android 的库
- Retrofit:我们将用它来执行 API 请求的 HTTP Rest 客户端。我选择 Retrofit 是因为它的 RxJava 适配器 可以将 HTTP 响应更容易地转换为 Observable。
- Gson:我们将用它来将 HTTP 响应转换为 Java 模型的 JSON 反序列化器。
- Junit:Java 单元测试框架。我们将使用其中的一些注释和断言方法。
- Mockito:除了其模拟功能外,此框架还将用于验证类的交互(即:方法调用)。
创建模型类
下一步是创建代表从 characters 请求 返回的响应的模型。
我的建议是使用不可变模型,因为它具有优势。现在,我将所有属性设置为 public 和 final,而不是为每个属性创建 getter 方法,因为我打算写本文的第二部分,其中我将解释如何使用 reflection 验证模型。因此,所有值都在对象 实例化 时由其 构造函数 设置。
CharactersModel.java:
public class CharacterModel {
public final String name;
public final String height;
public final String mass; // "SerializedName" is a Gson annotation to remap the original JSON field into another custom name@SerializedName("hair_color")
public final String hairColor;
@SerializedName("skin_color")
public final String skinColor;
@SerializedName("eye_color")
public final String eyeColor;
@SerializedName("birth_year")
public final String birthYear;
public final String gender;
public final String homeworld;
public final List<String> films;
public final List<String> species;
public final List<String> vehicles;
public final List<String> starships;
public final String created;
public final String edited;
public final String url;}
CharactersResponseModel.java:
public class CharactersResponseModel {
public final int count;
public final String next;
public final String previous;
public final List<CharacterModel> results;
}
API 请求
现在,我们已经配置好项目并创建了我们的模型,我们将实现 API 请求功能。
首先,在你的清单文件中添加访问互联网的权限:
<uses-permission android:name="android.permission.INTERNET"/>
如上所述,我们将执行请求以获取 Star Wars 角色列表,因此需要按照 Retrofit 文档中描述的方式添加此端点描述,以 Java 接口的形式。正如我们接下来要看到的那样,接口更容易测试,因为我们可以使用 Mockito 来模拟它。
CharactersDataSource.java
public interface CharactersDataSource {
@GET("people/")
Observable<CharactersResponseModel> getCharacters();
}
下面是上面描述的接口的实现,基本上它使用 Retrofit 和其 RxJava 适配器 进行 HTTP 请求,并将响应转换为 Observable。
CharactersRemoteDataSource.java
public class CharactersRemoteDataSource implements CharactersDataSource {
private CharactersDataSource api;
private final String URL = "http://swapi.co/api/";
public CharactersRemoteDataSource() {
Retrofit retrofit = new Retrofit.Builder()
.baseUrl(URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build();
this.api = retrofit.create(CharactersDataSource.class);
}
@Override
public Observable<CharactersResponseModel> getCharacters() {
return this.api.getCharacters();
}
}
Presenter 和 View 层之间的协议
接下来,我们将创建一个定义 presenter 和 view 之间通信的 interface。interface 是必要的,以保持每个类不相关并使它们更容易在测试中模拟。
视图接口 将具有以下方法:
- **onFetchDataStarted:**通知视图请求即将开始。对于提供用户反馈,如显示 进度条 等非常有用。
- **onFetchDataCompleted:**通知视图不再返回更多数据。
- **onFetchDataSuccess:**将请求的数据作为其参数返回,以我们上面定义的模型类型。
- **onFetchDataError:**请求期间发生错误。例如,可能是连接失败。
主持人接口 将具有以下方法:
- **loadData:**将告诉 presenter 开始获取数据。
- **subscribe:**通知 presenter 其 view 已变为活动状态。这可用于触发 API 请求。
- **unsubscribe:**通知 presenter 其 view 已变为非活动状态。这可用于取消尚未返回的任何先前请求。
- **onDestroy:**通知 presenter 其 view 实例即将被销毁。在本教程中,view 将实例化 presenter,并通过 private 属性 互相引用,因此当 view 正在经历 onDestroy 生命周期时,我们应该在 presenter 上清除此引用,否则这将导致内存泄漏。
MainContract.java
public interface MainContract {
interface View {
void onFetchDataStarted();
void onFetchDataCompleted();
void onFetchDataSuccess(CharactersResponseModel charactersResponseModel);
void onFetchDataError(Throwable e);
}
interface Presenter {
void loadData();
void subscribe();
void unsubscribe();
void onDestroy();
}
}
展示层
接下来,我们创建我们的 presenter,并实现上面定义的接口。这一层是我们应用程序中最复杂的层,因此我将详细解释。
在本教程中,所有依赖项都将在构造函数中注入,这些依赖项将由实例化 presenter 的人定义。我们可以使用工具来处理 DI(如 Dagger),但这不是本文的重点。现在,我们只需要一个简单的依赖注入来帮助我们的单元测试。
以下是我们 presenter 的每个依赖项的简要描述:
- **charactersDataSource:**Star Wars 角色端点的 Retrofit 描述。
- **backgroundScheduler:**我们的 API 请求 Observable 将在其上运行的 Scheduler。
- **mainScheduler:**我们希望我们的 observer 等待 API 请求 Observable 回调的 Scheduler。
- **view:**实现上面定义的 view interface 的任何实例(可以是模拟)。
在 constructor 方法中,我们还必须初始化我们的 CompositeSubscription 实例,这是一个将由 Observables 生成的所有 Subscriptions 持有的对象。当不再需要响应时,此对象将用于取消订阅 Observers(例如:应用程序进入后台状态)。
这是Presenter的初始实现,包括它的属性和构造函数:
@NonNull
private CharactersDataSource charactersDataSource;
@NonNull
private Scheduler backgroundScheduler;
@NonNull
private Scheduler mainScheduler;
@NonNull
private CompositeSubscription subscriptions;
private MainContract.View view;public MainPresenter(
@NonNull CharactersDataSource charactersDataSource,
@NonNull Scheduler backgroundScheduler,
@NonNull Scheduler mainScheduler,
MainContract.View view) {
this.charactersDataSource = charactersDataSource;
this.backgroundScheduler = backgroundScheduler;
this.mainScheduler = mainScheduler;
this.view = view;
subscriptions = new CompositeSubscription();
}
Presenter中的_loadData_方法以外的其他接口方法的实现都很简单和易于理解:
@Override
public void subscribe() {
loadData();
}
@Override
public void unsubscribe() {
subscriptions.clear();
}
@Override
public void onDestroy() {
this.view = null;
}
最后,我们实现了_loadData_方法,它将使用_CharactersDataSource_实例执行API请求,并在成功或错误的情况下通知_view_。
@Override
public void loadData() {
view.onFetchDataStarted();
subscriptions.clear();
Subscription subscription = charactersDataSource
.getCharacters()
.subscribeOn(backgroundScheduler)
.observeOn(mainScheduler)
.subscribe(new Observer<CharactersResponseModel>() {
@Override
public void onCompleted() {
view.onFetchDataCompleted();
}
@Override
public void onError(Throwable e) {
view.onFetchDataError();
}
@Override
public void onNext(CharactersResponseModel rootModel) {
view.onFetchDataSuccess(rootModel);
}
});
subscriptions.add(subscription);
}
Presenter测试
即使没有_view_实现(在这种情况下是_Activity_),我们也可以测试_presenter。在本教程中,我们只关注两个测试用例:
- 给定_presenter_已经请求数据并且其数据源已成功返回角色数据,我想验证_view_是否接收到它。
- 给定_presenter_已经请求数据并且数据源以某种方式返回错误,我想验证_view_是否接收到适当的反馈。
在默认的Android Studio项目创建中,已经包含了一个名为_ExampleUnitTest.java_的简单单元测试类的_package_。这个_package_通常以你的_applicationId_加上_test_为名。我们将在这个_package_中创建一个名为_MainPresenterTest.java_的类。
在我们的测试类中,我们将首先声明我们的模拟对象,这些对象是对象测试(presenter)所需的。需要模拟的两个依赖项是_view_和_data source_。
@Mock
private CharactersDataSource charactersDataSource;
@Mock
private MainContract.View view;
“@Mock”注释来自我们在_build.gradle_中声明的_Mockito_依赖项,这意味着该库将负责创建一个模拟实例。
为了确保每个新测试都创建一个新的_mock_,因此所有测试都是独立的,我们将在这个测试类的“@Before”步骤中初始化_mocks_。
@Before
public void setup() {
MockitoAnnotations.initMocks(this);
}
我们将通过JUnit的“@Test”注释表示每个测试用例,通过点击方法名称旁边的绿色图标运行该测试。下面的测试应该通过,因为没有实现。
@Test
public void fetchValidDataShouldLoadIntoView() {}
我们现在将实现此测试用例,从定义数据源模拟的行为开始。由于我们希望单元测试快速且不依赖于Internet连接,因此我们将告诉数据源返回一个固定的响应。
@Test
public void fetchValidDataShouldLoadIntoView() { CharactersResponseModel charactersResponseModel = new CharactersResponseModel(0, null, null, null); when(charactersDataSource.getCharacters())
.thenReturn(Observable.just(charactersResponseModel));
}
上面的代码意味着每当调用_getCharacters()_方法时,返回声明的_CharactersResponseModel_实例。
现在我们应该实例化presenter并传递_mocks_作为依赖项:
MainPresenter mainPresenter = new MainPresenter(
this.charactersDataSource,
Schedulers.immediate(),
Schedulers.immediate(),
this.view
);
这里的一个技巧是“Schedulers.immediate()”作为后台和主调度程序,这样在获取角色数据时就不会有延迟。
接下来,我们调用presenter接口的_loadData_方法,这将允许我们编写测试断言。这是我们测试用例的当前状态:
public void fetchValidDataShouldLoadIntoView() {
CharactersResponseModel charactersResponseModel = new CharactersResponseModel(0, null, null, null);
when(charactersDataSource.getCharacters())
.thenReturn(Observable.just(charactersResponseModel));
MainPresenter mainPresenter = new MainPresenter(
this.charactersDataSource,
Schedulers.immediate(),
Schedulers.immediate(),
this.view
);
mainPresenter.loadData();
}
简而言之,在调用此序列中的_loadData_之后,将测试以下_view_断言:
- onFetchDataStarted方法应该被调用一次
- onFetchDataSuccess方法应该被调用一次,并返回由_CharactersDataSource_返回的完全相同的模型。
- onFetchDataCompleted方法应该被调用。
InOrder inOrder = Mockito.inOrder(view);inOrder.verify(view, times(1)).onFetchDataStarted();
inOrder.verify(view, times(1)).onFetchDataSuccess(charactersResponseModel);
inOrder.verify(view, times(1)).onFetchDataCompleted();
现在我们编写一个类似的测试用例,但针对错误情况:
@Test
public void fetchErrorShouldReturnErrorToView() {
Exception exception = new Exception();
when(charactersDataSource.getCharacters())
.thenReturn(Observable.<CharactersResponseModel>error(exception));
MainPresenter mainPresenter = new MainPresenter(
this.charactersDataSource,
Schedulers.immediate(),
Schedulers.immediate(),
this.view
);
mainPresenter.loadData();
InOrder inOrder = Mockito.inOrder(view);
inOrder.verify(view, times(1)).onFetchDataStarted();
inOrder.verify(view, times(1)).onFetchDataError(exception);
verify(view, never()).onFetchDataCompleted();
}
结论
MVP架构加上_Mockito_和_Reactive Extensions_使单元测试变得非常简单。我们只为一个屏幕应用程序编写了基本的测试用例,但当应用程序开始增长时,软件质量和开发速度的改进将更加明显。
除了自动化手动测试的优势外,我个人也喜欢单元测试给我的代码带来的信心,使其能够在不引入错误的情况下进行重构。
译自:https://medium.com/android-news/unit-testing-api-requests-on-android-5efc4efe18df
评论(0)