在
上一篇文章中,我们熟悉了测试金字塔和自动化测试的好处。 但是理论通常与实践不同。 今天,我们想谈谈我们在测试数百万iOS用户使用的
应用程序代码方面的经验。 还有关于我们的团队要获得稳定的代码所必须走的艰难道路。
情况是这样的:假设开发人员设法说服自己和企业相信需要用测试覆盖代码库。 随着时间的流逝,该项目已成为一万多个单元测试和一千多个UI测试。 如此庞大的测试基础导致了一些问题,我们要说出解决方案。
在本文的第一部分中,我们将了解使用干净的(非集成)单元测试时出现的困难,在第二部分中,我们将考虑UI测试。 要了解我们如何提高测试运行的稳定性,欢迎来到Cat。
在理想的情况下,无论源代码的数量和启动顺序如何,单元测试都应始终使用相同的源代码来显示相同的结果。 并且不断下降的测试不应通过持续集成服务器(CI)的障碍。
实际上,可能会遇到这样一个事实,即同一单元测试将显示阳性或阴性结果-这意味着“闪烁”。 出现这种情况的原因在于测试代码的实现不佳。 而且,这样的测试可以成功通过CI,然后以后它将开始落在其他人的请求(PR)上。 在类似情况下,希望禁用此测试或玩轮盘赌并再次运行CI运行。 但是,这种方法不利于生产,因为它破坏了测试的可信度,并给CI带来了毫无意义的工作。
这个问题在今年的苹果WWDC国际会议上得到了强调:
- 本节讨论并行测试,使用测试分析单个目标代码的覆盖率以及启动测试的顺序。
- 苹果在这里谈论了测试网络请求,黑客攻击,测试通知和测试速度。
单元测试
为了应对闪烁测试,我们使用以下操作序列:

0.我们根据基本标准评估质量测试代码:隔离,moka的正确性等。 我们遵循以下规则:闪烁的测试会更改测试代码,而不是测试代码。
如果该项目没有帮助,请按照下列步骤操作:
1.我们确定并复制了测试通过的条件;
2.找出跌倒的原因;
3.更改测试代码或测试代码;
4.转到第一步,检查跌倒原因是否已消除。
玩秋天
最简单,最明显的选择是在同一版本的iOS和同一设备上运行问题测试。 通常,在这种情况下,测试是成功的,并且出现以下提示:“一切都对我本地有效,我将在CI上重新启动组装”。 实际上,这只是问题尚未解决,并且测试继续在其他人的支持下进行。
因此,在下一个验证步骤中,您需要在本地运行应用程序的所有单元测试,以识别一个测试对另一个测试的潜在影响。 但是即使经过这样的验证,您的测试结果也可能是肯定的,但是问题仍然未被发现。
如果整个测试序列成功完成,并且未记录到预期的下降,则可以重复多次运行。
为此,在命令行上,您需要使用xcodebuild运行循环:
#! /bin/sh x=0 while [ $x -le 100 ]; do xcodebuild -configuration Debug -scheme "TargetScheme" -workspace App.wcworkspace -sdk iphonesimulator -destination "platfrom=iOS Simulator, OS=11.3, name=iPhone 7" test >> "report.txt"; x=$(( $x +1 )); done
通常,这足以重现跌倒并继续进行下一步-确定记录下来的跌倒原因。
跌倒的原因和可能的解决方案
考虑一下您在工作中可能会遇到的单元测试闪烁的主要原因,识别它们的工具以及可能的解决方案。
导致测试失败的原因主要分为三类:
绝缘不良隔离是指封装的一种特殊情况,即:一种语言机制,它允许限制某些程序组件对其他程序组件的访问。
环境的隔离起着重要的作用,因为对于测试的纯度而言,没有任何事情会影响被测试的实体。 应特别注意旨在检查代码的测试。 他们使用全局状态实体,例如全局变量,钥匙串,网络,CoreData,Singleton,NSUserDefaults等。 正是在这些地区,出现了表现出隔离不良的最大潜在场所。 假设在创建测试环境时,设置了全局状态,该状态在另一个测试代码中隐式使用。 在这种情况下,检查被测代码的测试可能会开始“闪烁”-因为根据测试的顺序,可能会出现两种情况-设置全局状态和不设置全局状态。 通常,所描述的依赖项是隐式的,因此您可能会意外地忘记设置/重置此类全局状态。
为了使依赖关系清晰可见,可以使用依赖关系注入(DI)原理,即:通过构造函数参数或对象的属性传递依赖关系。 这将使替换模拟依赖项而不是真实对象变得容易。
呼叫异步所有单元测试均同步执行。 异步测试的难度之所以增加,是因为预期单元测试范围的完成会使测试中测试方法的调用“冻结”。 结果将是测试的稳定下降。
要测试这种测试,有几种方法:
- 运行NSRunLoop
- waitForExpectationsWithTimeout
这两个选项都要求您指定一个带有超时的参数。 但是,不能保证所选的间隔足够。 在本地,您的测试将通过,但是在CI负载较重的情况下,可能没有足够的电源,它将掉落-从此处将出现“闪烁”。
让我们提供某种数据处理服务。 我们要验证的是,在收到服务器的响应后,它将传输此数据以进行进一步处理。
要通过网络发送请求,该服务将使用客户端进行处理。
可以使用模拟服务器异步编写这样的测试,以确保稳定的网络响应。
@interface Service : NSObject @property (nonatomic, strong) id<APIClient> apiClient; @end @protocol APIClient <NSObject> - (void)getDataWithCompletion:(void (^)(id responseJSONData))completion; @end - (void)testRequestAsync {
但是测试的同步版本将更加稳定,使您摆脱使用超时的麻烦。
对他来说,我们需要一个同步模拟APIClient
@interface APIClientMock : NSObject <APIClient> @end @implementation - (void)getDataWithCompletion:(void (^)(id responseJSONData))completion { __auto_type fakeData = @{ @"key" : @"value" }; if (completion != nil) { completion(fakeData); } } @end
这样测试将看起来更简单且工作更稳定
- (void)testRequestSync {
可以通过封装单独的实体来隔离异步操作,该实体可以独立进行测试。 其余逻辑需要同步测试。 这种方法将避免异步带来的大多数陷阱。
另外,在从后台线程更新UI层的情况下,您可以检查看看我们是否在主线程中,以及从测试中进行调用会发生什么情况:
func performUIUpdate(using closure: @escaping () -> Void) {
有关详细说明,请参阅
D. Sandell的文章 。
测试代码超出您的控制通常我们会忘记以下几件事:
- 方法的实现可能取决于应用程序的本地化,
- SDK中有一些私有方法可以由框架类调用,
- 方法的实现可能取决于SDK版本

上述情况在编写和运行测试时会带来不确定性。 为避免负面影响,您需要在所有语言环境以及应用程序支持的iOS版本上运行测试。 另外,应注意,无需测试对您隐藏了其实现的代码。
这样,我们想完成有关自动测试Sberbank Online iOS应用程序(专门用于单元测试)的文章的第一部分。
在本文的第二部分中,我们将讨论编写1500个UI测试时出现的问题,以及克服这些问题的方法。
本文由
regno撰写-开发主管兼iOS开发人员Anton Vlasov。