AWS CDK × CodeBuild × CypressでE2Eテストを自動化&実行動画をS3に保存してみる

こんにちは!エンジニアの岩間です。

最近、AWS CDK × CodeBuildでCypressのE2Eテスト自動化 & レポート動画をS3に保存してみる機会があったのでその手順とCDKのコードを公開したいと思います。

 

目次

はじめに

Cypressとは、WebアプリケーションのテストやE2E(エンドツーエンド)のテストを行うためのオープンソースのテストツールです。
主にJavaScriptで記述されており、モダンなフロントエンド開発に適したテストフレームワークとして利用されています。
テスト実行時にはスクリーンショットやビデオキャプチャを取得し、問題の解析やレポートに利用することができます。

 

以前のブログで取り上げた単体テストは、個々のコンポーネント(関数やメソッドなど)をテストすることを目的としているのに対し、E2Eテストは全体的な動作をテストするのが目的とされています。
E2Eテストでは、ユーザーがアプリケーションを使用する際に行う操作やフローをシミュレートし、期待どおりの結果が得られるかどうかを確認します。

 

構成図

構成図は以下です。

 

開発者がAWS CodeCommitのdevelopブランチにソースコードをプッシュすると、AWS CodePipelineが自動的にトリガーされ、その後AWS CodeBuildでCypressのE2Eテストが実行される、といった流れです。

 

CodeBuildはVPCのプライベートサブネット内に配置しており、別のVPCのパブリックサブネットのECS on EC2で実行しているLaravelアプリケーションに対してテストを行います。

CodeBuildをVPCのプライベートサブネットに配置している理由は、EC2 on ECSのセキュリティグループでIP制限をかけたい、ということに関係しています。

CodeBuildをVPC外に配置する場合、共用のグローバルIPが利用されることになり、実行毎にIPアドレスが固定されません。

そのため、CodeBuildをプライベートサブネットに配置し、NATゲートウェイ経由でインターネットアクセスさせることで、ECS on EC2のセキュリティグループの接続元IPをNATゲートウェイのパブリックIPで制限をかけることができます。

 

なお、テスト実行時のスクリーンショットと動画は、NATゲートウェイ経由でS3にインターネットアクセスし保存します。

 

前提条件

以下のバージョンで構築しています。

AWS CDK:2.116.0

Cypress:13.7.0

CodePipeline:v1

 

ディレクトリ構成

ディレクトリ構成は以下の通りです。

.
├── pipeline-cdk(CDKのディレクトリ)
├── e2e(Cypressのディレクトリ)
│ ├── cypress
│ │ ├── test.cy.js(テストファイル)
│ ├── fixtures
│ ├── support
│ ├── cypress.config.js(Cypressの設定ファイル)
│ ├── packege-lock.json
│ ├── packege.json
│ ├── support
└── buildspec_e2e_test.yml(CodeBuildで実行されるファイル)

 

コーディング&設定

以下のファイルのコーディング&設定を行なっていきます。

  • e2e/cypress.config.ts
  • e2e/cypress/test.cy.js
  • pipeline-cdk/lib/pipeline-cdk-stack.ts(CDKのコード)
  • buildspec_e2e_test.yml(CodeBuildで実行されるファイル)

 

Cypressの設定

まずはCypressの設定をしていきます。

Cypress Cloudへの登録

Cypressでテスト実行時の動画を記録するには、Cypressプロジェクトの識別IDであるプロジェクトIDと、録画の認証キーであるレコードキーを取得する必要があります。
そのため、Cyperss Cloudに登録&新しくプロジェクトを作成し上記を取得しておきましょう。

具体的な設定手順はこちら

 

プロジェクトIDとレコードキーを取得することで、テスト結果と動画がCypress Cloudで管理され、過去のテスト動画のリプレイや、詳細なレポートをダッシュボードから確認することができます!

※無料プランの場合、500件までのテスト結果の保持と、50ユーザーまで作成可能です。(2024/4時点)

 

