Reader 是我们 Android library 里面最外层的 API ,要测试它要先考虑它有跟那些元件作互动,以下列出了它有互动的元件:
ktRssReaderConfig
RssCache
XmlFetcher
Parser
这些互动的元件就是我们要做 mocking 的目标。
typealias Config = KtRssReaderConfig.() -> Unit
object Reader {
val logTag: String = this::class.java.simpleName
@Throws(Exception::class)
inline fun <reified T> read(
url: String,
customParser: ((String) -> T?) = { null },
config: Config = {},
): T {
// 略
}
@Throws(Exception::class)
suspend inline fun <reified T> coRead(
url: String,
crossinline customParser: ((String) -> T?) = { null },
crossinline config: Config = {}
) = suspendCoroutine<T> {
// 略
}
inline fun <reified T> flowRead(
url: String,
crossinline customParser: ((String) -> T?) = { null },
crossinline config: Config = {}
) = flow<T> { emit(read(url = url, customParser = customParser, config = config)) }
fun clearCache() {
// 略
}
}
(完整程序码在这里)
这是 Reader 的 API ,我们可以看到它其实是提供了不同的读取 function: read
、 coRead
、 flowRead
,他们的差别只在於说是用哪种非同步的方式去读取,内部的流程是一致,所以我们不用特别写三组重复的 function 都测一样的东西,把它们不同的部分包成 lambda,让测试者决定要做甚麽事情,这其实就是有点像是注册一个 callback 。我们可以来准备一下共用的部分程序码:
abstract class ReaderTestBase {
protected val fakeUrl = "fakeUrl"
protected val fakeType = Const.RSS_STANDARD
private val fakeXmlContent = "fakeXmlContent"
@RelaxedMockK
protected lateinit var mockRssCache: DatabaseRssCache<RssStandardChannel>
@RelaxedMockK
protected lateinit var mockFetcher: XmlFetcher
@RelaxedMockK
protected lateinit var mockException: Exception
@Before
fun setup() {
MockKAnnotations.init(this)
mockkObject(ThreadUtils)
mockkObject(KtRssProvider)
}
@After
fun tearDown() {
clearAllMocks()
}
protected fun mockGetRemoteChannelSuccessfully(block: (RssStandardChannel) -> Unit) {
// 略
}
protected fun mockGetRemoteChannelFailed(block: () -> Unit) {
// 略
}
protected fun mockGetCacheChannelSuccessfully(block: (RssStandardChannel) -> Unit, ) {
// 略
}
protected fun mockGetCacheChannelFailed(block: (RssStandardChannel) -> Unit) {
// 略
}
protected fun mockFlushCache(block: (RssStandardChannel) -> Unit) {
// 略
}
protected fun mockFetchDataSuccessfullyButSaveCacheFailed(block: (RssStandardChannel) -> Unit) {
// 略
}
}
我们可以看到在 class 一开始我们先宣告一系列的 @RelaxedMockK
这些是我们在过程中会互动的元件,接着,在 setup 的部分, MockKAnnotations.init(this)
先初始化我们用到的 MockK annotation ,如果你是直接写 mockk<T>()
而没有用到其他的 annotation 的话,应该是不用初始化。我们还有准备 mock 两个 object , ThreadUtil
和 KtRssProvider
。 Mock ThreadUtil
是因为我们在 read
function 有检查呼叫的当下是否不是在 main thread ,而 KtRssProvider
是我们设计来注入一些会用到的互动元件,有一点点像是 dependency injection 的味道,但是是土炮版本,毕竟我们只有在这边有用到,不会直接引入一整个 DI library 。 KtRssProvider
注入的东西有 database、fetcher 、 parser 、 cache,待会我们就可以直接 mock KtRssProvider
对它指定回传的东西,是不是很方便?在每项测试结束的时候,我们要把上一个测项的 mock 物件清理乾净,所以在 @After
的地方要记得˙呼叫 clearAllMocks()
,确保下个测项正常运作。在这个类别的尾端,我们可以看到有几个 mockXXX 的 function ,里面都带有 block ,这个就是刚刚提到抽取出来不同流程的 lambda ,让外部使用者决定要测的东西。我们挑一个 function 了解一下里面的作法:
protected fun mockGetRemoteChannelSuccessfully(block: (RssStandardChannel) -> Unit) {
val expected = mockkRelaxed<RssStandardChannelData>()
every { ThreadUtils.isMainThread() } returns false
every { KtRssProvider.provideRssCache<RssStandardChannel>() } returns mockRssCache
every { mockRssCache.readCache(fakeUrl, fakeType, any()) } returns null
every { KtRssProvider.provideXmlFetcher() } returns mockFetcher
every { mockFetcher.fetch(url = fakeUrl, charset = any()) } returns fakeXmlContent
mockkConstructor(AndroidRssStandardParser::class)
every { anyConstructed<AndroidRssStandardParser>().parse(fakeXmlContent) } returns expected
every { ThreadUtils.runOnNewThread(any(), any()) } answers {
mockRssCache.saveCache(fakeUrl, expected)
}
block(expected)
}
这个 function 是准备正常读取流程会用到的 mock 物件与行为,为了接下来的流程准备,而准备完毕後就是呼叫 block
内的 lambda 来验证测试结果,所以今天不管这个流程是在一般的 read
被呼叫或是在 coroutine 里面被呼叫,都不用再写第二遍。另外,为什麽 reader 在读取和测试的时候都不会遇到型别错误的问题?因为我们在实作 parser 和 reader 的时候都是用泛型的方式去实作,所以保留了很大一部分的弹性,就算是用 annotation processor 产生的 parser 程序码也可以透过 reader 外部 API 塞进去。
class ReadTest : ReaderTestBase() {
// 略
@Test
fun `Get remote channel successfully`() = mockGetRemoteChannelSuccessfully { mockItem ->
val actual = Reader.read<RssStandardChannel>(fakeUrl) {
useCache = false
}
never {
mockRssCache.readCache(fakeUrl, fakeType, any())
mockRssCache.saveCache(fakeUrl, mockItem)
}
actual shouldBe mockItem
}
// 略
}
class FlowReadTest : ReaderTestBase() {
// 略
@Test
fun `Get remote channel successfully`() = mockGetRemoteChannelSuccessfully { mockItem ->
runBlocking {
Reader.flowRead<RssStandardChannel>(fakeUrl) {
useCache = false
}.test {
mockItem shouldBe expectItem()
expectComplete()
}
}
// 略
}
class CoroutineReadTest : ReaderTestBase() {
// 略
@Test
fun `Get remote channel successfully`() = mockGetRemoteChannelSuccessfully { mockItem ->
runBlocking {
val actual = Reader.coRead<RssStandardChannel>(fakeUrl) {
useCache = false
}
actual shouldBe mockItem
}
}
// 略
}
最後,我们就可以使用刚刚写好的 base class 去进行不同种的测试,第一种是在正常地呼叫 read
function ,第二种则是呼叫 flowRead
,最後一种是在 coroutine 上呼叫。
使用 MockK 来写测试是真的很方便,只要专注在测试本身上面就好,不用去管太多 mock 物件怎麽生成,当然这些范例只是我们在 library 中测试的一小部分,如果想要更多测试范例的朋友可以直接去看我们的测试程序码,除了 :processorTest
里面的测试,各 module 里面也有许多的 android test 和 local unit test 可以参考。
小弟因疫情影响,整天在家苦等Offer 於是心血来潮,将自己之前的考题整理成笔记 上来与大家分享交流...
CSS的选择器分为基础选择器以及复合选择器 本日将将继续说明复合选择器 复合选择器可以更准确更高效的...
转换为Genero後,FourJs’ 为了扩展整体程序语言,令他可以执行更多不一定与资料库相关的功能...
k8s上能够流程化的进行布署与管理後,进一步地可以探讨应该如何进行安全性的议题,一部分可以透过rba...
Keyword: Ktor, Suspend Function 到Day11使用Ktor进行网路请求...