【问题标题】:Unit testing android application with retrofit and rxjava使用改造和 rxjava 对 android 应用程序进行单元测试
【发布时间】:2017-02-11 03:08:41
【问题描述】:

我开发了一个使用 rxJava 改造的 android 应用程序,现在我正在尝试使用 Mockito 设置单元测试,但我不知道如何模拟 api 响应以创建不打真正的电话,但有虚假的回应。

例如,我想测试方法 syncGenres 是否适用于我的 SplashPresenter。我的课程如下:

public class SplashPresenterImpl implements SplashPresenter {

private SplashView splashView;

public SplashPresenterImpl(SplashView splashView) {
    this.splashView = splashView;
}

@Override
public void syncGenres() {
    Api.syncGenres(new Subscriber<List<Genre>>() {
        @Override
        public void onError(Throwable e) {
            if(splashView != null) {
                splashView.onError();
            }
        }

        @Override
        public void onNext(List<Genre> genres) {
            SharedPreferencesUtils.setGenres(genres);
            if(splashView != null) {
                splashView.navigateToHome();
            }
        }
    });
}
}

Api 类是这样的:

public class Api {
    ...
    public static Subscription syncGenres(Subscriber<List<Genre>> apiSubscriber) {
        final Observable<List<Genre>> call = ApiClient.getService().syncGenres();
        return call
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(apiSubscriber);
    }

}

现在我正在尝试测试 SplashPresenterImpl 类,但我不知道该怎么做,我应该这样做:

public class SplashPresenterImplTest {

@Mock
Api api;
@Mock
private SplashView splashView;

@Captor
private ArgumentCaptor<Callback<List<Genre>>> cb;

private SplashPresenterImpl splashPresenter;

@Before
public void setupSplashPresenterTest() {
    // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To
    // inject the mocks in the test the initMocks method needs to be called.
    MockitoAnnotations.initMocks(this);

    // Get a reference to the class under test
    splashPresenter = new SplashPresenterImpl(splashView);
}

@Test
public void syncGenres_success() {

    Mockito.when(api.syncGenres(Mockito.any(ApiSubscriber.class))).thenReturn(); // I don't know how to do that

    splashPresenter.syncGenres();
    Mockito.verify(api).syncGenres(Mockito.any(ApiSubscriber.class)); // I don't know how to do that



}
}

您对我应该如何模拟和验证 api 响应有任何想法吗? 提前致谢!

编辑: 遵循@invariant 的建议,现在我将一个客户端对象传递给我的演示者,并且该 api 返回一个 Observable 而不是订阅。但是,在进行 api 调用时,我的订阅服务器上出现 NullPointerException。测试类如下所示:

public class SplashPresenterImplTest {
@Mock
Api api;
@Mock
private SplashView splashView;

private SplashPresenterImpl splashPresenter;

@Before
public void setupSplashPresenterTest() {
    // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To
    // inject the mocks in the test the initMocks method needs to be called.
    MockitoAnnotations.initMocks(this);

    // Get a reference to the class under test
    splashPresenter = new SplashPresenterImpl(splashView, api);
}

@Test
public void syncGenres_success() {
    Mockito.when(api.syncGenres()).thenReturn(Observable.just(Collections.<Genre>emptyList()));


    splashPresenter.syncGenres();


    Mockito.verify(splashView).navigateToHome();
}
}

为什么我会收到 NullPointerException?

非常感谢!

