Zenn
🚴

ECS上にE2Eテスト基盤としてPlaywrightによる実行環境を構築した

2025/01/20に公開
10

こんにちは、QAの木下です。
この記事では、E2E自動テスト環境の改善に成功した話について、紹介します。

E2E自動テストツール導入時からの変化

現行のE2E自動テストツールの課題

ウェルスナビで実施しているリグレッションテストは、企業が提供するE2E自動テストツールを利用する前提で、テストケースやテストシナリオを作成していました。
そのためテストケースやテストシナリオが、ベンダーロックイン状態となっていて、企業でサービスに対して大きな変更が行われた場合、ウェルスナビでのテスト実施に大きな影響がありました。
もしE2E自動テストツールの利用ができなくなった場合、手動でリグレッションテストを実施する必要があります。
しかし、現在のウェルスナビでのリグレッションテストは、E2E自動テストツールを導入する前と比較して、対象のWebサイトが提携企業の増加とともに大きく増えているため、テスト対象の範囲が多くE2E自動テストツールは必要不可欠です。
つまり現状、E2E自動テストツールを利用できないと、リリースサイクルを守れない、あるいは、テストの品質が落ちるといったリスクを抱えていました。

QAチームの成長

一方で、ウェルスナビのQAチームは、E2E自動テストツールを導入した頃と比較してメンバーが増え、各々が自身の専門領域に注力できる環境となってきました。
それにより、今までテスト自動化に加えて、品質保証の業務も対応していたメンバーが、E2E自動テストツールの改修に専任できる状況になりました。

より良いE2E自動テストツールの検証

そこで、新たな課題の発生やメンバーの充実により、現行のE2E自動テストツールの課題を解決するため、過去記事で取り上げたツールから選定を改めて行いました。
検証の結果、以下の理由から Playwright を採用しました。

  • ツールを無償で利用できる
    • OSS
  • ツールの品質が安定している
    • Microsoftが支援
    • flakyさが少なく、テスト実行が安定
  • 開発可能な言語が豊富である
    • TypeScript
    • JavaScript
    • Java
    • Python
    • C#
  • テスト可能なブラウザが豊富である
    • Chrome
    • Safari (webkit)
    • Firefox
    • Edge
  • ブラウザ操作で、テストケースを自動生成できる
    • VSCodeの拡張機能を利用
  • 実行速度が早い
  • 機能が充実している
    • 自動待機機能あり
    • リトライ機能あり
    • 動画撮影機能あり
    • レポーティング機能あり

Playwright採用の重要な要素は、高品質かつ安定的にテストを実行できる点でした。
E2E自動テストツールは、人では補いきれないテストを代わりに行うことで、プロダクトの品質を保証することが目的となります。
そのためE2E自動テストツールが、実行のたびに結果が変わる Flaky Test とならないことは、非常に重要なツール選定基準でした。

E2E自動テストツールを誰もが使えるように

Playwrightの実行基盤をAWS上に構築することを検証

初案

E2E自動テストツールを誰もが手軽に利用できるように、Playwrightの実行基盤の構築先の検証が始まりました。
Playwrightは、テストを実行するとローカル環境で実行され、実行結果がプロジェクトディレクトリ内に出力されます。
しかしE2E自動テストでのリグレッションテストの結果は、社員全員がいつでも確認できる環境が望ましいです。
そのためE2E自動テストの基盤は、Web上で操作や確認ができることが最良と考え、AWS上に構築することとしました。
初案は、以下の図のとおりです。
AWS上にPlaywrightを構築した初案
最初の案では、AWS FargateでAPIとしてテスト実行のリクエストをポーリングして、swaggerからリクエストが届くと、Playwrightを実行する構成としました。

改善案

しかし、実際に構築してみると、以下の課題が発生したため、別の方法を検討しました。

  • 複数同時にテストシナリオを実行できない
    • 一度swaggerからFargateへリクエストしてPlaywrightを実行すると、Playwrightの実行が完了するまで、並行してPlaywrightを実行できない
  • テストシナリオの実行時間が長い
    • 一つのテストシナリオに大量のテストケースを用意すると、実行に時間がかかり、提携企業の数だけのテストシナリオを実行すると、営業時間内にテストが終わらない

そこで、以下の図のとおり、AWS ECSで予めタスク定義を行っておき、リクエストの際にswaggerからタスクを生成して実行する方法に変更しました。
AWS上にPlaywrightを構築した改善案
結果として、複数同時にテストシナリオを実行でき、テストケースが多いテストシナリオも分割して実行できるよう改善できました。
また、APIからバッチへと構成を変更したことで、ECSが実行されるタイミングのみ課金が発生する仕組みとなりました。それにより、初案と比較して、E2E自動テスト実行による費用を大きく削減できました。

