使用 MockWebServer 挂起功能测试 [英] Suspending function test with MockWebServer

查看:28
本文介绍了使用 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 构建器,则不需要 testCoroutineDispatchertestCoroutineScope,这是什么原因?通常挂起的函数即使使用 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))
}

这里有几点需要注意:

  1. 线程阻塞调用的使用应该绝不在没有超时的情况下被调用.最好是一无所获,然后永远阻塞一个线程.

  1. 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屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