Data layer testing (1)

在切回去写 domain layer 之前,我们先把之前写好的 data layer class 补回 unit test。在开始写之前,我们要先加入一些 testing 会用到的 dependency(StriktMockK):

dependencies {
    testImplementation platform("io.strikt:strikt-bom:$striktVersion")
    testImplementation "io.strikt:strikt-core"
    testImplementation "io.strikt:strikt-mockk"

    testImplementation "io.mockk:mockk:$mockkVersion"
    androidTestImplementation "io.mockk:mockk-android:$mockkVersion"
}

Strikt 是 assertion library,就是用来检验那个 variable 是不是 null、等於甚麽、如果是 collection 的话亦可以检验里面是不是有这些项目,是不是完全按照那个顺序等等的 library。类似的 library 有 kotlin.testTruthHamcrestAssertJ。其实用甚麽 assertion library 都是个人喜好,只要做到 assertion 而且有错时能指出清晰的错处就可以了。至於 MockK 是 Kotlin 的 mock library,它就是用来把 class 或 interface 做一个假的版本,那你就可以控制它回传甚麽,这种做法在 unit testing 时很常用到。

现在我们就写第一个 unit test:EtaResponseMapperTest

EtaResponseMapper 只有一个 public 的 function:map。那我们的目标是通过传入不同的 HttpResponse 从而把所有使用 EtaResponseMapper 的情景都试一次。另一个讲法是要把整个 EtaResponseMapper 的所有分支都走遍一次,亦即是指 branch coverage。

首先我们先写 unit test class 最基本的东西:

class EtaResponseMapperTest {

    private lateinit var mapper: Mapper<HttpResponse, EtaResult>

    @Before
    fun setUp() {
        mapper = EtaResponseMapper()
    }

    @Test
    fun `internal server error`() {
    }
}

我们第一个 test 就试 Internal Server Error 的情景。Function 名为方便阅读我用了 backtick 包住,这样 function 名就可以有 space character。

@Test
fun `internal server error`() = runBlockingTest {
		val response = mockk<HttpResponse>()
		val httpClientCall = mockk<HttpClientCall>()
		every { response.status } returns HttpStatusCode.InternalServerError
		every { response.call } returns httpClientCall
		coEvery { httpClientCall.receive<EtaResponse>() } returns EtaResponse(
			  status = EtaResponse.STATUS_ERROR_OR_ALERT,
			  message = "Error",
		)
		expectThat(mapper.map(response)).isA<EtaResult.InternalServerError>()
}

包住 runBlockingTest 是因为我们会在入面 call suspended function。

其实 mapper 会用到 HttpResponsestatusreceive。所以我们就要针对这两个东西来换成自己想要的东西,令到我们可以让程序是做到 Internal Server Error 的情景。

首先 mockk<HttpResponse>() 的意思是做一个假的 HttpResponse。然後 every { response.status } returns HttpStatusCode.InternalServerError 就是说凡是执行 response.status 都会回传 HttpStatusCode.InternalServerError。那就是满足 EtaResponseMapper 入面的 when (o.status) { ... } 能进去 HttpStatusCode.InternalServerError 的部分。

其实要试 InternalServerError 的话,是不用再 mock 其他东西。但为了其他 test case,我们会示范 mock 拿 response data class 的部分。

看看 HttpStatusCode.OK 的部分,它会 call receive。一般来说我们会 mock 那个 receive 让它回传我们想看到的东西。不过,当我们看一看那个 receive 的话,就会发现它是一个 inline function:

public suspend inline fun <reified T> HttpResponse.receive(): T = call.receive(typeInfo<T>()) as T

Inline function 的意思是 compile 时 Kotlin compiler 会将那个 function 内容抄到 call 那个 function 的位置,之後就没有那个 function 的㾗迹。所以我们再追踪那个 callreceive

public abstract val call: HttpClientCall
public suspend fun receive(info: TypeInfo): Any

这次不是 inline function,那我们可以 mock 了。首先是要 mock HttpClientCallresponse.call 回传我们另一个假的 HttpClientCall。之後因为之前的 inline function 会 call receive 取得 response data class,而 receive 是一个 suspended function,我们要用 MockK 的 coEvery 控制它回传我们想要的 object。

val httpClientCall = mockk<HttpClientCall>()
every { response.call } returns httpClientCall
coEvery { httpClientCall.receive<EtaResponse>() } returns EtaResponse(
	  status = EtaResponse.STATUS_ERROR_OR_ALERT,
	  message = "Error",
)

因为控制 HTTP client 的 response 是每一个 test 都会做的东西,我们就把这几句抽取成为一个 function:

private fun mockHttpResponse(
    statusCode: HttpStatusCode,
    etaResponse: EtaResponse
): HttpResponse {
    val response = mockk<HttpResponse>()
    val httpClientCall = mockk<HttpClientCall>()
    every { response.status } returns statusCode
    every { response.call } returns httpClientCall
    coEvery { httpClientCall.receive<EtaResponse>() } returns etaResponse
    return response
}

最後先前那个 test 就可以变成这样:

@Test
fun `internal server error`() = runBlockingTest {
    val response = mockHttpResponse(
        statusCode = HttpStatusCode.InternalServerError,
        etaResponse = EtaResponse(
            status = EtaResponse.STATUS_ERROR_OR_ALERT,
            message = "Error",
        ),
    )
    expectThat(mapper.map(response)).isA<EtaResult.InternalServerError>()
}

最後一句 expectThat 是 Strikt 的写法,expectThat 入面放的是要检验的项目,然後就可以继续串接 Strikt 的 method 就能针对它做检验。

因为篇幅有点长,我们在下一篇示范正常输出班次的情景。


<<:  DAY23-JAVA的例外

>>:  android studio 30天学习笔记-day 10-rxjava2+retrofit

申请海外新创加速器好难

今年申请了两个加速器:Y Combinator、Berkley SkyDeck 这两个加速器在国际上...

#3 Python教学2

基本运算子 最最基本的运算子-赋值运算子 「=」 是最基本的运算子,它的作用是将 「=」 右方的数值...

心得,完结洒花

Unity Shader 今天是铁人赛的最後一天,本来想说继续写点东西,毕竟Shader,应该说「电...

[DAY-26] 做有意义的事,不要便宜行事 / 说实话,或至少不要说谎

做有意义的事,不要便宜行事 Pursue what is meaningful (not what...

学习Python纪录Day6 - String type和Container type的运算子

String type和Container type的运算子 连接运算子 重复运算子 成员运算子 关...