ファイル設定など

次に、cypress.config.js で以下の設定を追加します。

const { defineConfig } = require("cypress");

module.exports = defineConfig({
  e2e: {
    setupNodeEvents(on, config) {},
    baseUrl: 'http://xxx.xxx' // テスト対象のURLを設定
  },
  video: true, // ビデオ録画設定
  projectId: 'xxxxxxxx', // Cypress Cloudから取得したプロジェクトID
});

baseUrlにはテスト対象のURLを指定します。
cypress run 実行中のビデオ録画はデフォルトで無効になっているため、上記のようにvideo:trueに設定します。
また、プロジェクトIDはCypress Cloudで取得したものを指定します。

 

【オプション】日本語文字化け対応

テスト実行時のスクリーンショットや録画で、日本語フォントが□□□□のように文字化けしてしまうことがあります。

そのためテストケースを記載したファイル(test.cy.js)のcy.getのオプションに日本語の文字化けさせないように以下の設定を追加します。

cy.visit(
"/login"  // アクセス先のパスを設定
, {
      onBeforeLoad(window) {
          // ブラウザの言語を日本語に設定
        Object.defineProperty(window.navigator, 'language', { value: 'ja-JP' });
        Object.defineProperty(window.navigator, 'languages', { value: ['ja'] });
      },
      headers: {
          // Accept-Languageヘッダーを日本語に設定
         'Accept-Language': 'ja-JP,ja;q=0.9,en-US;q=0.8,en;q=0.7',
        },
});

 

CDK

次にCDKの設定です。今回はTypeScriptで、CodePipelineのCDKスタックを定義しています。

pipeline-cdkディレクトリ配下で、

  • cdk init sample-app --language typescript
  • cdk bootstrap

を実行済みの状態で、lib/pipeline-cdk-stack.tsに以下の記述を追加します。

 

※今回VPC、サブネット、NATゲートウェイは既にコンソールで作成済みのため、CDKではリソースのIDをもとに取得&設定しています。(「xxxx…」の箇所)

リソースIDやCypressのレコードキーはcdkディレクトリ内のconfig.tsで環境ごとに定義したり、SSMパラメータストアから取得するやり方がおすすめです。

import { CfnOutput, Stack, StackProps, ScopedAws } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as codebuild from 'aws-cdk-lib/aws-codebuild';
import * as ec2 from 'aws-cdk-lib/aws-ec2';
import * as codecommit from 'aws-cdk-lib/aws-codecommit';
import * as codepipeline from 'aws-cdk-lib/aws-codepipeline';
import * as codepipeline_actions from 'aws-cdk-lib/aws-codepipeline-actions';

