Paytner Tech Blog

ペイトナーのテックブログです

ペイトナー請求書のフロントエンドにスナップショットテストを導入した話

はじめに

ペイトナー請求書のフロントエンドを主に担当している @fuqda です。 本稿では、ペイトナー請求書のフロントエンドの品質改善の一つとして、最近実施したスナップショットテストの導入についてご紹介させて頂ければと思います。

この記事の対象読者

  • Vue.js / Nuxt.jsでJestのスナップショットテストを実装する方法について関心がある方
  • テストコードがないフロントエンドにこれから自動テストを導入しようとしている方

スナップショットテストとは?

スナップショットのテストはUI が予期せず変更されていないかを確かめるのに非常に有用なツールです。 https://jestjs.io/ja/docs/snapshot-testing

Jestの公式ドキュメントにも記載があるようにHTML要素の差分を検査し、違いがあれば失敗にするテストのことです。 なお、CSSのスタイル崩れなどの検知は出来ないので、そういった用途にはビジュアルリグレッションテストの導入を検討する必要がある点は注意しましょう。

導入前の状況

  • コンポーネントのテストが無いのでレビューコストが高い
    • 自動テストで担保すべき動作確認も手動で行う必要がある
  • ライブラリのアップデートや軽微な変更だと油断しているとデグレしてしまう
    • Nuxt.jsのバージョンアップでコンポーネントの自動読み込みの仕様が変わったことで、特定のコンポーネントが読み込まれなくなっていたが、気付けずリリースして障害を出してしまった...

コンポーネントのスナップショットテスト

※ Jestなど必要なパッケージはすでにインストール済みの前提で進めます
※ 実際のコンポーネントはもっと複雑ですが、あくまで例なのでご容赦ください

テスト対象のファイルの例

<template>
  <div>
    <p>{{ text }}</p>
  </div>
</template>

<script lang="ts">
import { defineComponent, ref } from "@nuxtjs/composition-api";

export default defineComponent({
  setup() {
    const text = ref('スナップショットテスト');

    return {
      text,
    }
  }
});
</script>

テストコードの例

import { shallowMount } from '@vue/test-utils'
import IndexPage from './index.vue';

let wrapper: ReturnType<typeof shallowMount<Vue>>;

describe("〇〇の場合", () => {
  beforeEach(() => {
    wrapper = shallowMount(IndexPage);
  });
  it("スナップショットテスト", () => {
    expect(wrapper.element).toMatchSnapshot();
  });
});

上記の例では、shallowMountしたコンポーネント対して、 toMatchSnapshot() を実行することで、キャプチャ結果と現在のHTMLを比較しています。

初回実行時は、テストファイルと同じ階層に __snapshots__/<テストファイル名>.snap のスナップショット結果ファイルが生成されます。 そのファイル作成後は、前回のキャプチャとの差分を元に差分があればテストが失敗するようになります。

差分がある場合は、以下のようにテストが失敗します。

$ npm run test pages/www/index.spec.ts

> www-frontend@1.0.0 test
> jest pages/www/index.spec.ts

 FAIL  pages/www/index.spec.ts
  〇〇の場合
    ✕ スナップショットテスト (8 ms)

  ● 〇〇の場合 › スナップショットテスト

    expect(received).toMatchSnapshot()

    Snapshot name: `〇〇の場合 スナップショットテスト 1`

    - Snapshot  - 1
    + Received  + 1

      <div>
        <p>
    -     スナップショットテスト
    +     スナップショットテストが失敗するテキスト
        </p>
      </div>

       9 |   });
      10 |   it("スナップショットテスト", () => {
    > 11 |     expect(wrapper.element).toMatchSnapshot();
         |                             ^
      12 |   });
      13 | });

      at Object.<anonymous> (pages/www/index.spec.ts:11:29)

スナップショット結果を更新したい場合は?

  • 差分を更新したい場合は、 npm test -- -u のコマンドで __snapshots__/<テストファイル名>.snap のファイルを更新することが出来ます!

スナップショット導入時にハマったこと

  • nuxt-svg-loaderを使ってSVG画像を描画している部分をJest実行時にレンダリング出来ない

解決法

image.png (173.5 kB)

const VueTemplateCompiler = require("vue-template-compiler");

module.exports.process = (svgSource, _) => {
  const result = VueTemplateCompiler.compileToFunctions(`${svgSource}`);

  return {
    code: `module.exports = { render: ${result.render} }`,
  };
};
module.exports = {
  // 中略
  transform: {
    // 中略
    "^.+\\.svg$": "<rootDir>/svgTransform.ts",
  },

導入後の状況

CIでテストがパスしなければfailするようになったので以前より表示関連のバグを検知しやすくなりました。 以前はCIで自動テストが動いてなかったのもあり、GitHubのPRを開いてCIが落ちていればデグレに気付けるだけでだいぶ動作確認コストは減ったように思います。

jest-ci-result.png (72.0 kB)

終わりに

意図しない変更差分によるデグレをお手軽に検知するための第一歩としてスナップショットはおすすめです。 今回はテストの対象にNuxt.jsのコンポーネントを取り上げましたが、スナップショットテストはReactなどのその他のフレームワークでも活用出来ます。 まだテストコードが無くて何からテストを始めていいかわからないという方も、手始めにスナップショットテストから始めてみてはいかがでしょうか。