使用 Slather 產生 iOS/Mac 專案的單元測試覆蓋率報告

在 Xcode 6 以及之前的版本中,如果你使用蘋果內建在 Xcode 當中的 OCUnit/XCUnit 等測試框架撰寫單元測試的話,Xcode 會在測試的過程中產生 gcc 格式的覆蓋率(coverage)報告,叫做 gcda (意思是 gcov data file)檔案;如果我們想將產生測試覆蓋率的流程整合到 Jenkins 等持續整合系統中,我們可以用 gonvr 等工具,將測試報告轉換成 XML 或 HTML 格式,這將我們就可以在系統中,看到每個 build 的覆蓋圖表,以及覆蓋率變化的趨勢。

但是在 Xcode 7 之後,蘋果將 gcc 的工具換成 llvm 的工具,這種幾年前的方案便變得不敷使用。在 Xcode 7 中,雖然我們可以指定要求產生 gcc 格式的覆蓋率檔案,似乎可以與我們過去的工作流程相容,但是產生出來的結果非常不準確;而蘋果的立場大概是,反正 Xcode Server 可以正確顯示覆蓋率報告,如果你用的不是 Xcoder Server,他們就不管了。而現在要將 Xcode 7 中 llvm 工具產生出來的報告,轉換成 XML/HTML 等格式,我們大概會選擇 Slather

Slather 是一套用 Ruby 開發的 command line 工具,是 llvm-cov 工具的前端,我們可以使用 gem install slather 指令安裝。

基本使用

要使用 Slather 產生各種格式的報告,基本流程是,只要先在 command line 底下,先用 xcodebuild 命令執行過 Xcode 裡頭的單元測試,再執行一次 slather 即可。

Xcodebuild

比方說,你有一個叫做 MyProject.xcodeproj 的 Xcode Project 檔案,裡頭有個 Target 叫做 MyProject,同時有個對應到這個 Target 的 Scheme,也叫做 MyProject,你大概會下這樣的指令:

xcodebuild -configuration Debug -project MyProject.xcodeproj -scheme MyProject -enableCodeCoverage YES -destination 'platform=iOS Simulator,name=iPhone 7'  test

在這邊要注意幾件事情:
1. 我們特別把 enableCodeCoverage 指定成 YES,代表我們強制要求產生覆蓋率報告。雖然這個設定也可以在 Xcode 的 GUI 環境裡頭做,但也可以用 command line 參數指定。
2. 以前我們大概會指定 target 與 arch 參數,指定要編譯哪個 target,以及產生哪個平台(sdk 參數,如 iphoneos/iphonesimulator)與 CPU(arch 參數,像 x86_64/i386/arm 等)的 binary,不過,現在我們會選擇使用 scheme/destination 參數,主要原因是,這年頭在編譯一個 iOS app 的時候,裡頭不見得只有 iOS 的 binary—如果你 iOS app 同時支援 Apple Watch,那麼 Apple Watch 使用的 watch app 與 watch app extension 就要用 watchos SDK 編譯,你將 sdk 參數設定為 iphoneos 編譯到需要 watchos SDK 的部份,就會編譯失敗。

如果要跟 Jenkins 整合,我們通常還會把 Xcode 的測試結果,轉換成 JUnit 的格式,再匯入 Cobertura Plugin 中,這時候就會用上 ocunit2junit。指令就會像這樣:

xcodebuild -configuration Debug -project MyProject.xcodeproj -scheme MyProject -enableCodeCoverage YES -destination 'platform=iOS Simulator,name=iPhone 7'  test 2>&1 > test_log.txt 
cat test_log.txt | ocunit2junit

現在就可以使用 slather 產生報告了,如果是 Cobertura 的 XML 格式,我們可以下

slather coverage  -x --output-directory reports --scheme MyProject MyProject.xcodeproj

如果要的是 HTML 格式的報告

slather coverage  -x --output-directory reports --scheme MyProject MyProject.xcodeproj

在 Jenkins 中,可以使用 HTML Publisher Plugin 發佈 HTML 格式的測試報告。

使用 Slather 可能會遇到的問題

Slather 本身的文件相當簡略,看起來並不困難,但實際用起來,之前一直遇到 Slather 回報找不到覆蓋率報告,或是無法解讀覆蓋率檔案的問題,我們花了好幾個星期,才把所有專案都換成使用 Slather 產生報告。當你遇到 Slather 說找不到覆蓋率報告,除了可能是你沒有指定 enableCodeCoverage 參數外,還有幾個可能:

有時 Slather 會找不到 Test Bundle 的位置

有的時候你的 Scheme 的名稱,與你用來作測試的 Target 的名字不見得是一樣的。像是,在下圖中,我們建了一個叫做 JustAnotherTest 的 Scheme,但是這個 Scheme 中所使用的 Target 叫做 AnotherTest,編譯出來的 XCUnit bundle 叫做 AnotherTest.xctest,名字不同的時候,Slather 就可能會跳出 No coverage files found 錯誤。

這種狀況下,我們就得要自己指定去哪裡讀取覆蓋率檔案。我們可以加上 —binary-basename AnotherTest.xctest,告訴 Slather 叫去找 AnotherTest.xctest 的覆蓋率。

Slather 不支援 Universal Binary

如果你的單元測試 Target 把「Build Active Architecture」(也就是 ONLY_ACTIVE_ARCH)設成 NO,那麼,你可能會產生同時支援多種 Architecture 的 binary,像是同時編了 x86_64/i386 的 binary。Slather 如果遇到這種檔案,就會無法正確產生報告,原因是 llvm-cov 就無法處理這種檔案,所以我們必須要在 Xcode 中關閉這項設定。

可以參見 Slather 討論串

排除我們不想計算的部份

如果直接使用前面的指令,可以發現,Slather 會把一些我們不想列入計算的部份也都算在覆蓋率裡頭,像是我們呼叫到的蘋果本身的 Framework(UIKit、Foundation 等),還有我們自己用到的第三方 Framework/Library。這時候我們就可以用 –ignore 指令排除這些部分,如果有多項要排除,就要寫多次 –ignore。像是

slather coverage --ignore '../*' --ignore 'ExternalLibraries/*'  -x --output-directory reports --scheme MyProject MyProject.xcodeproj

「../* 」代表的是在我們專案目錄以外的東西,也就是排除掉系統本身的 Framework,而我們把第三方元件放在 ExternalLibraries 的話,也在這邊一起排除了。

This work is licensed under a Creative Commons Attribution-NonCommercial-NoDerivatives 4.0 International License.

About zonble

iOS Developer at KKBOX.

This entry was posted in Code, iOS, Mac and tagged , , , , , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *