iT邦幫忙

2021 iThome 鐵人賽

DAY 23
0
Mobile Development

如何使用 Kotlin Annotation Processor 做出自己的 Custom Data Parser Library系列 第 23

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 。

上一篇
規劃 Parser 的測試
下一篇
使用 MockK 做測試
系列文
如何使用 Kotlin Annotation Processor 做出自己的 Custom Data Parser Library30

尚未有邦友留言

立即登入留言