如何在单元测试中处理模拟的RxJava2可观察抛出异常

时间:2017-06-22 04:12:43

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

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

我使用Mockito来模拟类,但我似乎无法克服如何实现我想要运行的其中一个测试。

以下是我的测试:

  1. 调用api,接收数据列表,然后显示列表。 loadAllPlacesTest()
  2. 调用api,接收空数据,然后显示列表。 loadEmptyPlacesTest()
  3. 调用api,路上会发生一些异常,然后显示错误信息。 loadExceptionPlacesTest()
  4. 我成功测试了#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 实现接口的manager类如下所示:

    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. 我不确定为什么代码不会转到Presenter的onError()如果我错了以下,请更正我,但这就是我的想法:
      •     a - `apiManager.getPlacesPagedObservable(“”,0)`observable本身抛出一个异常,这就是为什么`.subscribe()`不能发生/继续,并且观察者的方法不会被调用,
      •     b - 当observable中的操作遇到类似JSONException的异常时,它只会转到onError()
      1. 对于loadExceptionPlacesTest()我认为上面的 1b 是让演示者的onError()被调用并让测试通过的方法。它是否正确?如果是在测试中如何做到这一点。如果不是,你们可以指出我错过了什么或做错了吗?

1 个答案:

答案 0 :(得分:10)

我会留在这里以供将来参考,并且能够详细说明,即使我已经在评论中回答了

您要完成的是将流放入Mockito.`when`(apiManager.getPlacesPagedObservable( Mockito.anyString(), Mockito.anyInt())) .thenThrow(Exception(EXCEPTION_MESSAGE1)) 流程。不幸的是,通过嘲笑它:

apiManager.getPlacesPagedObservable(anystring, anystring)

你实际上是在告诉Mockito以一种只调用onError会抛出异常的方式来设置你的模拟。

确实在Rx流中抛出异常会导致整个流停止并以apiManager.getPlacesPagedObservable(anystring, anystring)方法结束。但是,这正是您正在使用的方法的问题。抛出异常时,您不在流中。

相反,您想要做的是告诉Mockito,一旦您致电onError,您想要返回最终会在Observable.error()中的流。这可以通过Mockito.`when`(apiManager.getPlacesPagedObservable( Mockito.a‌​nyString(), Mockito.anyInt())) .thenReturn(Observable.error( Exception(EXCEPTION_MESSAGE1))) 轻松实现,如下所示:

Observable.error()

您可能需要在此处onError添加此部分中的某些类型信息,您可能还需要使用其他内容而不是可观察的 - 单一,可完成等。

上面的模拟将告诉Mockito设置你的模拟以返回一个可以在订阅后立即出错的observable。这会将您的订阅者直接置于具有指定异常的{{1}}流中。