Playwrightを実行する基盤のディレクトリ構成

それでは、Playwrightを実行するための構成について、紹介します。
まず、Playwrightの実行基盤のディレクトリ構成は、以下のとおりです。

.
├── cmd ... Playwrightを実行するGoのmainディレクトリ
├── pkg ... Playwrightを実行するGoのパッケージディレクトリ
├── playwright-report ... Playwright実行後に格納される結果ディレクトリ
├── src ... 複数のWebサイトを対象とした、Playwrightのテストケースディレクトリ
│   ├── utils ... Playwrightでの共通ライブラリディレクトリ
│   └── roboad ... ロボアドディレクトリ
│       └── tests ... ロボアドのテストケースディレクトリ
│           └── images ... ロボアドのテストケースで、ファイルアップロードに利用する画像ディレクトリ
└── test-plans ... Playwrightのテストシナリオディレクトリ
    └── roboad ... ロボアドのテストシナリオディレクトリ

AWS ECS上で実行されるPlaywrightのプロジェクト

Playwrightのコード

次に、PlaywrightプロジェクトでのDockerfileやPlaywrightの設定ファイルの一部を紹介します。

Dockerfileでは、Playwrightのプログラムとは別に、Playwrightを実行しS3への結果アップロード、Slackへの通知を行うプログラムを用意しました。
Dockerfile
# Playwrightをコマンド実行するプログラムを用意します
FROM golang:1.22-alpine as builder
WORKDIR /app
COPY go.* ./
RUN go mod download
COPY . ./
RUN GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -ldflags "-s -w" -mod=readonly -v -o batch ./cmd/batch

# Playwrightの構成をコンテナに用意、合わせてPlaywrightをアップデートします
FROM  mcr.microsoft.com/playwright:latest
COPY --from=builder /app/batch /app/batch
WORKDIR /app
COPY src/ /app/src/
COPY test-plans/ /app/test-plans/
COPY *.ts /app/
COPY *.json /app/
RUN cd /app && npm ci
RUN npx playwright install --with-deps
RUN npx playwright install msedge
ENTRYPOINT ["/app/batch"]
playwright.config.tsでは、Playwrightの実行結果が他の実行結果と重複しないよう、結果が出力されるディレクトリの名前を変更します。
playwright.config.ts
import * as path from "path";
import {defineConfig, devices} from '@playwright/test';

// Playwrightプロジェクトディレクトリに出力されるテスト結果のディレクトリ名を変更します
const reportDirectory = path.resolve('./', 'playwright-report', process.env.RESULT_DIR ?? '');

export default defineConfig({
    testDir: './test-plans',
    reporter: [
        ['html', {open: 'never', outputFolder: reportDirectory}],
    ],
});

Playwrightを実行するコード

Playwrightのテスト実行やS3へのアップロード、Slackへ通知するコードの一部を紹介します。

runCommand.goでは、Playwrightをコマンド実行します。
runCommand.go
import (
	"bytes"
	"context"
	"fmt"
	"os"
	"os/exec"
	"time"
)

type runCommand struct{}

func (u *runCommand) RunPlaywright(ctx context.Context, config string) error {
	var err error
	var cmd *exec.Cmd
	var out bytes.Buffer
	var stderr bytes.Buffer

	// Playwrightをコマンド実行します
	cmd = exec.CommandContext(ctx, "npx", "playwright", "test", "--config", config)
	date := time.Now().Format("060102150405.00")
	cmd.Env = os.Environ()
	// 実行結果のディレクトリ名を重複しないよう指定します
	cmd.Env = append(cmd.Env, "RESULT_DIR="+date)
	cmd.Stdout = &out
	cmd.Stderr = &stderr
	err = cmd.Run()
	if err != nil {
		return fmt.Errorf("failed to run playwright: %w", err)
	}
	return nil
}
s3Wrapper.goでは、Playwrightでの実行結果をS3にアップロードします。
s3Wrapper.go
import (
	"context"
	"fmt"
	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"github.com/aws/aws-sdk-go-v2/service/s3/types"
	"io/fs"
	"os"
	"path/filepath"
	"strings"
)

type s3Wrapper struct {
	bucketName string
	s3client   *s3.Client
}