【问题讨论】:

    标签: android mocking mockito rx-java retrofit2


    【解决方案1】:

    如何测试 RxJava 和改造

    1。摆脱静态调用——使用依赖注入

    您的代码中的第一个问题是您使用静态方法。这不是一个可测试的架构,至少不容易,因为它使模拟实现变得更加困难。 为了正确地做事,不要使用访问ApiClient.getService()Api,而是通过构造函数将此服务注入到presenter:

    public class SplashPresenterImpl implements SplashPresenter {
    
    private SplashView splashView;
    private final ApiService service;
    
    public SplashPresenterImpl(SplashView splashView, ApiService service) {
        this.splashView = splashView;
        this.apiService = service;
    }
    

    2。创建测试类

    实现您的 JUnit 测试类并在 @Before 方法中使用模拟依赖项初始化演示者:

    public class SplashPresenterImplTest {
    
    @Mock
    ApiService apiService;
    
    @Mock
    SplashView splashView;
    
    private SplashPresenter splashPresenter;
    
    @Before
    public void setUp() throws Exception {
        this.splashPresenter = new SplashPresenter(splashView, apiService);
    }
    

    3。模拟和测试

    然后是实际的模拟和测试,例如:

    @Test
    public void testEmptyListResponse() throws Exception {
        // given
        when(apiService.syncGenres()).thenReturn(Observable.just(Collections.emptyList());
        // when
        splashPresenter.syncGenres();
        // then
        verify(... // for example:, verify call to splashView.navigateToHome()
    }
    

    这样您就可以测试您的 Observable + 订阅,如果您想测试 Observable 的行为是否正确,请使用 TestSubscriber 的实例订阅它。


    疑难解答

    在使用 RxJava 和 RxAndroid 调度程序(例如 Schedulers.io()AndroidSchedulers.mainThread())进行测试时,您在运行 observable/subscription 测试时可能会遇到一些问题。

    空指针异常

    第一个是NullPointerException在应用给定调度程序的行上抛出,例如:

    .observeOn(AndroidSchedulers.mainThread()) // throws NPE
    

    原因是AndroidSchedulers.mainThread() 内部是一个LooperScheduler,它使用了android 的Looper 线程。这种依赖在 JUnit 测试环境中不可用,因此调用会导致 NullPointerException。

    比赛条件

    第二个问题是,如果应用的调度器使用单独的工作线程来执行 observable,那么执行@Test 方法的线程和所述工作线程之间就会出现竞争条件。通常它会导致在可观察执行完成之前返回测试方法。

    解决方案

    上述两个问题都可以通过提供符合测试的调度程序来轻松解决,而且选择很少:

    1. 使用RxJavaHooksRxAndroidPlugins API 覆盖对Schedulers.?AndroidSchedulers.? 的任何调用,强制Observable 使用,例如Scheduler.immediate()

      @Before
      public void setUp() throws Exception {
              // Override RxJava schedulers
              RxJavaHooks.setOnIOScheduler(new Func1<Scheduler, Scheduler>() {
                  @Override
                  public Scheduler call(Scheduler scheduler) {
                      return Schedulers.immediate();
                  }
              });
      
              RxJavaHooks.setOnComputationScheduler(new Func1<Scheduler, Scheduler>() {
                  @Override
                  public Scheduler call(Scheduler scheduler) {
                      return Schedulers.immediate();
                  }
              });
      
              RxJavaHooks.setOnNewThreadScheduler(new Func1<Scheduler, Scheduler>() {
                  @Override
                  public Scheduler call(Scheduler scheduler) {
                      return Schedulers.immediate();
                  }
              });
      
              // Override RxAndroid schedulers
              final RxAndroidPlugins rxAndroidPlugins = RxAndroidPlugins.getInstance();
              rxAndroidPlugins.registerSchedulersHook(new RxAndroidSchedulersHook() {
                  @Override
                  public Scheduler getMainThreadScheduler() {
                      return Schedulers.immediate();
              }
          });
      }
      
      @After
      public void tearDown() throws Exception {
          RxJavaHooks.reset();
          RxAndroidPlugins.getInstance().reset();
      }
      

      这段代码必须封装 Observable 测试,所以它可以在 @Before@After 内完成,如图所示,它可以放入 JUnit @Rule 或放在代码中的任何位置。只是不要忘记重置挂钩。

    2. 第二种选择是通过依赖注入向类(Presenters、DAO)提供显式的Scheduler 实例,然后再次使用Schedulers.immediate()(或其他适合测试的方法)。

    3. 正如@aleien 所指出的,您还可以使用注入的RxTransformer 实例来执行Scheduler 应用程序。

    我在生产中使用了第一种方法,效果很好。

    【讨论】:

    • 非常感谢,请问如何同时测试成功和错误情况?
    • 对于错误测试,您可以使用Observable.error(Throwable throwable) 工厂方法,例如when(apiService.syncGenres()).thenReturn(new RuntimeException());。如果您想更具体地了解错误,请使用改造的HttpException
    • 非常感谢
    • 我已经更新了我的答案,在使用 api 对象并将其传递给我的演示者之后,我的订阅者收到了 nullpointerexception 错误。到底是怎么回事?非常感谢
    • 异常的堆栈跟踪是什么?你的主持人现在看起来怎么样?可能的原因是:您正在使用忘记模拟的其他依赖项;您正在使用Schedulers.io() 或类似的(有一些方法可以在Schedulers.immediate() 上强制执行,这将完成这项工作,例如通过注入调度程序实例或使用RxJavaHooks - @aleien 指出,所以请参阅答案中的链接)
    【解决方案2】:

    让您的syncGenres 方法返回Observable 而不是Subscription。然后你可以模拟这个方法返回Observable.just(...),而不是进行真正的api调用。

    如果您想在该方法中保留Subscription 作为返回值(我不建议这样做,因为它破坏了Observable 的可组合性),您需要使该方法不是静态的,并传递任何@987654327 @ 作为构造函数参数返回并在测试中使用模拟的服务对象(这种技术称为依赖注入)

    【讨论】:

      【解决方案3】:

      您从 api 方法返回 Subscription 是否有任何特殊原因?从 api 方法返回 Observable(或 Single)通常更方便(特别是关于 Retrofit 能够生成 Observables 和 Singles 而不是调用)。如果没有特殊原因,我建议切换到这样的:

      public interface Api {
          @GET("genres")
          Single<List<Genre>> syncGenres();
          ...
      }
      

      因此您对 api 的调用将如下所示:

      ...
      Api api = retrofit.create(Api.class);
      api.syncGenres()
         .subscribeOn(Schedulers.io())
         .observeOn(AndroidSheculers.mainThread())
         .subscribe(genres -> soStuff());
      

      这样,你就可以模拟 api 类并编写:

      List<Genre> mockedGenres = Arrays.asList(genre1, genre2...);
      Mockito.when(api.syncGenres()).thenReturn(Single.just(mockedGenres));
      

      您还必须考虑到您将无法在工作线程上测试响应,因为测试不会等待它们。为了绕过这个问题,我会 recommend reading these articles 并考虑使用调度程序管理器或transformer 之类的东西,以便能够明确告诉演示者使用哪些调度程序(真实的或测试的)

      【讨论】:

      • 非常感谢您的回答。我会看看你在说什么。一个问题,为什么要使用 Single 而不是 Observable?非常感谢
      • 通常您不会从 api 响应中接收到事件序列,而只会收到一大块数据(即使该数据是列表 (: )。Single 是什么 - 只有一个发出的对象。您可以使用 Observable,这真的没问题,但 Single 更具体地说明您的意图是什么 - 您只等待一个对象。此外,如果您想使用 Single.create() 创建自己的对象,则需要实现更少的方法。tldr:它比 Observable (:
      • 非常感谢。你有一个例子来学习如何使用 RxTransformer.java 吗?
      • 您只需将其传递给演示者,而不是编写 .subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()... 使用 .compose(rxTransformer.chainSchedulers()),例如这个:gist.github.com/aleien/52a079138156570017d64d2dbd8b627e
      【解决方案4】:

      我使用这些类:

      1. 服务
      2. 远程数据源
      3. RemoteDataSourceTest
      4. 主题演讲者
      5. TopicPresenterTest

      简单的服务:

      public interface Service {
          String URL_BASE = "https://guessthebeach.herokuapp.com/api/";
      
          @GET("topics/")
          Observable<List<Topics>> getTopicsRx();
      
      }
      

      对于远程数据源

      public class RemoteDataSource implements Service {
      
          private Service api;
      
          public RemoteDataSource(Retrofit retrofit) {
      
      
              this.api = retrofit.create(Service.class);
          }
      
      
          @Override
          public Observable<List<Topics>> getTopicsRx() {
              return api.getTopicsRx();
          }
      }
      

      密钥是MockWebServer from okhttp3

      这个库可以很容易地测试你的应用在进行 HTTP 和 HTTPS 调用时是否正确。它允许您指定要返回的响应,然后验证请求是否按预期发出。

      因为它会测试您的完整 HTTP 堆栈,所以您可以确信自己正在测试所有内容。您甚至可以从您的真实 Web 服务器复制和粘贴 HTTP 响应来创建有代表性的测试用例。或者测试您的代码在 500 错误或加载缓慢的响应等难以重现的情况下仍然存在。

      使用 MockWebServer 的方式与使用 Mockito 等模拟框架的方式相同:

      编写模拟脚本。 运行应用程序代码。 验证是否发出了预期的请求。 这是 RemoteDataSourceTest 中的一个完整示例:

      public class RemoteDataSourceTest {
      
          List<Topics> mResultList;
          MockWebServer mMockWebServer;
          TestSubscriber<List<Topics>> mSubscriber;
      
          @Before
          public void setUp() {
              Topics topics = new Topics(1, "Discern The Beach");
              Topics topicsTwo = new Topics(2, "Discern The Football Player");
              mResultList = new ArrayList();
              mResultList.add(topics);
              mResultList.add(topicsTwo);
      
              mMockWebServer = new MockWebServer();
              mSubscriber = new TestSubscriber<>();
          }
      
          @Test
          public void serverCallWithError() {
              //Given
              String url = "dfdf/";
              mMockWebServer.enqueue(new MockResponse().setBody(new Gson().toJson(mResultList)));
              Retrofit retrofit = new Retrofit.Builder()
                      .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                      .addConverterFactory(GsonConverterFactory.create())
                      .baseUrl(mMockWebServer.url(url))
                      .build();
              RemoteDataSource remoteDataSource = new RemoteDataSource(retrofit);
      
              //When
              remoteDataSource.getTopicsRx().subscribe(mSubscriber);
      
              //Then
              mSubscriber.assertNoErrors();
              mSubscriber.assertCompleted();
          }
      
          @Test
          public void severCallWithSuccessful() {
              //Given
              String url = "https://guessthebeach.herokuapp.com/api/";
              mMockWebServer.enqueue(new MockResponse().setBody(new Gson().toJson(mResultList)));
              Retrofit retrofit = new Retrofit.Builder()
                      .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                      .addConverterFactory(GsonConverterFactory.create())
                      .baseUrl(mMockWebServer.url(url))
                      .build();
              RemoteDataSource remoteDataSource = new RemoteDataSource(retrofit);
      
              //When
              remoteDataSource.getTopicsRx().subscribe(mSubscriber);
      
              //Then
              mSubscriber.assertNoErrors();
              mSubscriber.assertCompleted();
          }
      
      }
      

      您可以在GitHubthis tutorial 中查看我的示例。

      此外,在演示者中,您可以看到我使用 RxJava 进行的服务器调用:

      public class TopicPresenter implements TopicContract.Presenter {
      
          @NonNull
          private TopicContract.View mView;
      
          @NonNull
          private BaseSchedulerProvider mSchedulerProvider;
      
          @NonNull
          private CompositeSubscription mSubscriptions;
      
          @NonNull
          private RemoteDataSource mRemoteDataSource;
      
      
          public TopicPresenter(@NonNull RemoteDataSource remoteDataSource, @NonNull TopicContract.View view, @NonNull BaseSchedulerProvider provider) {
              this.mRemoteDataSource = checkNotNull(remoteDataSource, "remoteDataSource");
              this.mView = checkNotNull(view, "view cannot be null!");
              this.mSchedulerProvider = checkNotNull(provider, "schedulerProvider cannot be null");
      
              mSubscriptions = new CompositeSubscription();
      
              mView.setPresenter(this);
          }
      
          @Override
          public void fetch() {
      
              Subscription subscription = mRemoteDataSource.getTopicsRx()
                      .subscribeOn(mSchedulerProvider.computation())
                      .observeOn(mSchedulerProvider.ui())
                      .subscribe((List<Topics> listTopics) -> {
                                  mView.setLoadingIndicator(false);
                                  mView.showTopics(listTopics);
                              },
                              (Throwable error) -> {
                                  try {
                                      mView.showError();
                                  } catch (Throwable t) {
                                      throw new IllegalThreadStateException();
                                  }
      
                              },
                              () -> {
                              });
      
              mSubscriptions.add(subscription);
          }
      
          @Override
          public void subscribe() {
              fetch();
          }
      
          @Override
          public void unSubscribe() {
              mSubscriptions.clear();
          }
      
      }
      

      现在是 TopicPresenterTest:

      @RunWith(MockitoJUnitRunner.class)
      public class TopicPresenterTest {
      
          @Mock
          private RemoteDataSource mRemoteDataSource;
      
          @Mock
          private TopicContract.View mView;
      
          private BaseSchedulerProvider mSchedulerProvider;
      
          TopicPresenter mThemePresenter;
      
          List<Topics> mList;
      
          @Before
          public void setup() {
              MockitoAnnotations.initMocks(this);
      
              Topics topics = new Topics(1, "Discern The Beach");
              Topics topicsTwo = new Topics(2, "Discern The Football Player");
              mList = new ArrayList<>();
              mList.add(topics);
              mList.add(topicsTwo);
      
              mSchedulerProvider = new ImmediateSchedulerProvider();
              mThemePresenter = new TopicPresenter(mRemoteDataSource, mView, mSchedulerProvider);
      
      
          }
      
          @Test
          public void fetchData() {
      
              when(mRemoteDataSource.getTopicsRx())
                      .thenReturn(rx.Observable.just(mList));
      
              mThemePresenter.fetch();
      
              InOrder inOrder = Mockito.inOrder(mView);
              inOrder.verify(mView).setLoadingIndicator(false);
              inOrder.verify(mView).showTopics(mList);
      
          }
      
          @Test
          public void fetchError() {
      
              when(mRemoteDataSource.getTopicsRx())
                      .thenReturn(Observable.error(new Throwable("An error has occurred!")));
              mThemePresenter.fetch();
      
              InOrder inOrder = Mockito.inOrder(mView);
              inOrder.verify(mView).showError();
              verify(mView, never()).showTopics(anyList());
          }
      
      }
      

      您可以在GitHubthis article. 中查看我的示例

      【讨论】:

        【解决方案5】:

        解决方案是@maciekjanusz 与解释一起完美,所以我只会说这个,当您使用Schedulers.io()AndroidSchedulers.mainThread() 时会出现问题。 @maciekjanusz 的答案的问题在于它太复杂而无法理解,而且仍然不是每个人都使用 Dagger2(他们应该这样做)。另外,我不太确定,但使用 RxJava2 我对 RxJavaHooks 的导入无法正常工作。

        RxJava2 的更好解决方案:-

        RxSchedulersOverrideRule 添加到您的测试包中,然后在您的测试类中添加以下行。

        @Rule
        public RxSchedulersOverrideRule schedulersOverrideRule = new RxSchedulersOverrideRule();
        

        就是这样,没什么可补充的,你的测试用例现在应该可以正常运行了。

        【讨论】:

          【解决方案6】:

          我遇到了同样的问题

          .observeOn(AndroidSchedulers.mainThread())

          我用以下代码修复了它

          public class RxJavaUtils {
              public static Supplier<Scheduler> getSubscriberOn = () -> Schedulers.io();
              public static Supplier<Scheduler> getObserveOn = () -> AndroidSchedulers.mainThread();
          }
          

          并像这样使用它

          deviceService.findDeviceByCode(text)
                      .subscribeOn(RxJavaUtils.getSubscriberOn.get())
                      .observeOn(RxJavaUtils.getObserveOn.get())
          

          在我的测试中

          @Before
          public void init(){
              getSubscriberOn = () -> Schedulers.from(command -> command.run()); //Runs in curren thread
              getObserveOn = () -> Schedulers.from(command -> command.run()); //runs also in current thread
          }
          

          也适用于 io.reactivex

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2020-09-05
            • 2019-06-02
            • 2020-03-07
            • 2017-09-19
            • 1970-01-01
            • 1970-01-01
            • 2018-06-13
            • 2014-10-22
            相关资源
            最近更新 更多