首页
Preview

在Android上进行API请求的单元测试

在本文中,我想向你展示一个使用 RxAndroidRetrofitMockitoModel 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 请求 返回的响应的模型。

我的建议是使用不可变模型,因为它具有优势。现在,我将所有属性设置为 publicfinal,而不是为每个属性创建 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 层之间的协议

接下来,我们将创建一个定义 presenterview 之间通信的 interfaceinterface 是必要的,以保持每个类不相关并使它们更容易在测试中模拟。

视图接口 将具有以下方法:

  • **onFetchDataStarted:**通知视图请求即将开始。对于提供用户反馈,如显示 进度条 等非常有用。
  • **onFetchDataCompleted:**通知视图不再返回更多数据。
  • **onFetchDataSuccess:**将请求的数据作为其参数返回,以我们上面定义的模型类型。
  • **onFetchDataError:**请求期间发生错误。例如,可能是连接失败。

主持人接口 将具有以下方法:

  • **loadData:**将告诉 presenter 开始获取数据。
  • **subscribe:**通知 presenterview 已变为活动状态。这可用于触发 API 请求。
  • **unsubscribe:**通知 presenterview 已变为非活动状态。这可用于取消尚未返回的任何先前请求。
  • **onDestroy:**通知 presenterview 实例即将被销毁。在本教程中,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

版权声明:本文内容由TeHub注册用户自发贡献,版权归原作者所有,TeHub社区不拥有其著作权,亦不承担相应法律责任。 如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

点赞(0)
收藏(0)
阿波
The minute I see you, I want your clothes gone!

评论(0)

添加评论