LoginSignup
1
1

モノレポ環境でVisual Regression Testを導入

Posted at

はじめに

株式会社MG-DXでWebフロントエンド兼Flutterアプリエンジニアを業務を行なっています。自社のサービスとして、医療機関、薬局向けに薬急便のサービスを提供しています。

薬急便では複数のWebサービスを提供していますが、一つのリポジトリで開発して、いわゆるモノレポ環境になっています。packages配下に各サービスと共通のUIコンポーネントを置いた構成です。

└── packages
    ├── pharmacy
    ├── admin
    ├── client
    └── ui 

UIのテストは、HTMLのスナップショットテストは入っています。しかし、CSSは比較対象にならないので、レイアウトとして問題ないかがわかりません。そこで、テスト環境を強化することになり、Visual Regression Testを導入しました。

この記事で説明する内容

  • storybookを利用したスナップショット撮影
  • モノレポ環境でのVisual Regression Test

薬急便自体は、Gatsbyで構築しているのですが、今回はサンプルとしてはNext.jsで用意しました。ベースのモノレポ環境があるところから説明します。

ビルドしたstorybookでスナップショット撮影

用意したリポジトリは、最低限のページとコンポーネントで構成されています。今回は、storybookが用意されているものに対して、テスト行うように構築します。

今回、スナップショットの撮影では、playwrightを使います。流れとしては、

  1. storybookをビルド
  2. ビルドされた設定ファイルを読み取り、パラレルに撮影
  3. 特定のディレクトに画像を出力

までを実装します。

storybookをビルドしている理由ですが、storybookをすべてでスナップショットを撮ると、動作確認用に状態ごとを定義しているのもあり、それを全部出力するのはちょっと多すぎるので、ビルド時に書き出したJSONを使って、間引くことができるようにします。

基本はDefaultの名前でstorybookを書き出しているので、その内容だけを出力するようにします。

playwrightの設定

テストを実行する際に、他のテストを無視して、スナップショットの撮影だけのテストにしたいので、ConfigでVisual Regression Testのディレクトリだけを実行するようにします。

import type { PlaywrightTestConfig } from "@playwright/test";

const config: PlaywrightTestConfig = {
  timeout: 30000,
  globalTimeout: 600000,
  testDir: "./snapshot",
  snapshotDir: "./snapshot/__snapshots__",
  outputDir: "./snapshot/__output__",
  use: {
    viewport: { width: 720, height: 900 },
  },
  reporter: process.env.CI ? "line" : [["html", { outputFolder: "./snapshot/__report__" }]],
};

このサンプルではuiとwebがあるので、外部のlibs/playwrightを作って、共通で読み込んで処理しています。パッケージ内の./snapshotにテストを書いて、その中に結果を保存します。

storybookのビルド

package.jsonにビルドのscriptを追加します。

package.json
{
  "scripts": {
    "vrtest-setup": "storybook build --quiet --output-dir ./build"
  }
}

テストファイルの追加

ビルドされたファイルを読み取って、playwrightを使ってスナップショット撮影を行うテストを追加します。このとき、Story名を見て、Defaultだけを書き出すようにしています。

snapshot.test.ts
import { expect, test } from "@playwright/test";
import { readFileSync } from "fs";
import { resolve } from "path";

const storybookDir = resolve(__dirname, "..", "build");
const data: any = JSON.parse(readFileSync(resolve(storybookDir, "stories.json")).toString());

test.describe.configure({ mode: "parallel" });

const items = Object.values(data.stories);
items.forEach(async (story: any) => {
  test(`snapshot test ${story.title}: ${story.name}`, async ({ page }) => {
    if (story.name.match(/Default/)) {
      await page.goto(`http://127.0.0.1:3001/iframe.html?id=${story.id}&viewMode=story`, { waitUntil: "networkidle" });
      const image = await page.screenshot({ fullPage: true });
      expect(image, {}).toMatchSnapshot([story.title, `${story.id}.png`]);
    } else {
      test.skip();
    }
  });
});

また、通常のスナップショットテストで動かないようにsnapshotをjestの設定から外します。

config.js
{
  testPathIgnorePatterns: ["/node_modules/", "<rootDir>/snapshot"],
}

起動設定の追加

記事を参考にしました。最初はローカルファイル参照で起動していたんですが、CORSが出たので、普通にhttp-serverを起動する形にしました。

package.json
{
  "scripts": {
    "vrtest-server": "http-server ./build -p 3001",
    "vrtest-snapshot": "start-server-and-test vrtest-server http://127.0.0.1:3001 'yarn playwright test --update-snapshots'"
  }
}

