本記事は、NewsPicks Advent Calendar 2022 の 12/14 公開分の記事になります。
こんにちは。NewsPicks SREチームの 海老澤 です。
今回は iOSのE2Eテストを実機で動かす上でのインフラ周りの設定方法を紹介しようと思います。
課題
NewsPicksではサーバーリリース時に Firebase Test Labで iOSのE2Eテストを実行していました。
Firebase Test Labは時間帯(夕方くらいになると混んでくる傾向)によってはテスト開始が遅い場合があり、リリースサイクルを高速化するために実機iPhoneでの安定したE2Eテストの実行に取り組みました。
構成図
構成図は以下です。
まずリリース時にAWS Step Functionsから SQSにメッセージを送信し、S3のテスト結果をポーリングします。
受け取り側のMacはSQSメッセージをポーリングして、メッセージを受け取ったのちGoogle Cloud Storage(元々Firebase Test Labで実行されているものなので GCSにありますが、S3等でも問題ありません)にある.xctestrun
ファイルをダウンロードし、テストを実行します。テストを終えた後はS3にテスト結果を配置します。
Step Functionsがテスト結果をポーリングしていて、取得できればリリースしているユーザーに通知します。
詳細
S3, SQS,Step Functions はcdkで作成、Macでの処理はシェルで行なっています。
cdk
// S3 const testResultBucket = new s3.Bucket( this, `test-result-bucket`, { bucketName: `test-result-bucket`, removalPolicy: cdk.RemovalPolicy.DESTROY, lifecycleRules: [ { expiration: cdk.Duration.days(30), }, ], autoDeleteObjects: true, } ); // SQS const iosE2EQueue = new sqs.Queue(this, "Queue", { queueName: "IosE2E", }); // Step Functions 定義 詳細は後述 const localE2E = createE2ELocalTestSfn( this, testResultBucket, iosE2EQueue ); // 依存性を定義 localE2E.node.addDependency( iosE2EQueue, testResultBucket ); // localE2E をリリースのStep Functionの中などに定義します、 sfns.push( new tasks.StepFunctionsStartExecution( this, "Start local E2E", { stateMachine: localE2E, input: sfn.TaskInput.fromObject({ ~~~ }), integrationPattern: sfn.IntegrationPattern.REQUEST_RESPONSE, resultPath: "$.results.locale2e", } ) );
cdkのコードはこのようになります。S3, SQS を作ったのちにStep Functions を作成したいので依存性を定義しています。
次に Step Functions ですが、定義は図のようになっています

コードは下記のようになっています。
export function createE2ELocalTestSfn( stack: cdk.Stack, testResultBucket: Bucket, queue: sqs.IQueue ): sfn.StateMachine { // SQS にメッセージを送る const startTest = new tasks.SqsSendMessage(stack, "Start local E2E", { queue, messageBody: sfn.TaskInput.fromObject({ ~~ }), outputPath: "$", resultPath: "$.results.start", }); // すぐにはテストは終わらないので 5分待ちます。 const wait = new sfn.Wait(stack, "Wait for local E2E to Complete", { time: sfn.WaitTime.duration(cdk.Duration.minutes(5)), }); // SQS の messageIdを元に該当のテスト結果を取得します // S3から取得しているだけなので詳細は割愛します。 const getRun = createGetLocalE2E(stack, config, testResultBucket); const getRunState = new tasks.LambdaInvoke(stack, "get run local E2E", { lambdaFunction: getRun, payloadResponseOnly: true, payload: sfn.TaskInput.fromObject({ messageId: sfn.JsonPath.stringAt("$.results.start.MessageId"), }), resultPath: "$.results.run", }); // E2Eテストの結果をslack通知します。詳細は割愛します。 const report = new tasks.LambdaInvoke( stack, "Notify finished local E2E TEST", { lambdaFunction: slack, payload: sfn.TaskInput.fromObject({ ~~~ }), resultPath: "$.results.notifyFinishedLocalE2ETest", } ); // テスト結果が作成されるまでのポーリング処理です。 const choice = new sfn.Choice(stack, "Choice TestLocal State"); choice .when( sfn.Condition.not( sfn.Condition.stringEquals("$.results.run.state", "FINISHED") ), new sfn.Wait(stack, "Wait for local E2E Test to Complete Loop", { time: sfn.WaitTime.duration(cdk.Duration.minutes(1)), }).next(getRunState) ) .otherwise(report); const definition = startTest.next(wait).next(getRunState).next(choice); const machine = new sfn.StateMachine( stack, `e2e-test`, { stateMachineName: `e2e-test`, definition, timeout: Duration.minutes(60), } ); return machine; }
以上でS3, SQS,Step Functions の作成が終わりました。
※ 今回はテスト結果をポーリングしてしまったのですが、Step Functionsのタスクトークンのコールバックまで待機する機能を使えばテスト終了後即次のステップに進めて時間短縮になりそうです。
Mac側の処理
Mac側はiPhoneを繋いだ状態で下記を行います。
- 該当のアプリを削除 ※1
- SQSのメッセージ取得 (aws cliコマンド) メッセージがあった場合下記
- Google Cloud Storage から
.xctestrun
ファイル取得 (gsutil コマンド) - テスト実行 ※2
- テスト結果をs3に上げる (aws cliコマンド)
- Google Cloud Storage から
※1: 該当のアプリがインストール済みだとアカウント情報などが保持され、失敗するためアプリを削除してまっさらな状態からテストを実行します
アプリの削除はideviceinstallerを使います。 ideviceinstaller --list-apps
からApp IDを取得し、ideviceinstaller --uninstall {AppId}
で該当のアプリを削除します。
※2: テストは
xcodebuild test-without-building -xctestrun {.xctestrunファイル} -destination "id={DEVICE_ID}" | xcpretty -r html
で実行します。
DEVICE_ID は xctrace list devices
などで取得できます。またテスト結果はわかりやすくするため、 xcpretty でhtmlにしています。
github.com
作成されたスクリプトは Macの launchctl
でデーモン化し、常に動くようにします。
注: iPhone側の設定でパスコードが設定されていると失敗する場合があります。
結果
まだ動かし始めて、2,3営業日ほどですが、特に夕方はFirebase Test Labより安定していて、5,6分ほど早く終わる傾向にありました。

ぜひ参考にしてみてください。