Parser 的单元测试

这篇会讲解怎麽直接用 jUnit 来测试 parser 和 Android 环境的 parser ,接续上一篇,我们现在已经准备好了 RSS feed 的测试案例和 XML 档案,我们要想办法去取得 XML 的档案内容。我们可以把读取档案内容放在一个 util class 里面,这个 util 我们就放在一个 :testCommon 的 module 里,方便我们之後在任何一个 module 底下做测试。

object XmlFileReader {
    fun readFile(filename: String): String {
        val stringBuilder = StringBuilder()
        this::class.java.classLoader?.getResourceAsStream(filename)?.use { inputStream ->
            var availableCount = inputStream.available()
            while (availableCount > 0) {
                val string = inputStream.readByteString(availableCount).string(Charsets.UTF_8)
                stringBuilder.append(string)

                availableCount = inputStream.available()
            }
        }

        return stringBuilder.toString()
    }
}

用法就是呼叫 XmlFileReader.readFile 後面接完整路径的档名就可以了!实作部分主要是把档案透过 inputStream 的方式一段段读出放在 StringBuilder 里,当档案读完後就把 StringBuilder 的内容转成一般的字串。

拿到 XML 档案内容後,我们就可以来写测试了!要测试一个 parser ,其中的测项包含很多个 case ,但每个测试项目的程序码都是:

@Test
fun parse() {
    val xml = XmlFileReader.readFile(rssFilePath)
    val actualChannel = TestRssDataParser.parse(xml)

    actualChannel shouldBe expectedChannel
}

infix fun Any?.shouldBe(expected: Any?) = Assert.assertEquals(expected, this)

假设我们对这个 TestRssDataParser 有 30 几个测项,我们是不是就要写 30 几个这种 test function ?其实不用,我们可以使用 jUnit 的参数化测试,只要我们把测试项目的输入变数参数化,再验证测试的输出就可以了,不用写 30 几个 test function ,这是一个很方便的功能,因为很多时候在写测试的时候,都是输入不同,但是测试的流程是差不多的,这时候参数化测试的帮助就很大。

class CustomParserLocalTest {

    @RunWith(Parameterized::class)
    class CustomParserParseFunctionTest(
        private val rssFilePath: String,
        private val expectedChannel: TestRssData?
    ) {
        companion object {
            @JvmStatic
            @Parameterized.Parameters
            fun getTestingData() = listOf(
                arrayOf("${RSS_FOLDER}/rss_v2_full.xml", TestData.RSS_DATA),
                arrayOf("${RSS_FOLDER}/rss_v2_has_non_channel_attrs.xml", TestData.RSS_DATA),
                arrayOf("${RSS_FOLDER}/rss_v2_has_non_channel_attrs_follow_behind.xml", TestData.RSS_DATA),
                arrayOf("${RSS_FOLDER}/rss_v2_has_non_item_attrs.xml", TestData.RSS_DATA),
                arrayOf("${RSS_FOLDER}/rss_v2_has_non_item_attrs_follow_behind.xml", TestData.RSS_DATA),
            )
        }

        @Test
        fun parse() {
            val xml = XmlFileReader.readFile(rssFilePath)
            val actualChannel = TestRssDataParser.parse(xml)

            actualChannel shouldBe expectedChannel
        }
    }
}

在写参数化测试的时候,要在 class 的前面使用 annotation 标注 @RunWith(Parameterized::class) ,jUnit 就会知道接下来要进行参数化测试。这个类别有两个输入的参数 rssFilePathexpectedChannelrssFilePath 为 模拟 RSS feed 的测试案例 XML , expectedChannel 则为该测项的预估结果,若测试的结果不等於 expectedChannel ,则该测试失败。在 getTestingData 回传的列表是一组组的测试参数,也就是该测项的 XML 档案和预测结果,分别对应到 rssFilePathexpectedChannel ,而下面的 parse function 是每个测项的测试流程。

在 Android 的 instrumental 测试,其实也是类似的参数化测试方式,只是测试本身是跑在 Android 的装置上面,而不是跑在电脑的 JVM 上,因为 Android 的 parser 有用到那些 Android 相关的 dependency ,例如 XmlPullParser ,所以才会需要跑在 Android 的实机上。以下是 Android instrumental test 的范例程序码:

@RunWith(Enclosed::class)
class AndroidITunesParserTest {

    @RunWith(Parameterized::class)
    class ITunesParserParseFunctionTest(
        private val rssFilePath: String,
        private val expectedChannel: ITunesChannelData
    ) {
        companion object {
            @JvmStatic
            @Parameterized.Parameters
            fun getTestingData() =listOf(
arrayOf("${ITUNES_FOLDER}/itunes_rss_v2_full.xml",FULL_ITUNES_CHANNEL),
arrayOf("${ITUNES_FOLDER}/itunes_rss_v2_some_channel_attrs_missing.xml",PARTIAL_ITUNES_CHANNEL),
arrayOf("${ITUNES_FOLDER}/itunes_rss_v2_without_itunes_attributes.xml",PARTIAL_ITUNES_CHANNEL_2),
            )
        }

        private val parser: AndroidITunesParser = AndroidITunesParser()

        @Test
        fun parse() {
            val xml = XmlFileReader.readFile(rssFilePath)
            val actualChannel = parser.parse(xml)

            actualChannelshouldBeexpectedChannel
        }
    }

    @RunWith(Parameterized::class)
    class ITunesErrorTagParserErrorTagTest(private val rssFilePath: String): ErrorTagParserBaseTest() {

        @Test(expected = XmlPullParserException::class)
        fun parse() {
            val parser = AndroidITunesParser()
            val xml = XmlFileReader.readFile(rssFilePath)

            parser.parse(xml)
        }
    }
}

测试 Parser 虽然不难,但要把测试整理的很好,则需要花一些工夫。我觉得在写类似的测试时,有两个重点:

  1. 重复流程的测试项目,但输入参数不同,可以整理成参数化的测试。
  2. 多个 module 的测试中有类似的测试案例和方法,建议抽出来成为单独的测试 library module ,避免写重复的程序码,像是 KtRssReader 里面的 :testCommon module 。

<<:  Day 14 讯息伫列

>>:  Day-17 中位数与顺序统计

2.4.15 Design System - Tabs

所有的安排都不一定会照着计画走 比如说以前的旅行可能会像是踩点一样 安排好很多个景点 在有限时间底...

Day 30 我完成铁人了!

心得 哈罗大家,今天是铁人挑战第三十天,终於来到最後一天了,老实说我真的没想过我会参加这个比赛,更没...

Day 17:上架 Google Play

前言 当我们终於准备上架 Google Play,我们需要决定在哪些国家、年龄层、装置等才能够看到我...

Leetcode 挑战 Day 07 [118. Pascal's Triangle]

118. Pascal's Triangle 今天要挑战的是实作一个在数学上有许多应用的帕斯卡三角形...

参加 VMware 1V0-21.20 认证考试以获得成功的职业生涯

您是 VMware 认证技术助理 - 数据中心虚拟化考试的有抱负的候选人之一吗?然後你就中了头奖!多...