export class PipelineCdkStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, 'test-pipeline', props);

    // CodeCommitリポジトリ作成
    const sourceRepo = new codecommit.Repository(this, 'sample-code-repository', {
      repositoryName: 'TEST',
    });
    
    // CodePipeline作成
    const pipeline = new codepipeline.Pipeline(this, 'test-pipeline', {
      pipelineName: 'test-pipeline',
    });

    // CodeBuildを配置する既存のVPC-Aを取得
    const vpc = ec2.Vpc.fromVpcAttributes(this, 'vpc', {
      vpcId: 'xxxxxxxxx', // CodeBuildを配置する既存のVPCのID
      availabilityZones: ['ap-northeast-1a', 'ap-northeast-1c'], // CodeBuildを配置する既存のVPCのAZ
        publicSubnetIds: ['xxxxxxxxxxx', 'xxxxxxxx']  // CodeBuildを配置する既存のVPCのパブリックサブネット
    });

    // CodeBuild用のセキュリティグループ作成
    const codeBuildE2eSecurityGroup = new ec2.SecurityGroup(this, 'codebuild-e2e-sg', {
      vpc: vpc,
    });

    // VPC-B内の既存のALBセキュリティグループを取得
    const albSecurityGroup = ec2.SecurityGroup.fromSecurityGroupId(this, 'alb-security-group', 'xxxxxxxxxxxx');

    // インバウンドルールを追加(NATGWのIPから80ポート許可)
    albSecurityGroup.addIngressRule(ec2.Peer.ipv4('xx.xx.xxx'),ec2.Port.tcp(80));
    
    // CodeBuildを配置するプライベートサブネットのIDを定義
    const codeBuildPrivateSubnetIds = ['xxxxxxxxxxx', 'xxxxxxxx'];

    // CodeBuildを配置するプライベートサブネットを取得 
    const privateSubnets: ec2.ISubnet[] = codeBuildPrivateSubnetIds.map(subnetId => ec2.Subnet.fromSubnetId(this, 'private-subnet', subnetId));
    
    // CodeBuild作成
    const e2eTestBuild = new codebuild.PipelineProject(
      this,
      'e2e-test',
      {
        environment: {
          // Cypress公式のイメージを指定
          buildImage: codebuild.LinuxBuildImage.fromDockerRegistry('public.ecr.aws/cypress-io/cypress/browsers:node-20.11.1-chrome-123.0.6312.58-1-ff-124.0-edge-122.0.2365.92-1'),
          computeType: codebuild.ComputeType.SMALL,
          privileged: true,
        },
        environmentVariables: {
          CYPRESS_RECORD_KEY: {
            value: 'xxxxxxxx', // Cypressのレコードキー
          }
        },
        vpc: vpc,
        securityGroups: [codeBuildE2eSecurityGroup],
        subnetSelection: {
          subnets: privateSubnets,
        },
        buildSpec: codebuild.BuildSpec.fromSourceFilename('buildspec_e2e_test.yml'),
      }
    );

    const sourceOutput = new codepipeline.Artifact();
    const e2eTestOutput = new codepipeline.Artifact();
    
    // CodePipelineにCodeCommitステージを追加
    pipeline.addStage({
      stageName: 'Develop-Commit',
      actions: [
        new codepipeline_actions.CodeCommitSourceAction({
          actionName: 'CodeCommit',
          repository: sourceRepo,
          output: sourceOutput,
          branch: 'develop',
        }),
      ],
    });
    
    // CodePipelineにCodeBuildステージを追加
    pipeline.addStage({
      stageName: 'E2E-Testing',
      actions: [
        new codepipeline_actions.CodeBuildAction({
          actionName: 'E2ETest',
          project: e2eTestBuild,
          input: sourceOutput,
          outputs: [e2eTestOutput],
        }),
      ],
    });

    // CloudFormation「出力」タブに出力設定
    new CfnOutput(this, 'CodeCommitRepositoryUrl', {
      value: sourceRepo.repositoryCloneUrlHttp,
    });
  }
}

 

ポイントは以下の2点です。

CypressのDockerイメージ指定

CypressをCodeBuildで実行するためのDockerイメージが公式で提供されているので、今回はそちらを利用しています。

OSはLinux、x86-64、ARM 64対応で、テスト実行するブラウザはChrome、Firefox、Microsoft Edgeが使用可能です。

 

CodeBuildの環境変数にレコードキーを渡す

CYPRESS_RECORD_KEYでCypress Cloudで取得したレコードキーを環境変数に設定しています。

 

buildspec_e2e_test.yml

buildspecファイルとは、AWS CodeBuildでビルドプロジェクトを実行する際に使用される構成ファイルです。

buildspec_e2e_test.ymlを以下のように記述します。

