Playwrightを使ったE2Eテストを導入した話 - インフラ編 Playwright × Allure Report × AWS

はじめに

こんにちは。ソーシャル経済メディア「NewsPicks」の QA/SET チームの海老澤です。 先日は Playwright を使ったE2Eテストの導入について、紹介させていただきました。 今回は作成したテストをAWS 基盤上で動かす方法を紹介させていただきます。

前回の記事 tech.uzabase.com

E2Eテスト実行のタイミング

NewsPicksでは 下記のタイミングで E2Eテストを実行させています。

①リリース時のカナリーデプロイ後

NewsPicks ではカナリーリリースを採用していてカナリーへのデプロイが完了した後、カナリーに向けてE2Eテストが動きます。

②開発環境デプロイ後

動作確認をしたい場合に feature ブランチなどでデプロイ後 E2Eテストを実行できるようにしています。

本記事では主に 「②開発環境デプロイ後」 を例に紹介します。

実行方法

具体的な実行方法としては、Slackからコマンドを実行します。コマンドにオプション --web-e2e を付けることで、デプロイ後にE2Eテストが実行される仕組みになっています。

デプロイのみのコマンド
@deploybot deploy np-server --env {環境名} --commit-id {コミットID | ブランチ名}

デプロイ後E2Eテストを実行したい場合
@deploybot deploy np-server --env {環境名} --commit-id {コミットID | ブランチ名} --web-e2e

上記コマンドでデプロイ -> E2Eテスト開始通知 -> E2Eテストの実行が行われます。テストの実行が完了すると下記のようにテスト結果レポートが通知されます。

E2Eテスト完了後の Slack 通知

Playwright 実行のインフラ

アーキテクチャは下記のようになっています。

  • ① Slack コマンドE2Eテストを実行する
  • ② StepFunctionsがトリガーされ、その中で E2E テストのCodeBuild が動く
  • ③ テスト結果は S3 にアップロードされる
  • ④ テスト完了後、結果の Slack 通知が来る
  • ⑤ テスト結果の通知内にある URLリンクから、Route 53, CloudFront を経由して、テスト結果へアクセス。

という流れになっています。

※ Slack の Bot からStepFunctions の実行詳細は本記事では割愛

レポートについて

レポート結果は CodeBuild のレポート機能ではなく、allure report を使って出力しています。

設定方法等は下記が参考になるかと思います。

allurereport.org

allure report はとても見やすく、テスト数や所要時間も表示できます。

また失敗時は 失敗したテストケースのエラーログ、エラー箇所、テストの動画 などが確認可能なので、原因調査、対応をとてもスムーズに行うことが可能です。

CodeBuild, CloudFront, Route53

cdk コード

具体的な CodeBuild, CloudFront, Route53 の構築について紹介します。

NewsPicksでは AWS リソースを作成する際, cdk コードで基本的には管理しています。下記がcdk のコードになります。

    // E2Eテスト実行後のテスト結果のための S3 Bucket, CloudFront Distribution を定義してます (後述)
    const e2eReport = new E2EReport(this, 'E2ETestReport')

    const buildImage = codebuild.LinuxBuildImage.fromAsset(this, 'BuildImage', {
      // CodeBuild 実行のための DockerFile のディレクトリを指定. DockerFileでやっているのは nodenv のインストールくらいなので割愛
      directory: `${__dirname}/codebuild-image`,
    })
    // CodeBuild の定義
    const project = new codebuild.Project(this, 'Project', {
      projectName: `newspicks-e2e-test`,
      source: codebuild.Source.gitHub({
        // Playwright のコードが格納されているリポジトリを指定
      }),
      artifacts: codebuild.Artifacts.s3({
        // テスト結果のレポートを格納する S3 バケットを指定
        bucket: e2eReport.s3Bucket,
      }),
      environment: {
        buildImage,
        computeType: codebuild.ComputeType.X_LARGE,
        environmentVariables: {
          REPORT_ID: {
            value: '',
            type: codebuild.BuildEnvironmentVariableType.PLAINTEXT,
          },
        },
      },
      buildSpec: codebuild.BuildSpec.fromObject({
        version: '0.2',
        env: {
          shell: 'bash',
        },
        phases: {
          install: {
            commands: [
              // テスト結果が保存されるパスを変えています。指定がなければ, Playwright が格納されているリポジトリのソースバージョンになります。
              // 実行する際に指定できるよう`REPORT_ID` は環境変数にしています。
              'export REPORT_ID=${REPORT_ID:-"newspicks-$CODEBUILD_RESOLVED_SOURCE_VERSION"}',
              '(cd e2e/ && nodenv install -s && nodenv exec npm install)',
            ],
          },
          build: {
            // Playwright の実行
            commands: [`(cd e2e/ && ENV_NAME=${envName} npx playwright test)`],
          },
          post_build: {
            commands: [
              // テストレポートの生成
              '(cd e2e/ && npx allure generate --clean)',
              '(cd e2e/ && cp -r allure-report/ $REPORT_ID/)',
            ],
          },
        },
        artifacts: {
          files: ['$REPORT_ID/**/*'],
          'base-directory': 'e2e',
        },
        cache: {
          paths: ['e2e/node_modules/**/*'],
        },
      }),
    })

   // CodeBuild から S3 へのアクセス権限追加
    e2eReport.s3Bucket.grantReadWrite(project.grantPrincipal)

