我们如何在iOS上测试Sberbank Online



上一篇文章中,我们熟悉了测试金字塔和自动化测试的好处。 但是理论通常与实践不同。 今天,我们想谈谈我们在测试数百万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)原理,即:通过构造函数参数或对象的属性传递依赖关系。 这将使替换模拟依赖项而不是真实对象变得容易。

呼叫异步

所有单元测试均同步执行。 异步测试的难度之所以增加,是因为预期单元测试范围的完成会使测试中测试方法的调用“冻结”。 结果将是测试的稳定下降。

 //act [self.testService loadImageFromUrl:@"www.google.ru" handler:^(UIImage * _Nullable image, NSError * _Nullable error) { //assert OCMVerify([cacheMock imageAtPath:OCMOCK_ANY]); OCMVerify([cacheMock dateOfFileAtPath:OCMOCK_ANY]); OCMVerify([imageMock new]); [imageMock stopMocking]; }]; [self waitInterval:0.2]; 

要测试这种测试,有几种方法:

  1. 运行NSRunLoop
  2. waitForExpectationsWithTimeout

这两个选项都要求您指定一个带有超时的参数。 但是,不能保证所选的间隔足够。 在本地,您的测试将通过,但是在CI负载较重的情况下,可能没有足够的电源,它将掉落-从此处将出现“闪烁”。

让我们提供某种数据处理服务。 我们要验证的是,在收到服务器的响应后,它将传输此数据以进行进一步处理。

要通过网络发送请求,该服务将使用客户端进行处理。

可以使用模拟服务器异步编写这样的测试,以确保稳定的网络响应。

 @interface Service : NSObject @property (nonatomic, strong) id<APIClient> apiClient; @end @protocol APIClient <NSObject> - (void)getDataWithCompletion:(void (^)(id responseJSONData))completion; @end - (void)testRequestAsync { // arrange __auto_type service = [Service new]; service.apiClient = [APIClient new]; XCTestExpectation *expectation = [self expectationWithDescription:@"Request"]; // act id receivedData = nil; [self.service receiveDataWithCompletion:^(id responseJSONData) { receivedData = responseJSONData; [expectation fulfill]; }]; [self waitForExpectationsWithTimeout:10 handler:^(NSError * _Nullable error) { expect(receivedData).notTo.beNil(); expect(error).to.beNil(); }]; } 

但是测试的同步版本将更加稳定,使您摆脱使用超时的麻烦。

对他来说,我们需要一个同步模拟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 { // arrange __auto_type service = [Service new]; service.apiClient = [APIClientMock new]; // act id receivedData = nil; [self.service receiveDataWithCompletion:^(id responseJSONData) { receivedData = responseJSONData; }]; expect(receivedData).notTo.beNil(); expect(error).to.beNil(); } 

可以通过封装单独的实体来隔离异步操作,该实体可以独立进行测试。 其余逻辑需要同步测试。 这种方法将避免异步带来的大多数陷阱。

另外,在从后台线程更新UI层的情况下,您可以检查看看我们是否在主线程中,以及从测试中进行调用会发生什么情况:

 func performUIUpdate(using closure: @escaping () -> Void) { // If we are already on the main thread, execute the closure directly if Thread.isMainThread { closure() } else { DispatchQueue.main.async(execute: closure) } } 

有关详细说明,请参阅D. Sandell的文章

测试代码超出您的控制
通常我们会忘记以下几件事:

  • 方法的实现可能取决于应用程序的本地化,
  • SDK中有一些私有方法可以由框架类调用,
  • 方法的实现可能取决于SDK版本


上述情况在编写和运行测试时会带来不确定性。 为避免负面影响,您需要在所有语言环境以及应用程序支持的iOS版本上运行测试。 另外,应注意,无需测试对您隐藏了其实现的代码。

这样,我们想完成有关自动测试Sberbank Online iOS应用程序(专门用于单元测试)的文章的第一部分。

在本文的第二部分中,我们将讨论编写1500个UI测试时出现的问题,以及克服这些问题的方法。

本文由regno撰写-开发主管兼iOS开发人员Anton Vlasov。

Source: https://habr.com/ru/post/zh-CN434320/


All Articles