ウェブサイトのフロントエンドのテスト

「Jamstack」について研究・調査を始めた時から、「APIからデータを取得して静的ファイルを生成するだけでなくどのような課題を改善・解決しようとしているのだろう」と私は考えてきました。その答えの1つに今回取り上げる「フロントエンドのテスト」が入るでしょう。「Why Use the Jamstack? | Jamstack」では直接テストについて言及されてはいないものの、「Developer Experience」の中に入っているかと思われます。「コンポーネント化」が気になることも言い続けていましたが、今回取り上げるテストにも関連がありました。

ウェブページ・ウェブサイトのリリース時には実装者による自己検品、ディレクターによるチェック・クライアントによるチェックを経て公開へと至るでしょう。しかし、ウェブサイトの運用ではタスク完了時点でコンテンツが正しいことだけでなく、検品・チェックに合格した状態を保ち続けることが重要だと考えます。テスティングフレームワークを利用してフロントエンドのテストができることは、検品・チェックに合格した状態を保つことを強くサポートしてくれるでしょう。

研究にあたり、当サイトではNext.jsを使用しているため「Testing | Next.js」を参照しました。また、「React コンポーネントのテストを書く時に考えることと、テストコードサンプル ++ Gaji-Laboブログ」などを参考にしました。結果、「Cypress」を使用することでE2Eテストが、また「Jest」を利用することでスナップショットテストやユニットテストが行えることが分かりました。

例1:ユニットテスト

当サイトのヘッダーでサイト名を表示している<SiteName />コンポーネントについてテストを書いてみました。このコンポーネントはサイトのホームページではh1要素で、それ以外のページではdiv要素でHTMLが出力されるようになっています。
当サイトのヘッダーでサイト名を表示している部分のキャプチャ

まず以下がコンポーネントのコードです。単にh1divかを切り替えているだけです。(MUIが上手く使えているか疑問ですしもっとよいclass名とか考えたいのですが、Jamstackの研究・調査が当サイトの第一義なのでひとまず目をつぶってください。)

import Typography from '@mui/material/Typography';
import Link from 'next/link';

export default function SiteName({ isHome }) {
  return (
    <Typography
      variant="h5"
      component={isHome ? 'h1' : 'div'}
      className="sitename"
    >
      <Link href="/">
        <a>
          PowerCMS X R&amp;D Website
        </a>
      </Link>
    </Typography>
  )
}

以下はユニットテストのテストコードです。コンポーネントのプロパティisHometrueまたはfalseを指定して挙動をテストします。

import React from 'react';
import { shallow } from 'enzyme';
import SiteName from '../components/SiteName';

describe('<SiteName />', () => {
  it('サイトホームではh1であること', () => {
    const wrapper = shallow(
      <SiteName isHome={true} />
    );
    expect(wrapper.html()).toContain('<h1');
  });

  it('サイトホーム以外ではh1でないこと', () => {
    const wrapper = shallow(
      <SiteName isHome={false} />
    );
    expect(wrapper.html()).not.toContain('<h1');
  });
});

このテストを実行するとPASSするのでコンポーネントは意図通り動作することが確認できます。ただ、モックを利用してRouterを操作するなどしisHometrueないしfalseを渡した方が良いと思うので、今後「テストのレシピ集 – React」なども確認してモックについて知識を深めていきたいと考えています。
SiteNameコンポーネントのテストコードをJestでテストしたターミナルの画面

PowerCMS Xのテンプレートだと<mt:setvar name="is_home" value="1" />のようなコードを利用して切り替えると思いますが、意図せずis_homeが上書きされて壊れそうな予感もします。しかし、先のようなコンポーネントとテストを使えば壊れることを防ぐことは容易でしょう。(PAML3でインライン変数が利用できることは書き添えておきます。)

例2:スナップショットテストとユニットテスト

次にサイトホームでブログ記事を表示している<EntryCard />コンポーネントについてテストを書いてみました。このコンポーネントはAPIからのデータに応じて画像・記事タイトル・記事の概要を表示します。
当サイトのホームページでブログ記事を表示しているコンポーネントのキャプチャ

