iOSでSberbank Onlineをテストする方法









前回の記事では、テストピラミッドと自動テストの利点について説明しました。 しかし、理論は通常、実践とは異なります。 今日は、数百万人のiOSユーザーが使用するアプリケーションコードのテストにおける経験についてお話したいと思います。 また、安定したコードを達成するために私たちのチームがしなければならなかった困難な道についても。



状況は次のとおりです。開発者が、コードベースをテストでカバーする必要があることを自分やビジネスに納得させることができたとします。 時間が経つにつれて、プロジェクトは数十万件以上のユニットテストと1,000件以上のUIテストになりました。 このような大きなテストベースは、いくつかの問題を引き起こしました。その解決策を教えてください。



記事の最初の部分では、クリーンな(統合されていない)単体テストで作業する際に生じる困難に精通し、2番目の部分では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
      
      





原則として、これは転倒を再現して次のステップに進むのに十分です-記録された転倒の原因を特定します。



秋の理由と可能な解決策



作業中に発生する可能性がある単体テストの点滅の主な原因、それらを特定するツール、および考えられる解決策を検討してください。



テストが失敗する理由には、主に3つのグループがあります。



断熱不良



分離とは、カプセル化の特殊なケース、つまり、一部のプログラムコンポーネントのアクセスを他のコンポーネントに制限できる言語メカニズムを意味します。



環境の分離は重要な役割を果たします。テストの純度のために、テスト対象のエンティティに影響を与えるものは何もないからです。 コードのチェックを目的としたテストには特に注意を払う必要があります。 グローバル変数、キーチェーン、ネットワーク、CoreData、シングルトン、NSUserDefaultsなどのグローバル状態エンティティを使用します。 これらの分野では、孤立が不十分であることを示す可能性のある場所の最大数が発生します。 テスト環境を作成するときに、グローバル状態が設定され、それが別のテストコードで暗黙的に使用されるとします。 この場合、テスト中のコードをチェックするテストが「点滅」し始めることがあります。テストのシーケンスによっては、グローバルステートが設定されている場合と設定されていない場合の2つの状況が発生するためです。 多くの場合、説明されている依存関係は暗黙的であるため、このようなグローバル状態の設定/リセットを誤って忘れることがあります。



依存関係が明確に見えるように、依存性注入(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の記事を参照してください



制御できないコードのテスト

多くの場合、次のことを忘れます。











上記のケースでは、テストの作成および実行時に不確実性が生じます。 マイナスの結果を避けるために、アプリケーションでサポートされているiOSのバージョンだけでなく、すべてのロケールでテストを実行する必要があります。 別途、実装が隠されているコードをテストする必要がないことに注意してください。



これで、ユニットテスト専用のSberbank Online iOSアプリケーションの自動テストに関する記事の最初の部分を完了したいと思います。



記事の第2部では、1500のUIテストの作成中に発生した問題と、それらを克服するためのレシピについて説明します。



この記事は、 regno -Anton Vlasov、開発責任者およびiOS開発者で書かれました。



All Articles