【问题标题】:How to handle mocked RxJava2 observable throwing exception in unit test如何在单元测试中处理模拟的 RxJava2 可观察抛出异常
【发布时间】:2017-11-25 04:17:29
【问题描述】:

过去几周我一直在 Kotlin 中使用 MVP 在 Android 中进行 TDD。事情一直进展顺利。

我使用 Mockito 来模拟类,但我似乎无法理解如何实现我想要运行的测试之一。

以下是我的测试:

  1. 调用api,接收数据列表,然后显示列表。 loadAllPlacesTest()
  2. 调用api,接收空数据,然后显示列表。 loadEmptyPlacesTest()
  3. 调用api,途中发生异常,然后显示错误信息。 loadExceptionPlacesTest()

我成功地测试了#1 和#2。 问题出在#3,我不确定如何在代码中进行测试。

RestApiInterface.kt

interface RestApiInterface {

@GET(RestApiManager.PLACES_URL)
fun getPlacesPagedObservable(
        @Header("header_access_token") accessToken: String?,
        @Query("page") page: Int?
): Observable<PlacesWrapper>
}

RestApiManager.kt 实现接口的管理器类如下所示:

open class RestApiManager: RestApiInterface{
var api: RestApiInterface
    internal set
internal var retrofit: Retrofit
init {
    val logging = HttpLoggingInterceptor()
    // set your desired log level
    logging.setLevel(HttpLoggingInterceptor.Level.BODY)

    val client = okhttp3.OkHttpClient().newBuilder()
            .readTimeout(60, TimeUnit.SECONDS)
            .connectTimeout(60, TimeUnit.SECONDS)
            .addInterceptor(LoggingInterceptor())  
            .build()


    retrofit = Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(client)
            .addConverterFactory(GsonConverterFactory.create())
            .addCallAdapterFactory(RxJava2CallAdapterFactory.create())//very important for RXJAVA and retrofit
            .build()
    api = retrofit.create(RestApiInterface::class.java)
}
override fun getPlacesPagedObservable(accessToken: String?, page: Int?): Observable<PlacesWrapper> {
    //return throw Exception("sorry2")
    return api.getPlacesPagedObservable(
            accessToken,
            page)
}
}

}

这是我的单元测试:

class PlacesPresenterImplTest : AndroidTest(){

lateinit var presenter:PlacesPresenterImpl
lateinit var view:PlacesView
lateinit var apiManager:RestApiManager
//lateinit var apiManager:RestApiManager

val EXCEPTION_MESSAGE1 = "SORRY"

val MANY_PLACES = Arrays.asList(PlaceItem(), PlaceItem());
var EXCEPTION_PLACES = Arrays.asList(PlaceItem(), PlaceItem());


val manyPlacesWrapper = PlacesWrapper(MANY_PLACES)
var exceptionPlacesWrapper = PlacesWrapper(EXCEPTION_PLACES)
val emptyPlacesWrapper = PlacesWrapper(Collections.emptyList())

@After
fun clear(){
    RxJavaPlugins.reset()
}
@Before
fun init(){
    //MOCKS THE subscribeOn(Schedulers.io()) to use the same thread the test is being run on
    //Schedulers.trampoline() runs the test in the same thread used by the test
    RxJavaPlugins.setIoSchedulerHandler { t -> Schedulers.trampoline() }

    view = Mockito.mock<PlacesView>(PlacesView::class.java)
    apiManager = Mockito.mock(RestApiManager::class.java)
    presenter = PlacesPresenterImpl(view,context(), Bundle(), Schedulers.trampoline())
    presenter.apiManager = apiManager

    //exceptionPlacesWrapper = throw Exception(EXCEPTION_MESSAGE1);
}


@Test
fun loadAllPlacesTest() {
    Mockito.`when`(apiManager.getPlacesPagedObservable(Mockito.anyString(), Mockito.anyInt())).thenReturn(Observable.just(manyPlacesWrapper))

    presenter.__populate()
    Mockito.verify(view, Mockito.atLeastOnce()).__showLoading()
    Mockito.verify(view, Mockito.atLeastOnce())._showList()
    Mockito.verify(view).__hideLoading()
    Mockito.verify(view).__showFullScreenMessage(Mockito.anyString())
}

@Test
fun loadEmptyPlacesTest() {

    Mockito.`when`(apiManager.getPlacesPagedObservable(Mockito.anyString(), Mockito.anyInt())).thenReturn(Observable.just(emptyPlacesWrapper))
    presenter.__populate()
    Mockito.verify(view, Mockito.atLeastOnce()).__showLoading()
    Mockito.verify(view, Mockito.atLeastOnce())._showList()
    Mockito.verify(view).__hideLoading()
    Mockito.verify(view).__showFullScreenMessage(Mockito.anyString())
}

@Test
fun loadExceptionPlacesTest() {
    Mockito.`when`(apiManager.getPlacesPagedObservable(Mockito.anyString(), Mockito.anyInt())).thenThrow(Exception(EXCEPTION_MESSAGE1))
    presenter.__populate()
    Mockito.verify(view, Mockito.atLeastOnce()).__showLoading()
    Mockito.verify(view, Mockito.never())._showList()
    Mockito.verify(view).__hideLoading()
    Mockito.verify(view).__showFullScreenMessage(EXCEPTION_MESSAGE1)
}
}

PlacesPresenterImpl.kt 这是主持人。

   class PlacesPresenterImpl
