for Startups Tech blog

このブログのデザインを刷新しました。(2023/12/26)

E2Eテストを書くことで変更失敗率を下げつつデプロイ回数を上げたい

f:id:forStartups:20220107185001j:plain

こんにちは。エンジニアの藤井(@yutafujii)です。 社内向けのプロダクト「タレントエージェンシー支援システム(SFA/CRM)」のエンジニアをしています。

今回は安心して開発に専念できるようE2Eテストを記述した話をさせていただきます。 アプリケーションはVue/Railsで動いておりCIはCircleCI、テストツールはPlaywrightを用いました。

変更失敗率とかデプロイ回数の話

つい先日t_wadaさんがパフォーマンス指標についてTweetしておりましたが、チームでも直近デプロイ回数を意識した方がよいのではという話になりました。

私が入社した2年前は、PullRequest(PR)はレビューを通ったら都度デプロイしてましたが、1年前に方針を変えていたんですよね。 その時は「なんか自分たちってぬるっとリリースしてるよね」という意見があって、アウトカムかどうかは別としてアウトプットは一定程度まとめて定期的なリズムを作ってリリースしてみましょうか、という話になっていました。

その後1年間は、毎週月曜日をリリース日と定め、前週の金曜日にリリース物をstaging環境に載っけて致命的なエラーがないかを動作確認するという運用を行いました。 その際、根幹となるユーザーストーリーを手動で1つ1つ実行してみて、エラーが起きないかをチェックしていました

f:id:forStartups:20211221134504p:plain
git flowに近い開発をしていた

当時の目的も時とともに薄れていき、あるスプリントの振り返りで「PRレビューが終わったものは都度リリースしてはどうか」という話が出ました。 しかしながらそこでは同時に

「都度すぐにリリースされるのは少し不安」

「コードレビューだけやっておいて金曜日のstagingの動作確認をもってして”リリースできる”か確認する感覚になっている」

という意見もチームから出ました。

これってよくよく考えると、自分たちは「ソフトウェアの変更を常にテストして自動で本番環境にリリース可能な状態にしておく」というCI/CDの目的を本質的に満たせていない状態に陥っていたとも言えます。

じゃあ改善しようという点にみんな異論はなかったのですが、 一方でコードベースも大きくなっていたこともあり、 PRをレビューするたびに根幹となるユーザーストーリーを手動で1個1個チェックするのも負担感が強くなっていました。

そこで、アプリケーションの根幹となるユーザーストーリーに対してE2E(End to End)テストを作成することにしました。

E2Eテストとは

E2Eテストは実際の動作の一連のフローに着目し想定通りの結果となるかを確認するもので、特徴として以下のような点が挙げられます。

  • 複数の処理を一連のフローとして実行して想定通りの結果になるか確認する
  • ブラウザを用いる(実際の動作がブラウザで行われることから)
  • production環境に対してまたは同等の環境に実行するのが理想的
  • 様々なデバイスでのテストで実行するのが理想的

今回私が実装したテストはこれら全てを実現してるわけではないのですが、ブラウザを利用し一連の処理をテストするという点で一定の目的を達成したと考えます。

ちなみにソフトウェアのテストをUnitテスト・Integrationテスト・E2E(End to End)テストの3つのレイヤに分けて、ピラミッド状にテストケースの数を構成するテストピラミッドという考え方があります。

f:id:forStartups:20211221143046p:plain
テストピラミッドの種類
出所)freeCodeCamp

Unitテストは実行時間が短い代わりに検証範囲が限定的で、 E2Eテストは実行時間が長い代わりに広範囲の検証を行えるというトレードオフがあり、 それぞれのレイヤのテスト数をどうバランスするかという論点への一つの考え方がテストピラミッドです。 私たちの場合は図左側の順三角形型(Unitテスト数が多く・E2Eテスト数は少なめ)のバランスを目指しました。

Playwrightを利用する

f:id:forStartups:20211221183823p:plain
Playwright LPより

E2Eテストのライブラリは以下の記事を参考にしながらPlaywrightを選定しました。軽量で速い・信頼度が高い(SeleniumやPuppeteerに比べて処理手順の安全な実行が可能)という点を評価して選定しています。

Playwrightはauto-waitsにより、安全な処理手順の実行が可能になっている →例えば、Puppeteerの場合だと待機時間を手動で設定しておく必要があるなど、 E2Eの一番のネックとなる部分がPlaywrightで抽象化されたので嬉しい。

PlaywrightでフロントエンドのE2Eテストを自動化してみた話

テストの設計

今回のE2Eテストではアプリケーションの根幹となるユーザーストーリーに焦点を絞ります。 プロダクトはCRMに分類されるアプリケーションなのですが、SaaSに例えるなら「リードを獲得するところから、顧客の収益化まで」にアプリケーションで行われる基本ケースの処理をすべてフローで実施して結果をテストします。

以下のフローを実行するようなイメージです(SaaSに例えた場合)

  • トップ画面を開く
  • 獲得リードの入力
  • リードナーチャリングで送信した情報の入力
  • 活性化した顧客の商談入力・ステータスを進捗させる
  • 受注処理

また、都度のCIビルドでE2Eテストを実行するのはテスト結果の示唆が待ち時間に見合わないと判断し、staging環境へ載っける準備ができた段階のソースコードに対してのみテストが実行されるようにします

テストの実装

Playwrightパッケージをレポジトリに追加し、テストコードを記述していきます。

$ yarn add @playwright/test playwright

テストを <application root>/e2e 配下に配置していきます

// e2e/test.spec.ts

import { test, expect } from '@playwright/test'

const config = require('./env.json')

