スクショをベースにするスナップショットテスト

目的

CSSやJSコードをいじった後、見た目の差分を確認できます。

Reactの場合、いっぱい使われている共通のコンポーネント修正した後、既存のコンポーネントへの影響を確認することができます。

必要なパッケージ

テストのカバーするページの定義

/routes/Test.tsx

interface RouteData {
  // ページ名
  name: string;
  // ページpath
  path: string;
  // ページ表示用のテストContainer Component
  component: React.SFC<{}> | React.ComponentClass<RouteComponentProps<any>>;
  // 横画面のスクショ比較が必要かどうか
  isHorizontalTestNeeded?: boolean;
}

// テスト実行時用データ
export const testRouteDatas: RouteData[] = [
  {
    name: "enzaメンバー登録ページ",
    path: "/register",
    component: UserSimpleRegisterPage,
    isHorizontalTestNeeded: true,
  },
  {
    name: "エントランスページ",
    path: "/entrance",
    component: EntrancePage,
    isHorizontalTestNeeded: true,
  },
  {
    name: "ゲームポータルトップページ",
    path: "/game",
    component: TopPage,
  },
  // ...
];

// テスト用のpathを作る
export const generateTestPath = (path: string) => (
  `/snapshotTest${path}`
);

// Route情報データ
export const testRoute = testRouteDatas.map((data) => (
  <Route
    path={generateTestPath(data.path)}
    key={generateTestPath(data.path)}
    component={data.component}
    exact={true}
  />
));

全体Route定義

App.tsx

import { testRoute } from "routes/Test";
// ...
<Switch location={location}>
  {/* develop環境だけテストのRouteにアクセスできる */}
  {ENV === "develop" ? testRoute : null}
</Switch>
// ...

スナップショットテスト定義

test/snapshot.ts

import * as puppeteer from "puppeteer";
import { generateTestPath, testRouteDatas } from "routes/Test";
import * as QueryString from "query-string";

let browser;

// Headless Chrome Browser を立ち上げ
beforeAll(async () => {
  browser = await puppeteer.launch({
    args: ["--no-sandbox", "--disable-setuid-sandbox"]
  });
});

testRouteDatas.forEach(async (routeData) => {
  const testUrl = `http://develop.env.host.name/${
    generateTestPath(routeData.path)
  }`;

  it(routeData.name, async () => {
    const page = await browser.newPage();
    page.setViewport({
      width: 360,
      height: 640,
    });
    await page.goto(testUrl);
    // 縦画面のページスクショ
    const image = await page.screenshot({ fullPage: true });
    // 現存のスクショと比較
    expect(image).toMatchImageSnapshot();
  });

  if (routeData.isHorizontalTestNeeded) {
    it(`横画面-${routeData.name}`, async () => {
      const page = await browser.newPage();
      page.setViewport({
        width: 640,
        height: 360,
      });
      await page.goto(testUrl);
      // 縦画面のページスクショ
      const image = await page.screenshot({ fullPage: true });
      // 現存のスクショと比較
      expect(image).toMatchImageSnapshot();
    });
  }
});

// Headless Chrome Browser を閉じる
afterAll(async () => {
  await browser.close();
});

jestでtoMatchImageSnapshotメソッドを使えるように

jest-setup.ts

import { toMatchImageSnapshot } from "jest-image-snapshot";

expect.extend({ toMatchImageSnapshot });

コマンド追加

package.json

{
  "scripts": {
    "test:snapshot": "jest /test/snapshot.ts",
    "test:snapshot:update": "jest /test/snapshot.ts --updateSnapshot"
  }
}

使い方

yarn test:snapshot

でテストを実行します。

差分が検出されたら、下記のようなファイルが作成されます。

f:id:cliffzhao:20180619184700p:plain

差分問題なかったら、下記のコマンドでスナップショットを更新します。

yarn test:snapshot:update

Todo

実はpuppeteerでクリックやキーボード入力などイベントを発火できます。

動作を定義できたらもっと前面にテストすることが可能だと思います。