這篇會講解怎麼直接用 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 就會知道接下來要進行參數化測試。這個類別有兩個輸入的參數 rssFilePath
和 expectedChannel
, rssFilePath
為 模擬 RSS feed 的測試案例 XML , expectedChannel
則為該測項的預估結果,若測試的結果不等於 expectedChannel
,則該測試失敗。在 getTestingData
回傳的列表是一組組的測試參數,也就是該測項的 XML 檔案和預測結果,分別對應到 rssFilePath
和 expectedChannel
,而下面的 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 雖然不難,但要把測試整理的很好,則需要花一些工夫。我覺得在寫類似的測試時,有兩個重點:
:testCommon
module 。