如何在单元测试中处理模拟的 RxJava2 可观察抛出异常
How to handle mocked RxJava2 observable throwing exception in unit test
过去几周我一直在使用 MVP 在 Android 中使用 Kotlin 进行 TDD。事情进展顺利。
我使用 Mockito 来模拟 classes,但我似乎无法克服如何实现我想要 运行 的测试之一。
以下是我的测试:
- 调用api,接收数据列表,然后显示列表。
loadAllPlacesTest()
- 调用api,接收空数据,然后显示列表。
loadEmptyPlacesTest()
- 调用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
实现接口的管理器 class 如下所示:
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"))
}
}
}}
Problem/Questions 为 loadExceptionPlacesTest()
- 我不确定为什么代码没有转到演示者的
onError()
。
如果我说错了,请纠正我,但这是我的想法:
-
a - `apiManager.getPlacesPagedObservable("", 0)` observable 本身会抛出异常,这就是为什么 `.subscribe()` 不能 happen/proceed 并且不会调用观察者的方法,
-
b - 它只会在可观察对象内部的操作遇到像 JSONException 这样的异常时转到 onError()
- 对于
loadExceptionPlacesTest()
我认为上面的 1b 是使演示者的 onError()
被调用并使测试通过的方法。这个对吗?如果是怎么做就测试吧。如果不是,你们能指出我遗漏或做错了什么吗?
我会把它留在这里以供将来参考,并且能够详细说明,即使我已经在评论中回答了。
您要完成的是将流放入 onError
流中。不幸的是,通过这样嘲笑它:
Mockito.`when`(apiManager.getPlacesPagedObservable(
Mockito.anyString(), Mockito.anyInt()))
.thenThrow(Exception(EXCEPTION_MESSAGE1))
你实际上是在告诉 Mockito 设置你的 mock,只要调用 apiManager.getPlacesPagedObservable(anystring, anystring)
就应该抛出异常。
在 Rx 流中抛出异常确实会导致整个流停止并在 onError
方法中结束。但是,这正是您使用的方法的问题。抛出异常时,您不在流中。
相反,您想要做的是告诉 Mockito,一旦您调用 apiManager.getPlacesPagedObservable(anystring, anystring)
,您想要 return 一个将在 onError
中结束的流。这可以通过 Observable.error()
轻松实现,如下所示:
Mockito.`when`(apiManager.getPlacesPagedObservable(
Mockito.anyString(), Mockito.anyInt()))
.thenReturn(Observable.error(
Exception(EXCEPTION_MESSAGE1)))
(您可能需要在此处的这一部分添加一些类型信息 Observable.error()
,您可能还需要使用其他东西来代替 observable - 单一的、可完成的、等等)
上面的模拟将告诉 Mockito 将您的模拟设置为 return 一个一旦被订阅就会出错的可观察对象。这将反过来将您的订阅者直接放入 onError
流中,并带有指定的异常。
过去几周我一直在使用 MVP 在 Android 中使用 Kotlin 进行 TDD。事情进展顺利。
我使用 Mockito 来模拟 classes,但我似乎无法克服如何实现我想要 运行 的测试之一。
以下是我的测试:
- 调用api,接收数据列表,然后显示列表。
loadAllPlacesTest()
- 调用api,接收空数据,然后显示列表。
loadEmptyPlacesTest()
- 调用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 实现接口的管理器 class 如下所示:
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"))
}
}
}}
Problem/Questions 为 loadExceptionPlacesTest()
- 我不确定为什么代码没有转到演示者的
onError()
。 如果我说错了,请纠正我,但这是我的想法:
- a - `apiManager.getPlacesPagedObservable("", 0)` observable 本身会抛出异常,这就是为什么 `.subscribe()` 不能 happen/proceed 并且不会调用观察者的方法,
- b - 它只会在可观察对象内部的操作遇到像 JSONException 这样的异常时转到 onError()
- 对于
loadExceptionPlacesTest()
我认为上面的 1b 是使演示者的onError()
被调用并使测试通过的方法。这个对吗?如果是怎么做就测试吧。如果不是,你们能指出我遗漏或做错了什么吗?
我会把它留在这里以供将来参考,并且能够详细说明,即使我已经在评论中回答了。
您要完成的是将流放入 onError
流中。不幸的是,通过这样嘲笑它:
Mockito.`when`(apiManager.getPlacesPagedObservable(
Mockito.anyString(), Mockito.anyInt()))
.thenThrow(Exception(EXCEPTION_MESSAGE1))
你实际上是在告诉 Mockito 设置你的 mock,只要调用 apiManager.getPlacesPagedObservable(anystring, anystring)
就应该抛出异常。
在 Rx 流中抛出异常确实会导致整个流停止并在 onError
方法中结束。但是,这正是您使用的方法的问题。抛出异常时,您不在流中。
相反,您想要做的是告诉 Mockito,一旦您调用 apiManager.getPlacesPagedObservable(anystring, anystring)
,您想要 return 一个将在 onError
中结束的流。这可以通过 Observable.error()
轻松实现,如下所示:
Mockito.`when`(apiManager.getPlacesPagedObservable(
Mockito.anyString(), Mockito.anyInt()))
.thenReturn(Observable.error(
Exception(EXCEPTION_MESSAGE1)))
(您可能需要在此处的这一部分添加一些类型信息 Observable.error()
,您可能还需要使用其他东西来代替 observable - 单一的、可完成的、等等)
上面的模拟将告诉 Mockito 将您的模拟设置为 return 一个一旦被订阅就会出错的可观察对象。这将反过来将您的订阅者直接放入 onError
流中,并带有指定的异常。