为什么我要编写快照测试?
测试是任何高质量软件的组成部分。 我相信每个公司或软件发行商都必须先验证产品的质量,然后才能投放市场。 据说,如果质量好,它将吸引更多的用户。 这个事实也适用于软件开发。
我们以手动或自动方式执行UI测试,以确认应用程序的行为正确并具有符合规范的所有功能。
如果我们进行更细化的单元测试,则可以测试单个单元。 我们通过编写肯定的测试(即正确通过测试)和否定的测试(即测试意外数据到来时单元的行为)来涵盖所有极端情况。
编写自动化测试以捕获UI“正确性”实际上是不可能的。 测试UI困难的原因是,UI最小模块的显着细节很难以编程方式表达。 正确性不能由输出的文本部分确定。
我们希望质量保证将重点放在需要人工关注或可以通过自动化完美测试的功能的确切状态下的确切组件上。
但是UI单元测试呢?
好吧,我们为此提供了一个解决方案: 快照测试!
iOSSnapshotTestCase如何捕获快照?
iOSSnapshotTestCase提供FBSnapshotVerifyView(view)方法来将期望与参考快照进行比较。 如果不存在参考快照,它将捕获。 这个方法使用`renderInContext()`来捕获快照,但是`renderInContext()`有一些限制,您不能使用它来测试Visual元素或UIAppearance。 为了测试这些元素,还有另一种方法“ drawViewHierarchyInRect”。 要使用此方法,您需要在测试中设置`useDrawViewHierarchyInRect = true`。
drawViewHierarchyInRect的性能不好,所以我建议仅在确实需要时使用useDrawViewHierarchyInRect = true。
iOSSnapshotTestCase如何比较快照?
有很多比较图像的方法。 我在演讲中得到的第一个答案是逐像素比较图像,这是正确的,但在逐像素比较之前,我们还需要考虑其他方面。
让我们看看它如何进行比较:
- 首先,它比较图像大小。 如果不匹配,则测试失败。
- 如果容差率为零,则使用C函数“ Memcmp()”在内存级别比较图像。 为此,它使用calloc()在内存中分配空间,并使用CGContextRef在内存中绘制图像。
- 如果容差率> 0,即表示您可以接受,即使存在x%的差异,它也会通过对numberOfPixels(即图像宽度*高度)进行迭代来逐像素比较图像。
iOSSnapshotTestCase的功能:
- 它会根据测试类和选择器自动命名磁盘上的参考映像。
- 如果要在单个测试方法中执行多个快照,则需要提供一个可选的“标识符”。
- 使用`isDeviceAgnostic`。 如果您在测试中将此属性设置为true,它将在测试名称中附加设备型号,操作系统编号和大小。
优点:
- 快照测试易于编写。 在执行重构时,您不必担心,就好像只有代码重构一样,测试也不会失败。
- 解耦:视图将松散耦合。
- 易于验证不同尺寸的布局。 我建议以一个模拟器(即iPhone 6)为基础来捕获快照。 如果您使用3倍快照的iPhone 8,则尺寸会很大。 如果您使用的是CI / CD工具,请确保也在那里设置了默认模拟器iPhone 6。
- 增加代码覆盖率:许多人不编写UI单元测试,一旦您开始编写,它将增强您的代码覆盖率报告。
- 更好的PR描述:由于两者松散耦合,因此您可以从View到ViewController开始开发。 如果您到目前为止仅创建了View,那么您也可以打开PR,因为审阅者可以在捕获的快照中可视化视图。
缺点:
- 每个快照大约需要4至100 KB的使用空间,这会占用大量存储库空间。
- 更改一个组件将导致重新记录所有快照测试。 假设您之前在所有视图中使用UILabel。 后来,您将UILabel更改为UITextView,这可能会导致测试失败,因为两个组件的呈现都将不同。
- 有些组件很难测试,例如动画,广告。
我已经在应用程序的主要目标中创建了“ ViewController”和“ BeachView”。
进口基金会
导入UIKit
类BeachView:UIView {
让名称= UILabel()
让细节= UILabel()
让imageView = UIImageView()
覆盖init(frame:CGRect){
super.init(frame:框架)
设定()
}
需要初始化吗?(编码器aDecoder:NSCoder){
fatalError(“创建视图时出现问题”)
}
私人功能setup(){
translationsAutoresizingMaskIntoConstraints = false
addViewsToParent(imageView,名称,详细信息)
setupStyleAttributes()
setupConstraints()
}
私人功能setupStyleAttributes(){
details.numberOfLines = 0
name.addAttributes(with:.black,fontSize:18)
details.addAttributes(with:.black,fontSize:16)
}
私人功能setupConstraints(){
imageView.translatesAutoresizingMaskIntoConstraints = false
imageView.topAnchor.constraint(equalTo:topAnchor).isActive = true
imageView.leadingAnchor.constraint(equalTo:LeadingAnchor).isActive = true
imageView.trailingAnchor.constraint(equalTo:railingAnchor).isActive = true
imageView.heightAnchor.constraint(equalToConstant:250).isActive = true
name.translatesAutoresizingMaskIntoConstraints = false
name.topAnchor.constraint(等于:imageView.bottomAnchor,常数:16).isActive = true
name.leadingAnchor.constraint(equalTo:leadingAnchor).isActive = true
name.trailingAnchor.constraint(equalTo:railingAnchor).isActive = true
details.translatesAutoresizingMaskIntoConstraints = false
details.topAnchor.constraint(equalTo:name.bottomAnchor,常数:16).isActive = true
details.leadingAnchor.constraint(equalTo:LeadingAnchor).isActive = true
details.trailingAnchor.constraint(equalTo:railingAnchor).isActive = true
details.bottomAnchor.constraint(equalTo:bottomAnchor).isActive = true
}
功能更新(与海滩:海滩){
更新(与:beach.imageURL)
name.text =海滩名称
details.text = beach.details
}
私人功能更新(带有url:URL){
做{
让数据=尝试数据(contentsOf:url)
imageView.image = UIImage(数据:数据)
} {
preconditionFailure(“ \(错误)”)
}
}
}
扩展UIView {
func addViewsToParent(_ views:UIView ...){
views.forEach {
addSubview(view)
}
}
}
扩展UILabel {
func addAttributes(颜色:UIColor,fontSize:CGFloat){
textColor =颜色
字体= UIFont.systemFont(ofSize:fontSize)
textAlignment = .center
}
}
这是应用程序在模拟器中的外观:
让我们在应用程序中进行设置以执行快照测试:
对于快照测试,我在应用程序中创建了新目标,即SnapshotTests。
文件->新目标-> iOS单元测试包。
- 在podfile中添加pod`iOSSnapshotTestCase`。
- 将以下环境变量添加到主要目标方案中:
注意:IMAGE_DIFF_DIR是可选的。 您可以将其保留在本地,但不要将其推送到存储库中。 IMAGE_DIFF_DIR将具有3个图像,即一个测试的参考图像,失败图像和比较图像,这些图像最终可能会占用存储库中的大量空间。 因此,将环境变量推送到存储库中没有任何意义。
编写快照测试的要求:
为了编写测试,您将需要视图和数据。 假设您的生产代码中具有以下方法:`update(with:model)`。 最有可能在生产代码中,您将从API或数据库中获取数据。 现在要编写测试,您需要注入数据。
如您在上方看到的,我们有一个模拟器屏幕截图。 从这个设计中,我想为海滩视图编写一个测试,为viewController视图编写另一个测试。
我创建了一个基类来配置快照测试选项:
`AJSnapshotTestCase.swift`
aainaj /快照测试
快照测试–快照测试演示 github.c
现在,我将为“ BeachView”(即“ BeachViewSnapshotTests”)编写快照测试。
导入XCTest
@testable导入SnapshotTesting
class BeachViewSnapshotTests:AJSnapshotTestCase {
让原点= CGPoint(x:0,y:0)
覆盖var recordingMode:布尔{
返回假
}
func testIsShowingCorrectlyOniPhone6Devices(){
makeSnapshot(for:CGRect(origin:origin,size:Device.iPhone6.size()))
}
func testIsShowingCorrectlyOn6Devices_WithDifferentName(){
let frame = CGRect(来源:来源,大小:Device.iPhone6.size())
让testData = Beach.testDataFromBundle(name:“ Aaina”)
makeSnapshot(for:frame,with:testData)
}
私人函数makeSnapshot(适用于框架:CGRect,带有testData:Beach = Beach.testDataFromBundle()){
让containerView = UIView()
containerView.frame =框架
让view = BeachView()
containerView.addSubview(视图)
containerView.backgroundColor = .white
view.translatesAutoresizingMaskIntoConstraints = false
view.leadingAnchor.constraint(equalTo:containerView.leadingAnchor,常数:50).isActive = true
view.trailingAnchor.constraint(equalTo:containerView.trailingAnchor,常数:-50).isActive = true
view.centerYAnchor.constraint(equalTo:containerView.centerYAnchor).isActive = true
view.update(with:testData)
验证(查看:containerView)
}
}
当您运行此测试`testIsShowingCorrectlyOniPhone6Devices`时,它将失败。 您需要将recordMode设置为true才能将快照保存在设备上。 仍然测试会失败,因为之前没有快照。 切换recordMode并运行测试,这次比较将完成并且测试将通过。
现在,我将进行一些重构。 我想修改注入数据的名称。 如果您发现我已经为其创建了漂亮的测试数据,则可以在任何需要的地方重复使用。 我还有其他选择,可以通过创建本地JSON或伪数据将数据注入视图。 但是,这两者都不适合任何架构,最终会导致很多重复。
在运行测试时注入修改后的数据后,它们将失败,并且可以在“ Failure_Diff”目录中看到失败的区别。
如果要集成快照测试,请记住以下几点:
- 体系结构:您可以捕获ViewController的快照,但是很难确定实际的测试失败原因。 我建议创建小的视图,您也可以完美地测试和重用它们。
- 异步性:执行快照测试时不能执行异步操作,因为超时会发生。 如果要从Web服务器异步加载图像,则在快照测试中可能看不到图像。 为此,您可以从包中加载图像,也可以从该URL进行建模。 您可以在示例代码中查看我的“ Beach + TestData.swift”。
在哪里可以找到失败和捕获的快照?
- 可以在指定目录中轻松找到快照。
- 查看失败图像和参考图像之间差异的快速方法是源树。
参考文献:
https://medium.com/@gbasile/ficient-test-data-creation-in-swift-822fc14fef27
示例代码— Github
结论:
我发现这是编写UI单元测试的好方法。 当您在大型团队中工作时,它会很有帮助。 我建议您也遵循同样的方法。 在将此集成到项目中之前,您可以对此进行POC。 我已在“参考”中附加了我的示例代码链接。 我已经在这里以编程方式创建了视图。 您也可以通过xib或情节提要加载视图以执行快照测试。
如果您有任何疑问,请告诉我。
您可以随时通过Linkedin或Twitter与我联系