E2EReport は以下のようになります。S3 Bucket, CloudFront Distribution を定義してます。

export class E2EReport extends Construct {
  s3Bucket: s3.Bucket
  cloudfrontDistribution: cloudfront.Distribution
  constructor(scope: Construct, id: string) {
    super(scope, id)

    this.s3Bucket = new s3.Bucket(this, 'e2eReportS3Bucket', {
      // 略
    })

    this.cloudfrontDistribution = new cloudfront.Distribution(
      this,
      'e2eReportCloudFront',
      //略
    )

    const hostedZone = route53.HostedZone.fromLookup(this, 'HostedZone', {
      domainName: {ドメイン名},
    })

    new route53.ARecord(this, 'ARecord', {
      zone: hostedZone,
      recordName: {レコード名},
      // ターゲットに 上記で定義した CloudFront Distributionを指定
      target: route53.RecordTarget.fromAlias(
        new targets.CloudFrontTarget(this.cloudfrontDistribution),
      ),
    })

    // CloudFrontからS3へのアクセス追加
    this.s3Bucket.addToResourcePolicy(
      new iam.PolicyStatement({
        actions: ['s3:GetObject'],
        effect: iam.Effect.ALLOW,
        principals: [new iam.ServicePrincipal('cloudfront.amazonaws.com')],
        resources: [`${this.bucketArn}/*`],
        conditions: {
          StringEquals: {
            'AWS:SourceArn': [
              cdk.Stack.of(this).formatArn({
                service: 'cloudfront',
                resource: 'distribution',
                resourceName: this.cloudfrontDistribution.distributionId,
                region: '',
              }),
            ],
          },
        },
      }),
    )
  }
}

cdk コードで管理することにより、Code Build のアップロード先、Route53 のターゲット追加、権限追加など各リソースへの紐付きが簡潔にできます。

cdk での IaC 管理はメリットづくめで詳細知りたい方は下記ブログをご参照ください

tech.uzabase.com

Code Build の computeType について

前回のブログ で紹介した通り、PlayWright ではプロセス数(workers)を指定しなければCPUに応じて適切な数プロセスを立ち上げます。 ※1

computeTypeとしてLARGEとX_LARGEを使用した場合の料金と実行時間の比較した結果、下記のようになりました。(テストケースが100件ほどの時に検証しました)

computeType 1 分あたりの料金 PlayWright の実行時間 1回あたりの料金
LARGE(8vcpu) 0.02 USD ※2 9分 0.18 USD
X_LARGE(36vcpu) 0.1002 USD ※2 4分 0.4008 USD

PlayWrightの実行に依存関係や、CodeBuild で動く レポートの作成、アップロード などの PlayWright 以外の処理があるため vpcu に綺麗に比例するようにはなりませんが上記のようになりました。 1回あたりの料金を考えると LARGE の方が良いですが、開発者の稼働料金、開発者体験を鑑みて X_LARGE を採用しました。

※1

Defaults to half of the number of logical CPU cores. Learn more about parallelism and sharding with Playwright Test.

playwright.dev

※2

aws.amazon.com

StepFunctions

続いて StepFunctions の 構築について紹介させていただきます。

定義は下記の図のようになっています。

  1. 環境へのdeploy
  2. E2E テスト実行開始の通知
  3. E2Eテストの実行

というフローです。

デプロイ & E2Eテスト 実行時の Step Functions 定義

※ 環境によってはモバイルアプリ等の 他の E2Eテストを動かす場合があるため Start E2E Test Web PlayWright は Map の中に定義してます。

cdk コード

cdk のコードは下記のようになります。

重要なのは deploy の StateMachine deploymentSfn は他で定義されていて、ここでは E2Eテスト関連の定義のみが記されているということです。 もともとは deploy StateMachine の中にE2E テスト実行も定義していたのですが、このように責務を分けることでメンテナンスがしやすくなりました。

export interface RunE2ETestSfnProps {
    // 他で定義された deploy の StateMachine を受け取る
    readonly deploymentSfn: sfn.StateMachine;
}

export class RunE2ETestSfn extends sfn.StateMachine {
    constructor(scope: Construct, id: string, props: RunE2ETestSfnProps) {
        const { deploymentSfn } = props;

        const npServerDeploymentSfnExecution = new tasks.StepFunctionsStartExecution(scope, "Deployment", {
            stateMachine: deploymentSfn,
            integrationPattern: sfn.IntegrationPattern.RUN_JOB,
            // 略
        });

        const notifyStartE2ETest = // 略

        const e2ETestParallelState = // codebuild の実行

        const definition = npServerDeploymentSfnExecution.next(notifyStartE2ETest).next(e2ETestParallelState);

        super(scope, `run-e2e`, {
            stateMachineName: `run-e2e-test`,
            definitionBody: sfn.DefinitionBody.fromChainable(definition),
        });
    }
}

終わりに

前回のPlaywright導入 の続きとして テスト実行環境をAWS上に構築し、デプロイメントパイプラインに組み込むことで、品質向上のための自動化と効率化の方法を紹介させていただきました。

ご参考になれば幸いです。

Page top