APIからのデータを基に記事が正しく表示されることは目視で確認済のため、現在の状態をスナップショットで取得しておき、予期しないUIの変更が発生していないかを確かめる「スナップショットテスト」を採用しました。また、画像を指定した場合に場合に指定した画像がimg要素のsrc属性にセットされるかを「ユニットテスト」で確認しました。コンポーネントにはダミーの記事データを作成して渡しています。

import React from 'react';
import { render } from 'enzyme';
import renderer from 'react-test-renderer';
import EntryCard from '../components/EntryCard';

describe('<EntryCard />', () => {
  it('レンダリングが正しいこと', () => {
    const entry = {
      id      : 1,
      basename: 'test',
      title   : 'テスト記事',
      excerpt : 'EntryCardコンポーネントのテストです',
    };
    const tree = renderer.create(<EntryCard entry={entry} />).toJSON();
    expect(tree).toMatchSnapshot();
  });

  it('指定した画像が表示されること', () => {
    const entry = {
      id        : 1,
      basename  : 'test',
      title     : 'テスト記事',
      excerpt   : 'EntryCardコンポーネントのテストです',
      card_image: {
        Permalink: 'https://powercmsx-dummy-url.cms.anothersky.pw/site1/assets/test.jpg',
      },
    };
    const expectSrc = 'https://assets.rd.powercmsx.anothersky.jp/site1/assets/test.jpg';
    const wrapper = render(<EntryCard entry={entry} />);
    expect(wrapper.find('img').prop('src')).toBe(expectSrc);
  });
});

このテストも無事にパスしました。もし<EntryCard />に変更が起こると、以下のようにスナップショットテストが通らなくなり、差分が表示されます。
スナップショットテストが通らなかったターミナルの画面

GitHub Actionsで継続的にテストを実行する

ここまではローカル環境でnpm testコマンドを実行してテスト結果を確認しました。しかし、テストの実行を忘れてしまえば目視でウェブページを確認していた頃となんら変わらなくなってしまいます。そこで、「GitHub Actions」を利用して継続的インテグレーション…つまりコードをリポジトリにPushした時に自動でテストを行うようにしました。

Pushする度にGitHub Actionsでテストが実行され、以下のように結果が表示されます。失敗時にはメールでも通知がありました。
GitHub Actionsで全てのワークフローを表示した画面

GitHub上で実行されたテストの詳細を開くと、ローカル環境でテストを行う時同様npm ciコマンドやnpm testコマンドが実行されていることが分かります。
GitHub Actionsで全てのワークフローを表示した画面

継続的インテグレーションを採用することにより確実にテストが実行できるので、ウェブサイトの品質維持に役立つことは間違いないでしょう。もちろん予めテストを書いておくのが前提条件です。

まとめ

このようにユニットテスト・スナップショットテストを用いてコンポーネントを容易にテストできることが分かりました。CMSのテンプレートではコンポーネント単位でテストをしたり、常に同一のダミーデータを流し込んだりすることは難しく目視確認するしかないと思われるため(もしくはCypress等を利用して都度サイトにアクセスして確認する?)、フロントエンドの出力にNext.js(React)やNuxt.js(Vue.js)を採用しテスティングフレームワークを併用する手法は大きなメリットの一つになると感じました。言うなれば「平成のフロントエンド実装」と「令和のフロントエンド実装」でしょうか。(10年ぐらい前に「Jenkins」でビルド・テストをしたことがあり「令和の」は言い過ぎかもしれませんが。)未だに手作業・目視作業に頼りがちなプロジェクトもあると思いますが、フロントエンド・エンジニアをはじめ関係各位の知識をアップデートしよりよい手法を取り入れていくことが重要だと考えます。他の分野・業界ではAIやDXというキーワードの下、業務の対応方法などが大きく変化していますよね。

実際にコードを書いて検証したことで、私はJamstackを採用するメリットの1つである「継続的インテグレーションが取り入れやすいこと」をよく理解できたように思います。今後さらにReactやE2Eテストも含めた各種テストについて知見を深めていきたいと考えています。