test.describe('MyApp', () => {

  // テスト毎にトップページへの遷移まで終えておく
  test.beforeEach(async ({ page }: any) => {
    // ログイン画面
    await page.goto(`${config.baseUrl}/`)

    // トップページ画面を表示していることを確認
    expect(await page.waitForSelector('text=MyApp')).toBeTruthy()
    await page.screenshot({
      path: 'e2e/screenshots/top.png',
      fullPage: true,
    })
  })


  test('基本フロー1', async ({ page }: any) => {
    await page.click('text="あああ"')
    expect(await page.waitForSelector('text="あああ一覧"')).toBeTruthy()
    // ... 15手順ほど続く
  }

  test('基本フロー2', async ({ page }: any) => {
    await page.click('text="あああ"')
    expect(await page.waitForSelector('text="あああ一覧"')).toBeTruthy()
    // ... 40手順ほど続く
  }
}

テストを書き始めるときはPlaywrightのTest Generator機能を使って実際にテストスコープとなるフローを手動で実行し、自動で書き起こされたテストコード利用すると便利です。

# Generator 起動コマンド
$ yarn playwright codegen <your_site_url>

E2Eテストの実行コマンドをpackage.jsonスクリプトに記載しておいて、CIコンテナでの実行時の呼び出しコマンドとしておきます。

  "scripts": {
    "test:e2e": "npx playwright test e2e/",
  },
# テスト実行コマンド
$ yarn test:e2e

E2EテストのCIへの導入

作成したE2Eテストは以下のようにCircleCIの中で実行されるようにします:

  • CircleCIに追加のジョブを作成
    • staging環境だけ実行されるように設定
  • ジョブ
    • 必要なライブラリのインストール・データベース構築
    • サーバーを起動
    • E2Eテスト

新しく追加するパイプラインの設定箇所

# .circleci/config.yml

workflows:
  main:
    jobs:
      - build:
          context: xxx
      - e2e:
          filters:
            branches:
              only: /^staging.*/
          requires:
            - build

ジョブのConfigurationはこのような感じです。

jobs:
  build:
    (省略)
  e2e:
    working_directory: ~/<your_github_organization>/<repository>
    shell: /bin/bash --login
    environment:
      CIRCLE_ARTIFACTS: /tmp/circleci-artifacts
    parallelism: 1
    docker:
       - image: circleci/ruby:2.7.x-node-browsers
         environment:
           RAILS_ENV: development
           DB_HOST: 127.0.0.1
           DB_USERNAME: 'xxxxxxxx'
           DB_PASSWORD: 'xxxxxxxx'
       - image: circleci/mysql:5.7.x
         command:
           mysqld --sql-mode=NO_ENGINE_SUBSTITUTION
       - image: redis
         name: redis
    steps:
    - checkout

E2Eテスト実行までの各ステップはこんな感じ

# パッケージやライブラリのインストール

    - run:
        name: Bundle Install
        command: bundle check --path=vendor/bundle || bundle install --jobs=4 --retry=3 --path vendor/bundle
    - run:
        name: Yarn Install
        command: yarn install --cache-folder ~/.cache/yarn
    - run:
        name: Elasticsearch install
        command: |
          wget -O ~/elasticsearch-6.5.x.tar.gz https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.5.x.tar.gz && \
          tar -xvf ~/elasticsearch-6.5.x.tar.gz -C ~/ && \
          if [ -z "`~/elasticsearch-6.5.x/bin/elasticsearch-plugin list | grep analysis-kuromoji`" ]; then \
          ~/elasticsearch-6.5.x/bin/elasticsearch-plugin install analysis-kuromoji; fi
# サーバー立ち上げのために必要な準備

    - run:
        name: set up database
        command: bin/rails db:create db:migrate RAILS_ENV=development
    - run:
        name: set up elasticsearch
        command: |
          ~/elasticsearch-6.5.x/bin/elasticsearch -d
          sleep 5
          bundle exec rake elasticsearch:setup:index  # Elasticsearchの全インデックス作成タスク
    - run:
        name: seeding
        command: |
          ~/elasticsearch-6.5.x/bin/elasticsearch -d
          bundle exec bin/delayed_job start
          bin/rails db:seed_fu RAILS_ENV=development
          bundle exec rake seeding:candidates[2]
    - run:
        name: put env.json
        command: |
          cat \<<-TEXT > e2e/env.json
          {
            "baseUrl": "http://localhost:3000"  # テストしたいドメインを与える
          }
          TEXT
# E2E test!
    - run:
        name: E2E test
        command: |
          ~/elasticsearch-6.5.x/bin/elasticsearch -d
          bundle exec bin/delayed_job start
          bin/rails s -d
          curl localhost:3000 > /dev/null 2>&1
          yarn test:e2e

ここまで書いたらブランチ名を staging/foo とかにしてGitHubにpushすると、、

f:id:forStartups:20211221163537p:plain

実行されました!

E2Eを導入してみて

承認されたPRを都度リリースすることで1週間のリリース回数は大幅に増え、作ったものがすぐユーザーに届く開発体験の良さを取り戻せました。

f:id:forStartups:20211221140957p:plain
週間リリース回数

E2Eテストが稼働していることで、これまで手動で行なっていた「根幹機能の動作確認」も不要になり、開発効率も間違いなく上がっていると思います。

E2Eテストを導入してから2ヶ月くらい経ちますが、実際にE2Eが失敗してソースコードのバグに気づけたこともあり効果を実感しています。

これからもユーザーに価値を素早く届け、かつ良質な開発体験が得られるような工夫を続けていきます

We are Hiring!

f:id:forStartups:20211020192743p:plain フォースタートアップスでは共に働く仲間を募集中です。本記事を読んで興味を持っていただけましたら採用情報をご覧ください。