Unit testing 模拟改装暂停功能无限响应
我想测试服务器不返回响应时的情况,我们触发下一个网络调用(例如搜索查询) 所以我们基本上有一个方法,内部视图模型和改造方法Unit testing 模拟改装暂停功能无限响应,unit-testing,mockito,kotlin-coroutines,android-viewmodel,Unit Testing,Mockito,Kotlin Coroutines,Android Viewmodel,我想测试服务器不返回响应时的情况,我们触发下一个网络调用(例如搜索查询) 所以我们基本上有一个方法,内部视图模型和改造方法 interface RetrofitApi { @GET("Some Url") suspend fun getVeryImportantStuff(): String } class TestViewModel(private val api: RetrofitApi) : ViewModel() { private var askJob:
interface RetrofitApi {
@GET("Some Url")
suspend fun getVeryImportantStuff(): String
}
class TestViewModel(private val api: RetrofitApi) : ViewModel() {
private var askJob: Job? = null
fun load(query: String) {
askJob?.cancel()
askJob = viewModelScope.launch {
val response = api.getVeryImportantStuff()
//DO SOMETHING WITH RESPONSE
}
}
}
当新的查询被询问,而旧的查询没有返回时,我想测试这个用例。
对于响应返回的情况,测试很容易
@Test
fun testReturnResponse() {
runBlockingTest {
//given
val mockApi:RetrofitApi = mock()
val viewModel = TestViewModel(mockApi)
val response = "response from api"
val query = "fancy query"
whenever(mockApi.getVeryImportantStuff()).thenReturn(response)
//when
viewModel.load(query)
//then
//verify what happens
}
}
但我不知道如何模拟并没有返回的挂起函数,并在新请求被触发时进行测试
@Test
fun test2Loads() {
runBlockingTest {
//given
val mockApi:RetrofitApi = mock()
val viewModel = TestViewModel(mockApi)
val response = "response from api"
val secondResponse = "response from api2"
val query = "fancy query"
whenever(mockApi.getVeryImportantStuff())
.thenReturn(/* Here return some fancy stuff that is suspend* or something like onBlocking{} stub but not blocking but dalayed forever/)
.thenReturn(secondResponse)
//when
viewModel.load(query)
viewModel.load(query)
//then
//verify that first response did not happens , and only second one triggered all the stuff
}
}
class TestViewModelTest {
@get:Rule
val coroutineScope = MainCoroutineScopeRule()
@get:Rule
val instantTaskExecutorRule = InstantTaskExecutorRule()
lateinit var retrofit: RetrofitApi
lateinit var utils: CoroutineUtils
val tottalyDifferentDispatcher = TestCoroutineDispatcher()
lateinit var viewModel: TestViewModel
@Before
fun setup() {
retrofit = mock()
utils = mock()
viewModel = TestViewModel(retrofit, utils)
}
@UseExperimental(ExperimentalCoroutinesApi::class)
@Test
fun test2Loads() {
runBlockingTest {
//given
val response = "response from api"
val response2 = "response from api2"
val query = "fancy query"
val query2 = "fancy query2"
whenever(utils.io)
.thenReturn(tottalyDifferentDispatcher)
val mutableListOfStrings = mutableListOf<String>()
whenever(retrofit.getVeryImportantStuff(query)).thenReturn(response)
whenever(retrofit.getVeryImportantStuff(query2)).thenReturn(response2)
//when
viewModel.testStream.observeForever {
mutableListOfStrings.add(it)
}
tottalyDifferentDispatcher.pauseDispatcher()
viewModel.load(query)
viewModel.load(query2)
tottalyDifferentDispatcher.resumeDispatcher()
//then
mutableListOfStrings shouldHaveSize 1
mutableListOfStrings[0] shouldBe response2
verify(retrofit, times(1)).getVeryImportantStuff(query2)
}
}
}
interface RetrofitWrapper {
suspend fun getVeryImportantStuff(): suspend (String)->String
}
class TestViewModel(private val api: RetrofitWrapper,
private val utils: CoroutineUtils) : ViewModel() {
private val text = MutableLiveData<String>()
val testStream: LiveData<String> = text
private var askJob: Job? = null
fun load(query: String) {
askJob?.cancel()
askJob = viewModelScope.launch {
val veryImportantStuff = api.getVeryImportantStuff()
val response = withContext(utils.io) {
veryImportantStuff(query)
}
text.postValue(response)
}
}
}
有什么想法吗
编辑:我不是真的喜欢mockito,任何模拟库都会很好:)
当做
Wojtek看起来像是要在服务器无法访问、超时或类似情况下测试场景 在这种情况下,在执行模拟时,您可以说在第一次尝试时它返回对象,然后在第二次执行时抛出异常,如
java.net.ConnectException:Connection timed out
whenever(mockApi.getVeryImportantStuff())
.thenReturn(someObjet)
.thenThrow(ConnectException("timed out"))
这应该是可行的,但您必须在ViewModel中执行try/catch块,这并不理想。我建议您添加额外的抽象
您可以使用存储库
或用例
或任何您喜欢的模式/名称将网络调用移动到那里。然后引入sealed class Result
来封装行为,并使您的ViewModel
更具可读性
class TestViewModel(val repo: Repo): ViewModel() {
private var askJob: Job? = null
fun load(query: String) {
askJob?.cancel()
askJob = viewModelScope.launch {
when (repo.getStuff()) {
is Result.Success -> TODO()
is Result.Failure -> TODO()
}
}
}
}
class Repo(private val api: Api) {
suspend fun getStuff() : Result {
return try {
Result.Success(api.getVeryImportantStuff())
} catch (e: java.lang.Exception) {
Result.Failure(e)
}
}
}
sealed class Result {
data class Success<out T: Any>(val data: T) : Result()
data class Failure(val error: Throwable) : Result()
}
interface Api {
suspend fun getVeryImportantStuff() : String
}
类TestViewModel(val repo:repo):ViewModel(){
私有变量askJob:作业?=null
乐趣加载(查询:字符串){
askJob?.cancel()
askJob=viewModelScope.launch{
何时(repo.getStuff()){
是Result.Success->TODO()
是Result.Failure->TODO()
}
}
}
}
类别回购(专用val api:api){
suspend fun getStuff():结果{
回击{
Result.Success(api.getveryiimportantstuff())
}catch(e:java.lang.Exception){
结果.失败(e)
}
}
}
密封类结果{
数据类成功(val数据:T):结果()
数据类失败(val错误:可丢弃):结果()
}
接口Api{
suspend fun getVeryImportantStuff():字符串
}
通过这种抽象级别,您的ViewModelTest
只检查两种情况下发生的情况
希望对你有帮助 我想出了解决这个问题的办法,但与我一开始的想法略有不同
interface CoroutineUtils {
val io: CoroutineContext
}
interface RetrofitApi {
@GET("Some Url")
suspend fun getVeryImportantStuff(query: String): String
}
class TestViewModel(private val api: RetrofitApi,
private val utils: CoroutineUtils) : ViewModel() {
private val text = MutableLiveData<String>()
val testStream: LiveData<String> = text
private var askJob: Job? = null
fun load(query: String) {
askJob?.cancel()
askJob = viewModelScope.launch {
val response = withContext(utils.io) { api.getVeryImportantStuff(query) }
text.postValue(response)
}
}
}
并对其进行测试
@Test
fun test2Loads() {
runBlockingTest {
//given
val response = "response from api"
val response2 = "response from api2"
val query = "fancy query"
val query2 = "fancy query2"
whenever(utils.io)
.thenReturn(tottalyDifferentDispatcher)
val mutableListOfStrings = mutableListOf<String>()
whenever(retrofit.getVeryImportantStuff())
.thenReturn(suspendCoroutine {
it.resume { response }
})
whenever(retrofit.getVeryImportantStuff()).thenReturn(suspendCoroutine {
it.resume { response2 }
})
//when
viewModel.testStream.observeForever {
mutableListOfStrings.add(it)
}
tottalyDifferentDispatcher.pauseDispatcher()
viewModel.load(query)
viewModel.load(query2)
tottalyDifferentDispatcher.resumeDispatcher()
//then
mutableListOfStrings shouldHaveSize 1
mutableListOfStrings[0] shouldBe response2
verify(retrofit, times(2)).getVeryImportantStuff()
}
}
@测试
有趣的test2Loads(){
运行阻塞测试{
//给定
val response=“来自api的响应”
val response2=“来自api2的响应”
val query=“花式查询”
val query2=“花式query2”
无论何时(utils.io)
.然后返回(总计不同的喷射器)
val mutableListOfStrings=mutableListOf()
无论何时(改装.getVeryImportantStuff())
.thenReturn(suspendCoroutine{
it.resume{response}
})
无论何时(改装.getVeryImportantStuff())。然后返回(suspendCoroutine{
it.resume{response2}
})
//什么时候
viewModel.testStream.observeForever{
mutableListOfStrings.add(它)
}
TotalyDifferentintDispatcher.pauseDispatcher()
加载(查询)
viewModel.load(查询2)
TotalyDifferentintDispatcher.resumeDispatcher()
//然后
可变字符串列表应保存大小1
mutableListOfStrings[0]应该是response2
验证(改装,次数(2)).getVeryImportantStuff()
}
}
但在我看来,这是一个有点太多的干扰,在代码中只能测试。但也许我错了:P不太可能,因为问题是恢复暂停,而不是例外。问题在于,当第一个响应并没有返回,而第二个请求被触发时,测试用例就开始了。将改型传递到ViewModel只是为了简单起见。但是谢谢你的努力,我想你不明白协同程序是如何工作的,或者我们无法理解彼此的问题是什么。协同路由挂起函数是异步执行的,所以基本上调用查询方法两次将触发两个不同的作业(在第一个作业未取消的情况下)。目前的目标是测试第一个被取消。在这个简单的场景中,但是还有很多地方这样的嘲弄是有用的是的,看起来我们彼此不理解。在您的场景中使用runBlockingTest将不起作用,因为每个挂起的值都将立即返回,所有协同路由要么必须完成,要么被取消。我认为您的测试场景在不支持线程的JUnit框架下是不可行的!我在考虑类似的东西,但我总是使用
val io:CoroutineDispatcher
。老实说,这看起来有点过分了,但现在我们知道这是可行的。是的,但是如果你想出了更好的主意,不要犹豫,告诉大家:D我不喜欢这个