func (s *s3Wrapper) Upload(ctx context.Context, directoryPath string) error {
	dirBase := filepath.Base(directoryPath)
	err := filepath.Walk(directoryPath, func(path string, info fs.FileInfo, err error) error {
		if err != nil {
			return fmt.Errorf("failed to walk into dir: %w", err)
		}
		if !info.IsDir() {
			var contentType string
			file, err := os.Open(path)
			if err != nil {
				return fmt.Errorf("failed to open %s: %w", directoryPath, err)
			}
			defer file.Close()
			key := dirBase + strings.TrimPrefix(path, directoryPath)
			// S3をWebサイト代わりとして利用できるよう、アップロードファイルの拡張子に応じて、Content-Typeを指定します
			switch filepath.Ext(path) {
			case ".html":
				contentType = "text/html"
				break
			case ".png":
				contentType = "image/png"
				break
			case ".webm":
				contentType = "video/webm"
				break
			case ".json":
				contentType = "application/json"
				break
			default:
				contentType = "application/octet-stream"
				break
			}
			_, err = s.s3client.PutObject(ctx, &s3.PutObjectInput{
				Bucket:      aws.String(s.bucketName),
				Key:         aws.String(key),
				ContentType: aws.String(contentType),
				Body:        file,
			})
			if err != nil {
				return fmt.Errorf("failed to upload %s: %w", directoryPath, err)
			}
		}
		return nil
	})
	if err != nil {
		return fmt.Errorf("failed to list files: %w", err)
	}
	return nil
}
slackWrapper.goでは、Playwrightでの実行結果をS3のURLを添えて、Slackに通知します。
slackWrapper.go
import (
	"context"
	"fmt"
	"github.com/slack-go/slack"
	"time"
)

var (
	Green = "#36a64f"
	Red   = "#e01e5a"
)

type slackWrapper struct {
	accessToken string // "xoxb-"から始まる"Bot User OAuth Token"
	channelId   string
}

func (s *slackWrapper) Notify(ctx context.Context, color string, title string, options map[string]string, s3 string) error {
	var fields []slack.AttachmentField
	for key, value := range options {
		fields = append(fields, slack.AttachmentField{
			Title: key,
			Value: value,
			Short: true,
		})
	}
	client := slack.New(s.accessToken)
	_, _, err := client.PostMessageContext(
		ctx,
		s.channelId,
		slack.MsgOptionBlocks(
			&slack.SectionBlock{
				Type: slack.MBTSection,
				Text: &slack.TextBlockObject{
					Type: slack.MarkdownType,
					Text: "テストシナリオの実行が完了しました",
				},
			},
			slack.NewDividerBlock(),
		),
		// テスト結果に応じた、色を指定して視覚的に伝わるよう設定します
		// また、Playwrightの実行結果が保存されたS3のURLを指定します
		slack.MsgOptionAttachments(
			slack.Attachment{
				Color:     color,
				Title:     title,
				TitleLink: s3,
				Fields:    fields,
				Footer:    time.Now().Format("2006/01/02 15:04:05"),
			},
		),
	)
	if err != nil {
		return fmt.Errorf("failed to post message to slack: %w", err)
	}
	return nil
}

AWS AppRunner上で実行されるswaggerのプロジェクト

AWS ECSのタスクを生成するコード

そして、swaggerプロジェクトでのコードの一部を紹介します。

ecsTask.goでは、AWS ECSのタスク定義に登録された定義をもとに、タスクを生成し実行します。
ecsTask.go
import (
	"context"
	"fmt"
	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/service/ecs"
	"github.com/aws/aws-sdk-go-v2/service/ecs/types"
)

type ecsTask struct {
	client            *ecs.Client
	cluster           string
	subnets           []string
	securityGroups    []string
	containerName     string
	taskDefinitionArn string
}

func (s *ecsTask) Run(ctx context.Context, cmdArgs []string) error {
	_, err := s.client.RunTask(ctx, &ecs.RunTaskInput{
		Cluster:        aws.String(s.cluster),
		TaskDefinition: aws.String(s.taskDefinitionArn),
		// 一つのテストシナリオを複数実行する必要がないため、タスクを1つのみとします
		Count:      aws.Int32(1),
		LaunchType: types.LaunchTypeFargate,
		Overrides: &types.TaskOverride{
			ContainerOverrides: []types.ContainerOverride{
				{
					Name:    aws.String(s.containerName),
					Command: cmdArgs,
				},
			},
		},
		NetworkConfiguration: &types.NetworkConfiguration{
			AwsvpcConfiguration: &types.AwsVpcConfiguration{
				Subnets:        s.subnets,
				SecurityGroups: s.securityGroups,
				AssignPublicIp: types.AssignPublicIpDisabled,
			},
		},
	})
	if err != nil {
		return fmt.Errorf("failed to run task: %w", err)
	}
	return nil
}
controlTaskPolicy.jsonでは、AWS Fargateでタスク生成に必要なポリシーを定義します。
controlTaskPolicy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ECSRunTaskPermissions",
      "Effect": "Allow",
      "Action": [
        "ecs:RunTask",
        "ecs:StopTask",
        "ecs:DescribeTasks",
        "ecs:ListTasks",
        "ecs:DescribeTaskDefinition"
      ],
      "Resource": [
        "arn:aws:ecs:ap-northeast-1:xxxxxxxxxxxx:task/hogehoge/*",
        "arn:aws:ecs:ap-northeast-1:xxxxxxxxxxxx:task-definition/foo:*",
        "arn:aws:ecs:ap-northeast-1:xxxxxxxxxxxx:cluster/piyopiyo"
      ]
    },
    {
      "Sid": "IAMPassRolePermission",
      "Effect": "Allow",
      "Action": "iam:PassRole",
      "Resource": "arn:aws:iam::xxxxxxxxxxxx:role/fugafuga"
    }
  ]
}

