使用 MockWebServer 挂起功能测试 [英] Suspending function test with MockWebServer
问题描述
我正在测试使用 MockWebServer 的挂起函数返回结果的 api,但它不适用于 runBlockingTest、testCoroutineDispatcher、testCorounieScope,除非使用了 launch
构建器,为什么?
I'm testing api that returns result using suspending function with MockWebServer, but it does not work with runBlockingTest, testCoroutineDispatcher, testCorounieScope unless a launch
builder is used, why?
abstract class AbstractPostApiTest {
internal lateinit var mockWebServer: MockWebServer
private val responseAsString by lazy {
getResourceAsText(RESPONSE_JSON_PATH)
}
@BeforeEach
open fun setUp() {
mockWebServer = MockWebServer()
println("AbstractPostApiTest setUp() $mockWebServer")
}
@AfterEach
open fun tearDown() {
mockWebServer.shutdown()
}
companion object {
const val RESPONSE_JSON_PATH = "posts.json"
}
@Throws(IOException::class)
fun enqueueResponse(
code: Int = 200,
headers: Map<String, String>? = null
): MockResponse {
// Define mock response
val mockResponse = MockResponse()
// Set response code
mockResponse.setResponseCode(code)
// Set headers
headers?.let {
for ((key, value) in it) {
mockResponse.addHeader(key, value)
}
}
// Set body
mockWebServer.enqueue(
mockResponse.setBody(responseAsString)
)
return mockResponse
}
}
class PostApiTest : AbstractPostApiTest() {
private lateinit var postApi: PostApiCoroutines
private val testCoroutineDispatcher = TestCoroutineDispatcher()
private val testCoroutineScope = TestCoroutineScope(testCoroutineDispatcher)
@BeforeEach
override fun setUp() {
super.setUp()
val okHttpClient = OkHttpClient
.Builder()
.build()
postApi = Retrofit.Builder()
.baseUrl(mockWebServer.url("/"))
.addConverterFactory(GsonConverterFactory.create())
.client(okHttpClient)
.build()
.create(PostApiCoroutines::class.java)
Dispatchers.setMain(testCoroutineDispatcher)
}
@AfterEach
override fun tearDown() {
super.tearDown()
Dispatchers.resetMain()
try {
testCoroutineScope.cleanupTestCoroutines()
} catch (exception: Exception) {
exception.printStackTrace()
}
}
@Test
fun `Given we have a valid request, should be done to correct url`() =
testCoroutineScope.runBlockingTest {
// GIVEN
enqueueResponse(200, RESPONSE_JSON_PATH)
// WHEN
postApi.getPostsResponse()
advanceUntilIdle()
val request = mockWebServer.takeRequest()
// THEN
Truth.assertThat(request.path).isEqualTo("/posts")
}
}
结果错误:java.lang.IllegalStateException:此作业尚未完成
如果使用 launch
构建器,则此测试不起作用,如果使用 launch
构建器,则不需要 testCoroutineDispatcher
或 testCoroutineScope
,这是什么原因?通常挂起的函数即使使用 runBlockingTest
This test does not work if launch
builder is used, and if launch
builder is used it does not require testCoroutineDispatcher
or testCoroutineScope
, what's the reason for this? Normally suspending functions pass without being in another scope even with runBlockingTest
@Test
fun `Given we have a valid request, should be done to correct url`() =
runBlockingTest {
// GIVEN
enqueueResponse(200, RESPONSE_JSON_PATH)
// WHEN
launch {
postApi.getPosts()
}
val request = mockWebServer.takeRequest()
// THEN
Truth.assertThat(request.path).isEqualTo("/posts")
}
上面的那个有效.
下面的测试也通过了一些时间.
Also the test below pass some of the time.
@测试fun 给定 api 返回 200,应该有帖子列表
() =testCoroutineScope.runBlockingTest {
@Test
fun Given api return 200, should have list of posts
() =
testCoroutineScope.runBlockingTest {
// GIVEN
enqueueResponse(200)
// WHEN
var posts: List<Post> = emptyList()
launch {
posts = postApi.getPosts()
}
advanceUntilIdle()
// THEN
Truth.assertThat(posts).isNotNull()
Truth.assertThat(posts.size).isEqualTo(100)
}
我尝试了很多组合调用 posts = postApi.getPosts()
而没有 launch
,使用 async
,把 enqueueResponse(200)
在 async async { enqueueResponse(200) }.await()
但测试失败,有时它通过有时它不与每个组合一些.
I tried many combinations invoking posts = postApi.getPosts()
without launch
, using async
, putting enqueueResponse(200)
inside async async { enqueueResponse(200) }.await()
but tests failed, sometimes it pass sometimes it does not some with each combination.
推荐答案
runBlockTest 在完成运行测试的协程之前不等待其他线程/作业完成.
There is a bug with runBlockTest not waiting for other threads/jobs to complete before completing the coroutine that the test is running in.
我尝试使用 runBlocking
成功(我使用 Hamcrest 的很棒的移植到 Kotlin 哈姆克雷斯特)
I tried using runBlocking
with success (I use the awesome port of Hamcrest to Kotlin Hamkrest)
fun `run test` = runBlocking {
mockWebServer.enqueue(MockResponse().setResponseCode(200).setBody(""))
// make HTTP call
val result = mockWebServer.takeRequest(2000L, TimeUnit.MILLISECONDS)
assertThat(result != null, equalTo(true))
}
这里有几点需要注意:
线程阻塞调用的使用应该绝不在没有超时的情况下被调用.最好是一无所获,然后永远阻塞一个线程.
The use of thread blocking calls should never be called without a timeout. Always better to fail with nothing, then to block a thread forever.
runBlocking
的使用可能被一些人认为是不可以.但是这篇博文概述了不同的运行并发代码的方法,以及它们的不同用例.我们通常希望使用 runBlockingTest
或 (TestCoroutineDispatcher.runBlockingTest
) 以便我们的测试代码和应用程序代码同步.通过使用相同的 Dispatcher,我们可以确保所有作业都完成,等等.TestCoroutineDispatcher
也有那个方便的时钟"功能使延迟消失.然而,当测试应用程序的 HTTP 层时,如果有一个模拟服务器在单独的线程上运行,我们有一个同步点 takeRequest
.所以我们可以愉快地使用 runBlocking
来允许我们使用协程和运行在不同线程上的模拟服务器一起工作,没有问题.
The use of runBlocking
might be considered by some to be no no. However this blog post outlines the different method of running concurrent code, and the different use cases for them. We normally want to use runBlockingTest
or (TestCoroutineDispatcher.runBlockingTest
) so that our test code and app code are synchronised. By using the same Dispatcher we can make sure that the jobs all finish, etc. TestCoroutineDispatcher
also has that handy "clock" feature to make delays disappear. However when testing the HTTP layer of the application, and where there is a mock server running on a separate thread we have a synchronisation point being takeRequest
. So we can happily use runBlocking
to allow us to use coroutines and a mock server running on a different thread work together with no problems.
这篇关于使用 MockWebServer 挂起功能测试的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!