これで実行すると、snapshot配下にstorybookの内容を撮影したものが保存されるようになり、Visual Regression Testの準備ができました。

reg-suitを使ってVisual Regression Testを行う

画像の比較には、reg-suitを使います。今回はモノレポ環境でreg-suitを行うので、保存場所もそれぞれで保存されるようにします。

基本的な設定は、よくある設定なのですが、keyの比較は独自に処理するようにします。

regconfig.json
{
    "core": {
      "actualDir": "./snapshot/__snapshots__",
      "thresholdRate": 0,
      "addIgnore": false,
      "ximgdiff": {
        "invocationType": "client"
      }
    },
    "plugins": {
      "reg-simple-keygen-plugin": {
        "expectedKey": "${EXPECTED_KEY}",
        "actualKey": "${ACTUAL_KEY}"
      },
      "reg-notify-github-plugin": {
        "clientId": "${REG_NOTICE_CLIENT_ID}",
        "prComment": true,
        "prCommentBehavior": "new",
        "setCommitStatus": true,
        "shortDescription": true
      },
      "reg-publish-s3-plugin": {
        "bucketName": "${REG_S3_BUCKET_NAME}",
        "pathPrefix": "web"
      }
    }
  }
  

キャッシュキーの比較方法

通常であれば、reg-keygen-git-hash-pluginを使えばいいんですが、今回は、スナップショットの撮影は該当のpackageに影響がある場合だけにします。このため、githubの履歴からだとキーがマッチしない場合があるので、使えません。

キーの計算は、Github ActionsのhashFilesを使って、指定するようにします。PR先を落として、影響範囲があるファイルをhashFilesで指定してキーを作成し、その後本来のPR元のキーを落として、スナップショットのテストをします。これをreg-suitに渡るようにすれば、対象のスナップショットを参照してくれます。

visual-regression-test-ui.yaml
      - name: set up base repository
        uses: actions/checkout@v3
        with:
          ref: ${{ github.event.pull_request.base.ref }}
      - name: set base env
        run: |
          echo "EXPECTED_KEY=$KEY" >> $GITHUB_ENV
        env:
          KEY: ${{ hashFiles('libs/**', 'package.json', 'packages/ui/**', 'yarn.lock') }}
      - name: set up head repository
        uses: actions/checkout@v3
        with:
          ref: ${{ github.event.pull_request.head.ref }}
      - name: set head env
        run: |
          echo "ACTUAL_KEY=$KEY" >> $GITHUB_ENV
        env:
          KEY: ${{ hashFiles('libs/**', 'package.json', 'packages/ui/**', 'yarn.lock') }}

おまけ

Github ActionsでのVisual Regression Test

GitHub Actions上でテストを行う場合、pull_requestとpush時にそれぞれ実行するように指定します。こうすることで、差分が出た時に、PRのapprovedでredからgreenになり、pushされると更新されます。

visual-regression-test-ui.yaml
name: visual regression test

on:
  pull_request:
    branches:
      - main
    paths:
      - libs/**
      - package.json
      - packages/ui/**
      - yarn.lock

  push:
    branches:
      - master
    paths:
      - libs/**
      - package.json
      - packages/ui/**
      - yarn.lock

jestの書き出し処理

jestのスナップショットもこのサンプルでは行なっているのですが、Fakeを無視するようにしてます。OpenAPIをorvalを使って書き出しているのですが、MSW用で書き出されたfakeのモデルを返すのを割り当ててランダムで変わってしまうので、無視するようにします。

jest/index.ts
import { composeStories, StoryFile } from "@storybook/testing-react";
import { render } from "@testing-library/react";
import React from "react";

export const renderFromStories = (stories: StoryFile) => {
  const composed = composeStories(stories);
  const testCases = Object.values(composed).map((Story: any) => [Story.storyName, Story, Story.args]);
  test.each(testCases)("renders %s", (_storyName, Component: any, args) => {
    if (_storyName.match(/Fake/)) return;

    const tree = render(<Component {...(args || {})} />);
    expect(tree.baseElement).toMatchSnapshot();
  });
};

最後に

今回の例は、PR単位で確認できるようにしていますが、実際のサービスでは、複数立ち上がっている中、Visual Regression Testはそれなりに時間がかかって、コストになるので、releaseとmainの1日1回の差分だけのテストにして、更新対象範囲がわかるだけでもいいかなと思っています。

Next.JSのサンプルとしても使えるようにしたいなと思って、サンプル環境を作るのにすごい時間がかかってしまいました。そこそこ、動くように作っているので、参考になれば幸いです。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1