【问题标题】:RxJava+Retrofit 2 unit test weird errorRxJava+Retrofit 2 单元测试奇怪的错误
【发布时间】:2017-03-09 22:58:07
【问题描述】:

我正在使用 MVP 模式创建 Android 应用程序。 为此,我正在使用 Retrofit 2 和 RxJava。应用运行良好

但是在单元测试中我遇到了奇怪的错误。相同的测试代码有时会通过,有时会失败。

此消息显示错误

Wanted but not invoked:
albumView.showProgress();
-> at kz.afckairat.kairat.media.AlbumPresenterTest.checkGetPhotoAlbums(AlbumPresenterTest.java:66)
Actually, there were zero interactions with this mock.

测试类

public class AlbumPresenterTest {

enter code here
private MediaService mediaService;

private AlbumView albumView;

private AlbumPresenterImpl photoAlbumPresenter;

@Before
public void setUp() throws Exception {

    albumView = mock(AlbumView.class);
    mediaService = mock(MediaService.class);

    photoAlbumPresenter = new AlbumPresenterImpl(albumView, mediaService, MediaType.PHOTO);

    RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() {
        @Override
        public Scheduler getMainThreadScheduler() {
            return Schedulers.immediate();
        }
    });
}

@After
public void tearDown() {
    RxAndroidPlugins.getInstance().reset();
}

@Test
public void checkGetPhotoAlbums() {
    List<Album> albums = getAlbumList();
    when(mediaService.getPhotoAlbums()).thenReturn(Observable.just(albums));

    photoAlbumPresenter.getAlbums();

    verify(albumView).showProgress();
    verify(albumView).showAlbums(albums);
    verify(albumView).hideProgress();

}

@Test
public void checkGetPhotoAlbumError() {
    String msg = "Error";
    when(mediaService.getPhotoAlbums()).thenReturn(Observable.error(new IOException(msg)));

    photoAlbumPresenter.getAlbums();

    verify(albumView).showProgress();
    verify(albumView).showError(msg);
    verify(albumView).hideProgress();
}


private List<Album> getAlbumList() {
    List<Album> albums = new ArrayList<>();
    Album album = new Album(1, "Test1", "test1.jpg", "01.01.2016", 2);
    albums.add(album);
    album = new Album(2, "Test2", "test2.jpg", "01.01.2016", 2);
    albums.add(album);
    return albums;
}
}

被测试的Presenter类

public class AlbumPresenterImpl implements AlbumPresenter {

private AlbumView view;
private MediaType type;
private List<Album> albums;
private MediaService mediaService;

public AlbumPresenterImpl(AlbumView view, MediaService mediaService, MediaType type) {
    this.view = view;
    this.mediaService = mediaService;
    this.type = type;
}

@Override
public void getAlbums() {
    Observable<List<Album>> observable;

    if (type.equals(MediaType.VIDEO)) {
        observable = mediaService.getVideoAlbums();
    } else {
        observable = mediaService.getPhotoAlbums();
    }

    observable.doOnSubscribe(view::showProgress)
            .doAfterTerminate(view::hideProgress)
            .subscribeOn(Schedulers.io())
            .observeOn(AndroidSchedulers.mainThread())
            .subscribe(items -> {
                albums = items;
                view.showAlbums(albums);
            }, throwable -> {
                view.showError(throwable.getLocalizedMessage());
            });
}

@Override
public void onResume() {
    if (albums == null) {
        getAlbums();
    }
}

@Override
public void onDestroy() {

}
}

为什么有时测试不通过?

非常感谢!

==================================

更新

@Fred 写的问题出在调度程序中

public class RxSchedulersOverrideRule implements TestRule {

private final RxJavaSchedulersHook mRxJavaSchedulersHook = new RxJavaSchedulersHook() {
    @Override
    public Scheduler getIOScheduler() {
        return Schedulers.immediate();
    }

    @Override
    public Scheduler getNewThreadScheduler() {
        return Schedulers.immediate();
    }
};

private final RxAndroidSchedulersHook mRxAndroidSchedulersHook = new RxAndroidSchedulersHook() {
    @Override
    public Scheduler getMainThreadScheduler() {
        return Schedulers.immediate();
    }
};

// Hack to get around RxJavaPlugins.reset() not being public
// See https://github.com/ReactiveX/RxJava/issues/2297
// Hopefully the method will be public in new releases of RxAndroid and we can remove the hack.
private void callResetViaReflectionIn(RxJavaPlugins rxJavaPlugins)
        throws InvocationTargetException, IllegalAccessException, NoSuchMethodException {
    Method method = rxJavaPlugins.getClass().getDeclaredMethod("reset");
    method.setAccessible(true);
    method.invoke(rxJavaPlugins);
}

@Override
public Statement apply(final Statement base, Description description) {
    return new Statement() {
        @Override
        public void evaluate() throws Throwable {
            RxAndroidPlugins.getInstance().reset();
            RxAndroidPlugins.getInstance().registerSchedulersHook(mRxAndroidSchedulersHook);
            callResetViaReflectionIn(RxJavaPlugins.getInstance());
            RxJavaPlugins.getInstance().registerSchedulersHook(mRxJavaSchedulersHook);

            base.evaluate();

            RxAndroidPlugins.getInstance().reset();
            callResetViaReflectionIn(RxJavaPlugins.getInstance());
        }
    };
}

}

代码取自 Github a link!

在测试类中

@Rule
public final RxSchedulersOverrideRule mOverrideSchedulersRule = new RxSchedulersOverrideRule();

【问题讨论】:

  • 听起来你覆盖了主线程调度以立即返回,但你没有覆盖Schedulers.io()。因此,如果我的想法是正确的,那么测试会在一个线程中运行,而演示者订阅者会在另一个线程中运行,从而导致竞争条件有时会按您的预期工作。你可以为了测试而尝试,只是为了在任何地方使用immediate调度器。
  • 非常感谢!你是对的。问题出在调度程序中
  • 好的,我会写一个更完整的答案,这样其他人也可以从中受益:)

标签: android unit-testing mockito rx-java retrofit2


【解决方案1】:

您似乎用以下方式覆盖了主线程调度程序:

RxAndroidPlugins.getInstance().registerSchedulersHook(new RxAndroidSchedulersHook() {
    @Override
    public Scheduler getMainThreadScheduler() {
        return Schedulers.immediate();
    }
});

但从代码来看,observables 仍然在Schedulers.io() 调度器上运行:

observable.doOnSubscribe(view::showProgress)
        .doAfterTerminate(view::hideProgress)
        .subscribeOn(Schedulers.io())
        // ...

您可能知道,即时调度程序在当前线程中执行代码,我猜因为您跳转到 io 调度程序,它与运行测试的调度程序不同。

这将使测试在一个线程中运行,而订阅者/可观察对象在另一个线程中运行。这可以解释为什么有时测试通过,有时却没有。有一个竞争条件。

最简单的基本方法是确保在测试时您在Schedulers.immediate() 上同时拥有observeOnsubscribeOn,并且在运行时您拥有正确的,即Schedulers.io()AndroidSchedulers.mainThread()

您可以通过重写调度程序、将它们作为构造函数传递来做到这一点,或者您甚至可以查看this,其中 Dan Lew 解释了如何使用compose 创建调度程序转换器。然后,您可以确保您的类在运行时使用适当的调度程序转换器,并在测试时使用一些转换器,将所有内容放在直接线程上。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2017-01-01
    • 1970-01-01
    • 2018-03-29
    • 1970-01-01
    • 2017-05-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多