constructor(var view: PlacesView, var context: Context, var savedInstanceState:Bundle?, var mainThread: Scheduler)
: BasePresenter(), BasePresenterInterface, PlacesPresenterInterface {

lateinit var apiManager:RestApiInterface
var placeListRequest: Disposable? = null


override fun __firstInit() {
    apiManager = RestApiManager()
}

override fun __init(context: Context, savedInstanceState: Bundle, view: BaseView?) {
    this.view = view as PlacesView
    if (__isFirstTimeLoad())
        __firstInit()
}


override fun __destroy() {
    placeListRequest?.dispose()
}

override fun __populate() {
    _callPlacesApi()
}


override fun _callPlacesApi() {
    view.__showLoading()
    apiManager.getPlacesPagedObservable("", 0)
            .subscribeOn(Schedulers.io())
            .observeOn(mainThread)
            .subscribe (object : DisposableObserver<PlacesWrapper>() {
                override fun onNext(placesWrapper: PlacesWrapper) {
                    placesWrapper?.let {
                        val size = placesWrapper.place?.size
                        view.__hideLoading()
                        view._showList()
                        System.out.println("Great I found " + size + " records of places.")
                        view.__showFullScreenMessage("Great I found " + size + " records of places.")
                    }
                    System.out.println("onNext()")
                }

                override fun onError(e: Throwable) {
                    System.out.println("onError()")
                    //e.printStackTrace()
                    view.__hideLoading()
                    if (ExceptionsUtil.isNoNetworkException(e)){
                        view.__showFullScreenMessage("So sad, can not connect to network to get place list.")
                    }else{
                        view.__showFullScreenMessage("Oops, something went wrong. ["+e.localizedMessage+"]")
                    }

                    this.dispose()
                }

                override fun onComplete() {
                    this.dispose()
                    //System.out.printf("onComplete()")
                }
            })


}

private fun _getEventCompletionObserver(): DisposableObserver<String> {
    return object : DisposableObserver<String>() {
        override fun onNext(taskType: String) {
            //_log(String.format("onNext %s task", taskType))
        }

        override fun onError(e: Throwable) {
            //_log(String.format("Dang a task timeout"))
            //Timber.e(e, "Timeout Demo exception")
        }

        override fun onComplete() {
            //_log(String.format("task was completed"))
        }
    }
}}

loadExceptionPlacesTest() 的问题/疑问

  1. 我不确定为什么代码没有发送到演示者的onError()如果我有以下错误,请纠正我,但这是我的想法:
  • a - `apiManager.getPlacesPagedObservable("", 0)` observable 本身会引发异常,这就是为什么 `.subscribe()` 不能发生/继续并且观察者的方法不会被调用的原因,
  • b - 只有当 observable 内部的操作遇到 JSONException 之类的异常时,它才会转到 onError()
  1. 对于loadExceptionPlacesTest(),我认为上面的1b 是让演示者的onError() 被调用并通过测试的方法。它是否正确?如果是怎么做就测试。如果不是,你们能指出我遗漏了什么或做错了什么吗?

【问题讨论】:

  • 你试过Mockito.`when`(apiManager.getPlacesPagedObservable(Mockito.anyString(), Mockito.anyInt())).thenReturn(Observable.error()) 吗?我认为您拥有它的方式是在 Rx 流之外引发异常。因此onError 不会发生。这就像说,当你调用这个方法时抛出这个异常,但你想要的是:当你调用这个方法时,返回一个错误的 observable 并将流放入onError。您可能需要调整 Observable.error() 部分以返回您需要的类型。
  • @Fred 感谢您的确认。你是对的,异常是在 Rx 流之外引发的。 Observable.error() 是我一直在寻找的答案。

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


【解决方案1】:

尽管我已经在 cmets 中回答了,但我将把它留在这里以供将来参考并能够详细说明

您要完成的是将流放入onError 流中。不幸的是,像这样嘲笑它:

Mockito.`when`(apiManager.getPlacesPagedObservable(
                   Mockito.anyString(), Mockito.anyInt()))
                           .thenThrow(Exception(EXCEPTION_MESSAGE1))

您实际上是在告诉 Mockito 设置您的模拟,只要调用 apiManager.getPlacesPagedObservable(anystring, anystring) 就会引发异常。

确实,在 Rx 流中抛出异常会导致整个流停止并最终出现在 onError 方法中。但是,这正是您使用的方法的问题。抛出异常时,您不在流中。

相反,您想要做的是告诉 Mockito,一旦您调用 apiManager.getPlacesPagedObservable(anystring, anystring),您希望返回一个将在 onError 中结束的流。这可以通过Observable.error() 轻松实现,如下所示:

Mockito.`when`(apiManager.getPlacesPagedObservable(
               Mockito.a‌​nyString(), Mockito.anyInt()))
                    .thenReturn(Observable.error(
                                Exception(EXCEPTION_MESSAGE1)))

(你可能需要在这部分添加一些类型信息Observable.error(),你可能还需要使用其他东西来代替 observable——single、completable 等。 )

上面的 mocking 会告诉 Mockito 设置你的 mock 以返回一个 observable,一旦它被订阅就会出错。这反过来会将您的订阅者直接放入onError 流中,但指定的例外情况。

【讨论】:

  • 我在测试 Rx 错误路径时遇到了同样的问题,而 Observable.error(exception) 是我的解决方案。谢谢
猜你喜欢
  • 1970-01-01
  • 2014-10-10
  • 1970-01-01
  • 1970-01-01
  • 2021-11-13
  • 2013-10-10
  • 1970-01-01
  • 2022-11-04
  • 1970-01-01
相关资源
最近更新 更多