本文翻译自Chris Grant的《iOS9 Day-by-Day :: Day 5 :: Xcode Code Coverage Tools》(https://www.shinobicontrols.com/blog/ios9-day-by-day-day5-xcode-code-coverage-tools)。感谢Chris Grant的辛苦工作!
代码覆盖是一个帮助我们衡量单元测试价值的工具。代码覆盖值越高表明测试越完善。如果光有大量的测试样例,但仅仅测试了众多功能中的一个,这样的测试集是没有什么价值的。
对于代码覆盖率并没有一个同一个的标准,而是取决于具体的额项目。例如一个拥有众多不能测试的可视化组件的项目,它的覆盖率肯定比一个数据处理框架要低。
Xcode代码覆盖工具
很早以前,Xcode中就包含了几个方法可以用来生成代码覆盖报告。但是这些方法都比较复杂,并且要手动设置许多东西。从iOS 9开始,苹果将代码覆盖工具直接整合到了Xcode中。这些新的工具与LLVM集成在一起,当表达式执行的时候就会进行记录。
代码覆盖工具的使用
下面我们用一个简单的例子来演示如何使用新的代码覆盖工具以及利用它们来改进测试集。最终的代码在GitHub上可以下载。
首先创建一个新的项目,并确保选中了单元测试(Unit Tests)选项。这样Xcode就会自动创建一个包含单元测试的项目。现在我们需要一个东西用来测试。这里我们增加一个空的Swift文件,并且定义一个全局函数,用来检查两个字符串是否相互倒置。虽然将这个定义为全局函数可能不是最好的设计,但目前位置已经足够了。
func checkWord(word: String, isAnagramOfWord: String) -> Bool {
//移除字符串两端的空白字符
let noWhitespaceOriginalString = word.stringByReplacingOccurrencesOfString(" ", withString: "").lowercaseString
let noWhitespaceComparisonString = isAnagramOfWord.stringByReplacingOccurencesOfString(" ", withString: "").lowercaseString
//如果长度不同,肯定不会互为异构词(通过改变字母顺序而形成的单词)
if noWhitespaceOriginalString.characters.count != noWhitespaceComparisonString.characters.count {
return false
}
//如果字符串相同,肯定互为异构词
if noWhitespaceOriginalString == noWhitespaceComparisonString {
return true
}
//如果为空字符串,默认为真
if noWhitespaceOriginalString.characters.count == 0 {
return true
}
var dict = [Character: Int]()
//遍历原始字符串
for index in 1...noWhiteOriginalString.characters.count {
//查找第i个位置的字母,然后存储
let originalWordIndex = advance(noWhitespaceOriginalString.startIndex, index - 1)
let originalWordCharacter = noWhitespaceOriginalString[originalWordIndex]
//同样查找比较字符串中第i个位置的字符
let comparedWordIndex = advance(noWhitespaceComparisonString.startIndex, index - 1)
let comparedWordCharacter = noWhitespaceComparisonString[comparedWordIndex]
//递增字典中每个字母对应的数目,如果不存在,则设置为0
dict[originalWordCharacter] = (dict[originalWordCharacter] ?? 0) + 1
//同样遍历比较的字符串,只是这次用递减
dict[comparedWordCharacter] = (dict[comparedWordCharacter] ?? 0) - 1
}
//遍历字典,如果有一个key对应的值不为0,则这两个字符串不是异构字符串
for key in dict.keys {
if(dict[key] != 0) {
return false
}
}
//全部为0
return true
}
这个函数相对比较简单,因此我们应该能够100%代码覆盖。
当我们写完算法后,就到了测试时间了。打开由项目自动创建的XCTestCase。添加一个简单的测试样例来判断“1”是“1”的异构字符串。
class CodeCoverageTests: XCTestCase {
func testEqualOneCharacterString() {
XCTAssert(checkWord("1", isAnagramOfWord: "1"))
}
}
在开始测试之前,我们需要确保已经打开代码覆盖功能了。默认情况下,该功能是关闭的,因此需要编辑测试方案(Test Scheme)来启用。
确认“Gather coverage data”选项被勾上,然后点击“Close”并开始测试。不出所料,刚才的测试样例被成功通过。
覆盖标签
一旦测试通过,我们就能够知道checkWord:isAnagramOfWord:
函数至少有一条路径是正确的。但是我们并不知道还有多少中情况是没有被测试到的。这时代码覆盖工具就要发挥作用了。在代码覆盖标签(Code Coverage Tab)中可以按目标(Target)、文件和函数分组查看覆盖率。
在Xcode的左侧打开报告导航标签(Report),然后选中刚才进行的测试,最后点击”Coverage“标签。
从上图可以看出,Xcode显示了一个类、函数的列表来显示每个级别测试覆盖程度。如果鼠标在checkWord
函数上悬停,可以看到我们仅仅覆盖了该类的28%。这简直无法接受!我们需要找出哪条路可行,哪条路不可行。因此需要继续改进这个测试集。双击函数名,Xcode会将代码覆盖统计情况显示在代码的旁边。
白色区域表示代码被执行,并且完全覆盖,灰色区域表示代码还没有执行。就是这些灰色区域,我们需要增加更多的代码进行测试!右侧的数字表示代码执行的次数。
改进
很明显,我们需要比28%更高的代码覆盖率。这里并不需要操作UI,因此很好用来进行单元测试。接下来我们增加更多的测试样例!理想状态下,我们期望到达函数中的所有return
语句。这样就可以得到完整的覆盖!下面是新增的测试:
func testDifferentLengthStrings() {
XCTAssertFalse(checkWord("a", isAnagramOfWord: "bb"))
}
func testEmptyStrings() {
XCTAssert(checkWord("", isAnagramOfWord: ""))
}
func testLongAnagram() {
XCTAssert(checkWord("chris grant", isAnagramOfWord: "char string"))
}
func testLongInvalidAnagramWithEqualLengths() {
XCTAssertFalse(checkWord("apple", isAnagramOfWord: "tests"))
}
这个测试集可以提供完整的代码覆盖。运行测试,然后在代码覆盖标签可以看到最新的测试报告。
可以看到我们终于实现了100%的代码覆盖。现在在回头看代码文件,全部都是白色了,而右边的数字至少都是1。
代码覆盖是判断我们的测试集是否实用的好方法。测试样例并不是越多越好,而是要实现完整覆盖。Xcode 7让代码覆盖检测变得更加简单,因此我们强烈建议在项目中启用Code Coverage功能。如果我们已经有一个测试集了,可以通过代码覆盖检验测试集的完善程度。
更多信息
关于Xcode 7代码覆盖的更多信息,可以查看WWDC session 410(“ Continuous Integration and Code Coverage in Xcode”)。别忘了我们可以在GitHub上下载到本文完整的示例代码。
戴维营教育
戴维营教育(Dive In Education),潜心做IT职业教育!紧跟时代潮流,不弄虚作假!不忘初心!