ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
テストのヒントとコツ
記述したコードが正しく動作するかどうかを検証する上でテストは欠かせませんが、コードにどうしても依存関係が含まれる場合があります。このセッションでは、そうしたテストしにくいコードでも、XCTestを使ってAppleプラットフォーム上でテストできるようにするテクニックについて紹介します。また、実行速度が高速でメンテナンスも少なくて済む、高品質なテストを記述するためのさまざまなヒントも紹介します。
リソース
関連ビデオ
WWDC22
WWDC20
-
ダウンロード
(音楽)
(拍手) こんにちは 当セッションへようこそ ブライアンです 同僚のスチュアートと私が テストのテクニックを ご紹介します
WWDC開催にあたり 会場周辺の 見どころを検索できる― アプリケーションがあったら 便利だと我々は考えました このアプリケーションで 周辺のスポットを探すと 現在地からの距離が出ます
もちろん テストスイートが必要です 開発中のどの時点であっても コードが正しく動くと― 確認するためです
テストを記述する際に 役立つテクニックを 4つ ご紹介します
ネットワークコードのテスト
Foundation 通知オブジェクトのテスト
プロトコルで モックを作成するテスト テストスピードを 高速で維持する方法です
最初はネットワークです
動的コンテンツ更新のため リモートウェブサーバから データを読み込ませます そこでネットワークコードの テストのコツです
まず昨年を振り返りましょう WWDC 2017の “Engineering for Testability” 徹底度と理解性 実行速度の バランスを保った― テストスイート作成を ピラミッドで説明しました
理想的なテストスイートは ユニットテストを中心とし 各クラスやメソッドを 対象とします
その特徴は 読みやすく― 不合格時に はっきり表示することです また実行速度が速く― 毎分 何千ものテストを 実行します
それを補完するのが インテグレーションテストです 個別サブシステムや クラスクラスタを対象とし それらの連携を 数秒以内に確認します
最上部はエンドツーエンドの システムテストです 多くは UIテストの形を取り― エンドユーザと同じように デバイス上で動かします 各部の接続状況と― OSや外部リソースとの 相互作用を確認します
このモデルの テストスイートで― コードベースが全体で いかに機能するか分かります
ネットワークスタックのテストに このモデルを使い テストスイート作成の 指針としました
これがネットワークリクエストと データフィード時の― 上位レベルのデータフローです
初期のプロトタイプでは View Controllerの 1メソッドで行っていました このようにです
ユーザの位置で パラメータを取ります
位置をクエリパラメータとし サービスAPIエンドポイントの URLを生成
そしてFoundationの URLSessionを使い URLのGETリクエストのため データタスクを作成
サーバが応答して データをアンラップ
FoundationのJSONDecoderで デコードし PointOfInterest値の配列へ これは他で宣言した構造体で プロトコルに準拠します
それをプロパティに保存し テーブルビューで表示
たった15行のコードで 以上のことができました SwiftとFoundationの おかげです
しかし 1つのメソッドにすると― コードのテストしやすさと 保守性が問題になります
ピラミッドの最下部を見ると 各フローのユニットテストを 記述することが重要です
まずリクエスト準備と 応答の解析についてです
テストしやすいコードに するために― View Controllerから 取り出して
PointsOfInterestRequest型に 2つのメソッドを作成 2つの分離メソッドが それぞれ 値を入力として 副作用なしで 出力値に変換するのです
コードのユニットテストを 記述しやすくなります
makeRequestメソッドの テストでは サンプルを作って メソッドに渡し 戻り値について アサーションを作成
応答の解析のテストでも モックJSONを渡し― 解析結果について アサーションを作成
もう1つ 注目すべき点があります throwsのテストメソッドに XCTestサポートを活用し do-catchブロックは不要で tryを使えるのです
次はURLSessionとの関係です
再びView Controllerから 取り出し シグネチャが合うメソッドと APIRequestプロトコルを作成 使用している APIRequestLoaderクラスは リクエスト型とURLSessionで 初期化されています
ここの loadAPIRequestメソッドは APIRequest値で URLリクエストを作成 URLSessionにフィードし APIRequestで応答を解析
ユニットテストの記述を 続けられますが― ピラミッドの上部へ移ります データフローをカバーする インテグレーションテストです
テストスイートの この層でテストしたいのは URLSessionとの連携です
Foundationの URLローディングシステムが フックを提供しています
ネットワークリクエストに使う 上位レベルのAPIを提供し インフライト要求を代表する URLSessionDataTaskを生成します 背後には下位レベルAPIの URLProtocolがあります ネットワーク接続をオープンし リクエストを記述し 応答をリードバックします
URLProtocolは サブクラス化され URLローディングシステムの 拡張性を高めます
FoundationはHTTPSのような ビルトインプロトコルを提供 テストではモックプロトコルで オーバーライドできます リクエストについての アサーションを作成し モック応答を返せます
URLProtocolはプログレスを URLProtocolClientを通じ システムに伝達
このように使えます テストバンドルに MockURLProtocolを作成 canInitリクエストを オーバーライドし あらゆるリクエストに 興味があると示します
canonicalRequestを 実装しますが startLoadingと stopLoadingも実装
このURLProtocolに フックするため― requestHandlerを テストに提供します
URLSessionタスクが始まると URLProtocolサブクラスを インスタンス化 URLRequest値とURLProtocol クライアントインスタンスを提供
そして startLoadingを呼び出し テストサブセットに requestHandlerを提供 URLRequestをパラメータとし 呼び出します
戻ってきたものを システムに渡します URLレスポンスと データの場合も エラーの場合もあります
テストリクエストの キャンセルは stopLoadingの実装と 同様です
スタブプロトコルがあれば テストを記述できます
APIRequestLoaderを作成 構成内容は リクエスト型と― URLProtocolのため 設定されたURLSessionです
テストベースにrequestHandlerを MockURLProtocolで設定 リクエストについて アサーションを作成し スタブ応答を提供します
そしてloadAPIRequestを 呼び出します 完了ブロックの 呼び出しを待ち 解析した応答について アサーションを作成
この層のテストの実行により コードの連携と― システムとの統合が 確認できます データタスクで レジュームを呼び忘れると このテストは 不合格となります 皆さんも経験があるでしょう
最後に エンドツーエンドの システムテストが重要です UIテストが効果的でしょう
UIテストについては WWDC 2015の“UI Testing in Xcode”をご参照ください
エンドツーエンドのテストを 記述する際に問題があります 不合格となった場合に― どこから原因を探るかと いうことです
我々は この問題への対策として― モックサーバの ローカルインスタンスを設定 UIテストを中断し モックに リクエストさせました データをコントロールでき UIテストの信頼性が 高まりました
この点でモックサーバは 役に立ちますが― 本物のサーバに リクエストするテストも有益です
そこでユニットテストバンドルで テストをします アプリケーションに対し 直接 呼び出すテストで 本物のサーバに直接 リクエストさせるのです するとアプリケーションと 同じように― サーバもリクエストを 受け取るか確認できます また同時にUIを テストする際の問題もなく サーバの応答を解析できるか 確認できます
まとめましょう コードを小さく分割し ユニットテストを 実行しやすくしました
ネットワークリクエストの モックに URLProtocolを使用しました
また バランスのいい テストスイートの作成を ピラミッドモデルで 説明しました
この後はスチュアートが テクニックをお教えします (拍手) ありがとう
ありがとう ブライアン まず 通知のテストについての ベストプラクティスをお教えします
ここで“通知”というのは Foundationレベルの通知で NSNotificationと Objective-Cです 通知の監視を テストすることもあれば 通知の送信を テストする場合もあります 通知は1対多数の コミュニケーションです つまり1つの通知の受け手は アプリケーション全体や― フレームワークコード内に 複数います そこで通知のテストでは 分離することが重要です 意図しない副作用を避け 信頼性を損なうのを 防ぐためです この問題のあるコードを 見ましょう PointsOfInterest TableViewControllerです 近くのスポットを テーブルビューで表示し 位置承認に変更があると データを再読み込みします authChangedという通知を CurrentLocationProvider クラスから監視します 通知を監視する時 必要なら 再読み込みし 目的のため フラグを設定します 通知を受け取ったかを フラグでチェックするのです デフォルトの通知センターで オブザーバを追加しています ユニットテストは どうなるでしょうか authChangedNotificationを ポストしてシミュレートし そしてデフォルトの NotificationCenterにポスト このテストは機能しても 未知の副作用が出る可能性も システム通知に多いですが― 多くのレイヤに監視され 未知の副作用があり得ます テスト速度が落ちることも そこでコードを分離して テストするのです
あるテクニックで うまく分離できます 通知センターは 複数のインスタンスがあります デフォルトのインスタンスが ありますが― 必要に応じ インスタンスを生成します これが分離のカギとなるのです 新しいNotificationCenterを 作成し デフォルトのインスタンスの 代わりに使うのです 依存性の注入とも呼ばれます View Controllerで 使ってみましょう デフォルトのNotificationCenterを 使った元のコードを 修正したものです パラメータとプロパティを イニシャライザに加えました オブザーバを加えず 新しいプロパティを使います
また.defaultのデフォルトの パラメータ値を加え 既存コードの破壊を避けます 新しいパラメータを渡すのは ユニットテストだけです
ではテストを更新します 元のテストコードです 修正して 分離した NotificationCenterを使います
このように通知の監視を テストしますが― 通知の送信のテストは? 分離したNotificationCenterを 使いますが― ビルトインExpectation APIを 活用します
我々のアプリケーションの CurrentLocationProviderです このクラスは後で話しますが アプリケーションの位置承認の 変更を― 他のクラスに 通知を送信して知らせます デフォルトの NotificationCenterを ハードコードしています
このユニットテストを 記述しました notifyAuthChangedメソッドが 呼ばれたら― 通知を送信するか確認します ここでは addObserverメソッドで ブロックベースの オブザーバを作成し ブロックの中に 移動させています ビルトインのXCTNSNotification Expectation APIを使い NotificationCenter オブザーバを作成できます コードの行数を減らせる いい改善です しかしデフォルトの NotificationCenterの使用は 再考すべきでしょう
元のコードです イニシャライザに 分離した NotificationCenterを入れ デフォルトの代わりに 使いましょう
テストコードに戻り 新しいNotificationCenterを 対象に渡すため修正 しかしExpectationに ご注目を Centerで 通知を受け取るなら― NotificationCenter パラメータを Expectationの イニシャライザに渡せます
またExpectationの タイムアウトが“0”なのは 待つ前に実行されることを 期待しているからです notifyAuthChanged メソッドが戻る前に― 通知は 送信されるべきだからです この2つの テストテクニックを使って テストを分離させられます デフォルトのパラメータ値を 指定したので― 既存のコードを 修正せずに済みます
ユニットテスト記述に 伴う問題をお話しします 外部クラスとのやり取りです
アプリケーションの開発中に クラスが― アプリケーション内やSDKの 別のクラスとやり取りします 外部クラスを作成するのは 難しいため― テストの記述も困難だと 分かりますね 直接 作成されないAPIだと 特によく起こります デリゲートメソッドがあれば さらに困難です プロトコルを使って 解決しましょう 外部クラスとのやり取りを モックし テストの信頼性は 損ないません
CurrentLocationProviderクラスで CoreLocationを使っています CLLocationManagerを イニシャライザに構成し 自身をデリゲートと 設定しています
checkCurrentLocationという メソッドです 現在地をリクエストし 完了ブロックで― スポットかどうか返します requestLocationメソッドを 呼び出しています すると現在地の取得を試みて 最後にデリゲートメソッドを 呼びます 見てみましょう
extensionを使い CLLocationManagerDelegate プロトコルに準拠 保存された完了ブロックを 呼びます ではユニットテストを 書きましょう
全体を読んでみると― まずCurrentLocationProviderを 作成して 目標精度とデリゲート設定を 確認します 順調ですね ここから ご注意を checkCurrentLocationメソッドを 確認したいですが― 問題があります requestLocationが いつ呼ばれるか分かりません CLLocationManager上の メソッドだからです
もう1つ問題なのは― CoreLocationが ユーザ認証を求めることです 認証していないと 許可ダイアログが表示されます デバイスの状態に左右され テストが不合格に なりやすくなります
この問題に対処するには 外部クラスのサブクラス化と メソッドの オーバーライドです 例えば― CLLocationManagerの サブクラス化と requestLocationメソッドの オーバーライドです 最初はよくてもリスクが高いです SDKからのクラスの一部は サブクラス化に向いておらず スーパークラスのイニシャライザを 呼ぶ必要もあります しかし主要な問題は― メソッド呼び出しのため コードを修正した時で そのメソッドの オーバーライドが必要です 別のメソッドを呼び出したと コンパイラが通知せず― つい忘れて テストが失敗します この方法は お勧めしません プロトコルで外部の型を モックするのがいい その方法を説明します
元のコードです まず 新しいプロトコルを 定義します LocationFetcherと 名づけました CLLocationManagerから使う メソッドとプロパティを含みます メンバーの名前と型が一致し CLLocationManagerに extensionを作成できます 要件を満たすからです
locationManagerプロパティを locationFetcherと名を変え locationFetcherプロトコルに 型を変えます
イニシャライザに デフォルトのパラメータ値を与え 既存コードの破壊を避けます
checkCurrentLocationメソッドに 1カ所 変更が必要です
最後に デリゲートメソッドです 少し問題なのはデリゲートが マネージャパラメータを― 本物のCLLocationManagerと 考えるからです デリゲートでは 少し複雑になりますが― プロトコルを適用できます 確認しましょう
LocationFetcher プロトコルに戻り デリゲートプロパティ名を LocationFetcherDelegateへ そして型は新プロトコルへ CLLocationManagerDelegateと ほぼ同じインターフェイスです しかしメソッド名を変え 最初のパラメータ型を LocationFetcherへ
LocationFetcherDelegate プロパティを実装しましょう もはや要件を 満たさないからです ゲッタとセッタを実装し フォースキャストを用い― CLLocationManager Delegateとの間で変換 フォースキャストを 使う理由は 後ほど
デリゲートプロパティは locationFetcherDelegateへ
最後に元のextensionを 新モックデリゲート プロトコルに準拠させます プロトコルとメソッドシグネチャを 置き換えるだけです しかし前のCLLocationManager Delegateプロトコルにも準拠 本物の CLLocationManagerが― モックデリゲートプロトコルを 知らないからです 本物に準拠したextensionを 戻すのがコツで 上の同じlocationFetcher デリゲートメソッドを呼ばせます
デリゲートゲッタとセッタで フォースキャストを使うのは クラスを両方のプロトコルに 準拠させるためです
ユニットテストではテストクラスで 入れ子になった構造体を定義 locationFetcherプロトコルに 準拠し 要求を満たします requestLocationメソッドでは ブロックを呼び カスタマイズできる 偽の位置を取得します デリゲートメソッドを起動し 偽の位置を渡します
材料がそろったので テストの記述です MockLocationFetcher 構造体を作成 handleRequestLocation ブロックを設定し 偽の位置を提供します 次にCurrentLocation Providerを作成し MockLocationFetcherに 渡します 最後に完了ブロックで checkCurrentLocationを呼びます 完了ブロックでは 位置がスポットなのかを アサーションが確認します
これで クラスによる CLLocationManagerの使用を うまくモックしましたね
プロトコルを使って やり取りをモックする方法を ご説明しました おさらいしましょう
最初に 新しいプロトコルを 定義しました 外部クラスに使うメソッドや プロパティがすべて― このプロトコルに含まれます
次に 元の外部クラスに extensionを作成 プロトコルへの準拠を 宣言します
そして外部クラスの利用を 新プロトコルと置き換えて 型を設定できるように イニシャライザパラメータを追加
またSDKで一般的なプロトコルの モック法の説明です 我々は このようにやりました まずモックデリゲート プロトコルを― 同じメソッドシグネチャで 定義しました 本物の型はモックプロトコル型と 置き換えました
元のモックプロトコルで デリゲートプロパティを改名 そのプロパティを extensionで実装しました
サブクラス化などと比べると 多くのコードが必要ですが 信頼性は高く コードが破たんしにくいのです なぜなら 呼び出したメソッドはすべて 新プロトコルに 含まれるからです
最後にテスト速度について お話しします
テストに時間がかかれば 開発中の実行を避け 長いテストを 飛ばすかもしれない 我々のテストスイートでは 素早く簡単にできます そのテスト速度を 維持したいのです テストで わざと待機や スリープ状態にした経験は? 原因はコードの非同期や タイマーの使用でしょう 遅延したアクションには 注意が要りますし すべての速度が 落ちかねません 必要ではない遅延を 避ける方法をご紹介します
例えば我々がビルド中の アプリケーションでは メインUIの下部に 注目地点を表示 10秒ごとに入れ替わりながら 近くの上位地点を表示します いくつかの選択肢の中から Timer APIを使います
このクラスの ユニットテストです FeaturedPlaceManagerを 生成し scheduleNextPlaceメソッドを 呼び出すまで 現在地を保存 実行ループは11秒間で 1秒は猶予時間です 最後に currentPlaceの変化を確認 これでは実行に かなり時間が要ります そこでコードに プロパティを公開し タイムアウトを1秒程度に カスタマイズします このようにコードを 変えるのです
この方法で遅延を 1秒に減らせます 先ほどより改善して 実行が速くなりましたが― まだ理想的ではない 短くなっても遅延はあります 問題は このコードが 時間依存的なことで― 遅延を短くするほど 信頼性が損なわれ得るのです スケジュール予測が CPUに依存するからです 特に非同期コードは そうとは限りません よりよい方法を見ましょう
まず遅延の仕組みを 特定すべきです 私の例ではタイマーで DispatchQueueからの asyncAfter APIの場合もあります この仕組みをモックして 直ちに遅延アクションを実行し 遅れをバイパスします
再び元のコードです scheduledTimerメソッドの 動きを確認しましょう このメソッドは2つのことを 行います タイマーの生成と― 現在の実行ループへの 追加です タイマーの生成に 便利なAPIですが― 2つを分ければ よりテストしやすくなります
scheduledTimerを使う 先ほどのコードを変更し まずタイマーを作成 次に新プロパティに保存した runLoopを追加 コードは前と同等です 2つを分ければ runLoopは― このクラスがやり取りする 外部クラスの1つです プロトコルのテクニックで モックするのです addTimerメソッドを含んだ 小プロトコルを作り
TimerSchedulerと 名づけました addTimerメソッドは― runLoop APIの シグネチャと適合します
このプロトコルをrunLoopと 置き換えましょう
本物のrunLoopを 使いたくないので タイマーを渡す モックスケジューラを作ります
TimerScheduler プロトコルに準拠する― MockTimerSchedulerという 新しい構造体を作成 タイマーの追加時に呼ばれる ブロックを含みます
では最終ユニットテストを 記述します まず MockTimerSchedulerを作り handleAddTimer ブロックを設定 スケジューラに加えられると タイマーの遅延を記録し タイマーを発動して 遅れをバイパスし ブロックを起動します
FeaturedPlaceManagerを生成し MockTimerSchedulerを提供 最後にscheduleNextPlaceを 呼んで テストを開始 遅延のないテストの 完成です 高速で 時間に依存せず 信頼性が増しました さらに アサーションを使い 時間の遅れを確認できます 前のテストでは不可能でした
このテクニックで 遅延は完全に取り除けます 遅延を含むコードのテストに 適します しかしテスト全体の 速度向上には― テストの大部分を直接構造に すべきです 遅延のモックは要りません 我々の アプリケーションだと― 遅延が次の注目地点に 変換されます 1~2つのテストで タイマーの遅れを正せます scheduleNextPlaceメソッドを 直接 呼び出し モックは必要ありません
テストの実行速度について あと2つ コツがあります NSPredicateExpectationの 使用に関しては― 他のExpectationクラスほどの 性能はありません 直接的な コールバックでないからです 条件の評価が別のプロセスである UIテストに主に用いられます ユニットテストには 直接的な方法がお勧めです XCTestExpectationや NSNotificationExpectation KVOExpectationなどです
もう1つのコツは― アプリケーションの起動を 速めることです ほとんどは起動時に セットアップを行います 通常の起動には 欠かせませんが― テスト実行の際には その多くが不要になります View Controllerの読み込みや ネットワークリクエストの開始 分析パッケージの 設定などです これらはユニットテストでも 不要です
XCTestは デリゲートが 起動終了を伝えるのを待ちます テスト時に 起動に時間がかかるなら― テスト実行だと検出させて 回避させるのも手です
1つの方法は― カスタム環境変数や 起動引数の特定です スキームエディタの左側の “Test”から“Arguments”を開き 環境変数か起動引数を 追加しましょう このスクリーンショットでは IS UNIT TESTINGを1にしています
デリゲートの didFinishLaunchingコードを このようなコードに 変えてください スキップしたコードは ユニットテストに不要と― 必ず確認してください
おさらいです
ブライアンが バランスのいいテスト作成と ネットワークテストの テクニックを紹介しました 私は Foundation通知の分離と― 依存性の注入を話しました また 外部クラスとの やり取りという― テストを書く際の課題への 解決策を提示 テスト速度を高め 遅延を避けるコツにも触れました テストを書く際に ぜひご活用ください
詳細はウェブサイトヘ 水曜のセッションを見逃した方は ビデオをご覧ください ありがとうございました (拍手)
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。