在切回去写 domain layer 之前,我们先把之前写好的 data layer class 补回 unit test。在开始写之前,我们要先加入一些 testing 会用到的 dependency(Strikt 和 MockK):
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.test、Truth、Hamcrest 和 AssertJ。其实用甚麽 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 会用到 HttpResponse
的 status
和 receive
。所以我们就要针对这两个东西来换成自己想要的东西,令到我们可以让程序是做到 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 的㾗迹。所以我们再追踪那个 call
和 receive
。
public abstract val call: HttpClientCall
public suspend fun receive(info: TypeInfo): Any
这次不是 inline function,那我们可以 mock 了。首先是要 mock HttpClientCall
让 response.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 就能针对它做检验。
因为篇幅有点长,我们在下一篇示范正常输出班次的情景。
>>: android studio 30天学习笔记-day 10-rxjava2+retrofit
今年申请了两个加速器:Y Combinator、Berkley SkyDeck 这两个加速器在国际上...
基本运算子 最最基本的运算子-赋值运算子 「=」 是最基本的运算子,它的作用是将 「=」 右方的数值...
Unity Shader 今天是铁人赛的最後一天,本来想说继续写点东西,毕竟Shader,应该说「电...
做有意义的事,不要便宜行事 Pursue what is meaningful (not what...
String type和Container type的运算子 连接运算子 重复运算子 成员运算子 关...