version: 0.2
phases:
  install:
    runtime-versions:
      nodejs: latest
  pre_build:
    commands:
      # 日本語フォントのインストール
      - apt install -y wget fontconfig
      - fc-cache -fv
      - mkdir -p ~/.fonts
      - wget https://noto-website-2.storage.googleapis.com/pkgs/NotoSansCJKjp-hinted.zip -O ~/.fonts/NotoSansCJKjp-hinted.zip
      - unzip ~/.fonts/NotoSansCJKjp-hinted.zip -d ~/.fonts
      - fc-cache -fv
      
      # Cypressのインストール
      - cd e2e
      - npm install

            # スクリーンショットと動画をおくためのフォルダを作成
      - mkdir screenshots
      - mkdir videos
  build:
    commands:
      # テスト実行
      - npx cypress run --record --browser chrome

artifacts:
  files: 
   # S3に出力するファイルを指定
    - e2e/cypress/screenshots/test.cy.js/*
    - e2e/cypress/videos/*

 

  • pre_buildフェーズ・・・テスト実行時のスクリーンショットと動画の日本語が文字化けしないように、日本語フォントをインストールしています。
  • buildフェーズ・・・テスト実行コマンドを記述しています。引数(–brower)には、テストを実行したいブラウザを指定しており、今回はchromeにしています。(Chrome/Electron/Edgeが指定可能)
  • artifactのフェーズ・・・S3に出力したいファイルを指定しています。
    ※Cypressのスクリーンショットはデフォルトでcypress/screenshotsフォルダ、動画はcypress/videosフォルダに保存されます。

CypressとAWS CodeBuild連携について詳しくはこちら

 

動作確認

以上で設定完了です!

CodeCommitのdevelopブランチにすべてのソースをpush後、動作確認を行います。

 

AWSコンソールでCodePipelineと検索し、対象のパイプライン名を選択すると、CodeCommitのdevelopブランチにpushしたのをトリガーにCodePipelineが動いていることを確認できます。

 

Artifacts出力の動作確認

CodePipelineが動いていることが確認できたら、次はテスト結果の動画とスクリーンショットがS3にアップロードされているか確認していきます。(buildspec_e2e_test.ymlのartifactで指定した箇所)

 

スクリーンショットは、cypress run 実行中にテスト失敗したケースのみ自動で撮影してくれます。(スクリーンショット、動画の設定について詳しくはこちら

そのためわざとテストを失敗させてみます。

E2E-Testingのステージが失敗すると以下の画像のような表示になるので、画面内の「AWS CodeBuild」のリンクをクリック→「ビルドの詳細」タブに移動します。

 

以下の画像のように、アーティファクトのセクションに「アーティファクトのアップロード場所」の項目があるのでリンクをクリックします。

 

S3の画面が表示されるのでzipファイルをダウンロードし、e2e/cypress/screenshotsとe2e/cypress/videos以下のファイルがあればOKです。

 

Cypress Cloud確認

今度はCypress Cloudにテスト結果が連携されているかどうか確認します。

Cypress Cloudにログイン後、対象のプロジェクトページに移動すると、テスト結果が確認できます。

 

以下の画像はテスト実行時間のレポートです。今回のテストは24秒かかったことがわかります。

「Test Results」のタブに移動すると、テストの実行動画が視聴できます。

対象のテストの「Test Replay」をクリックすると・・・・

 

 

 

 

以下の動画のようにテスト動画を見ることができます!(サンプル)

テストに失敗した場合は、どの部分で失敗したのか簡単に確認できます。

 

まとめ

以上、AWS CDK × CodeBuild × CypressでE2Eテストを自動化&実行動画をS3に保存してみるでした。

Cypressすごすぎました。

私は今回初めてCypressを使ったのですが、充実した公式ドキュメントや豊富な機能のおかげで、快適なテスト体験ができました。

他にもAWS CodeBuildと連携できたり(GitHub等も)、無料で詳細レポートやテスト動画の再生ができたりと、テストの作成から実行、そして結果の分析までを効率的に行うことできる素晴らしいテストツールです!

 

長くなりましたが、ここまで読んでいただきありがとうございました。

この記事が気に入ったら
いいね ! しよう

Twitter で

【採用情報】一緒に働く仲間を募集しています

採用情報
ページトップへ