E2E自動テスト基盤での実行結果

構築したE2E自動テスト基盤を実行すると、以下の図のように、Playwrightが実行されSlackにテストの実行結果が通知されます。
Slackでの通知例

またSlackのリンクを踏むと、以下の図のように、テストの実行結果をS3から参照できます。
Playwrightの実行結果

新たなE2E自動テストツールを検証したことで得られたもの

E2E自動テストのパフォーマンス改善

実行環境改善による、パフォーマンス改善

E2E自動テスト基盤をAWS上に構築したことで、以下のパフォーマンス改善が見られました。

  • 費用
    • 年間利用料を99%以上削減
      • 以前のツールのサービス利用料と、今回採用したツールでのインフラ利用料を比較
      • 新規プロダクトへの開発や既存プロダクトへの改修にかかる工数も低減
  • 同時実行数
    • 同時にテストできるテストシナリオが、無制限に増加

E2E自動テストツール変更による、パフォーマンス改善

また、E2E自動テストツールにPlaywrightを採用したことで、以下のパフォーマンス改善が見られました。

  • テストのシナリオ数
    • 373件から829件と、2倍以上増加
  • テストの実行時間
    • 1/5ほどに高速化
  • テストの成功確率
    • 78.59%から99.03%と、成功確率が上昇

今回の結果を受けて、テストのシナリオ数が増えながらも、テストの実行時間が減少し成功確率が上がっていることから、既存のプロダクトに対するテストケースの拡充を進めながら、新規プロダクトに対するテストケースの追加を行っています。

おわりに

E2E自動テスト基盤の構築完了までの流れ

本プロジェクトは、プロジェクトを計画してから5ヶ月ほどで、テストシナリオの作成とE2E自動テスト基盤の構築を完了でき、運用できる状況となりました。
現在では、新E2E自動テストツールを利用することで、ロボアド事業や新規プロダクトのテストとして利活用しています。
本プロジェクトでは、以下の流れで、検証を完了できました。

年月 概要 詳細
2024年3月〜4月 Playwrightでテストシナリオのサンプルを作成 ロボアド事業で実施していたテストシナリオを数点、Playwrightで作成し、動作を検証した
2024年4月〜5月 AWS上にPlaywrightの実行基盤を構築 ウェルスナビのインフラチームに相談の上、ECS環境を構築し、PlaywrightのCI/CD環境を整備した
2024年5月〜7月 ロボアド事業のすべてのテストシナリオを作成 ロボアド事業すべてのテストシナリオをPlaywright上に作成し、不足していたケースも拡充した
2024年8月〜10月 他部署への展開と既存テストシナリオの運用保守 他部署にPlaywrightの開発方法や実行基盤を共有し、Playwrightのテストシナリオ作成を支援しつつ、プロダクト改修に伴う既存テストシナリオの修正を行った

OSSのE2E自動テストの基盤ができたことで得られた成果

今回新たにE2E自動テストの実行環境が、AWS上に構築されたことで、誰もが自由にテスト実行やテスト結果を閲覧できるようになりました。
そしてE2E自動テストを低コストで実行できるようになったのも、非常に大きな成果でした。
またE2E自動テストツールを選定する際には、品質保証の観点からテスト成功率に対して、十分留意する必要がある知見を改めて得ました。
QAチームの状況として、会社の急速な成長とともにプロダクト開発のスピードやボリュームが増え、以前のE2E自動テストツールでは対応が難しい課題が発生してきました。
新しいE2E自動テストツールは、外部環境の変更によるリスクを減らし、QAチームがこれからの開発案件への品質保証に対応できる体制へと改善できました。
今後は、E2E自動テストを提供できていないWebサイトに対して、テストケース、テストシナリオを拡充し、高品質なテストを高速で実行できるよう、さらなる改善を図っていきます。

著者プロフィール

木下 智弘(きのした ともひろ)

2015年10月にウェルスナビに、エンジニアとして入社。
プライベートでは、Google Cloudを好んで利用しています。
Goのシンプルな考えが好きです。

10
WealthNavi Engineering Blog

Discussion

ログインするとコメントできます