# Nuxtの自動テスト環境を構築しました(1)

今回、僕が関わっているNuxtのプロジェクトに後付けで自動テストを導入しました。

巷では自動テストは必須だの必要だのとよく言われる割に、 ベストプラクティスというのが欠けている感じがして環境の構築にかなり苦労しました。

今回、Nuxtのプロジェクトに広く使えそうな手法を見つけましたので、 同じように悩んでいるみなさんの助けになればと思います。

TIP

記事作成にあたり、説明が長くなってしまったため記事を分けました。 今回はテストを作成する前段階の、方針決定から技術選定までのお話しです。

# テスト対象と方針

Nuxtでは、Vueコンポーネントを配置するディレクトリによって、 ページコンポーネント に分かれています。 それぞれ役割が明確に違うため、テスト対象は自ずと決まってきます。

  • ページはいくつかのコンポーネントを組み合わせ、APIのコールや状態の管理など複合的に動作するため、 機能テスト で検証します。
  • コンポーネントは単体で動作するように作り、APIのコールなどは含みません。そのため ユニットテスト で検証が可能です。

# ■ テストの作成方法

テストの作成方法ですが、 シナリオ主導型ファイル対応型 のパターンがあります。(どちらも僕が勝手に付けた造語です)

シナリオ主導型「トップページから入ってきたユーザーが、商品ページを開いて商品をカートに入れる」 といったユーザーの一連の行動をテストに書き起こしたもので、 ページやファイルと言ったプログラムの都合ではなく、ユーザー視点から見たテストが行えるのが特徴です。

これは主にV字開発モデルでいう 受け入れテスト に相当します。


ファイル対応型 は1ファイル(コンポーネント)に対して1つのテストファイルを作成し、メソッドや機能を網羅するようにテストを作成していきます。 条件を網羅して確認をおこなうため、単純な計算ミスや文言の間違いといった見落としがちな部分を重点的に確認するのに向いています。

これは 単体(ユニット)テスト結合(機能)テスト に当たるものです。


今回のテスト作成にあたり、僕が重点的に確認したかったのは 「外部から入場してきたユーザーが無事に商品購入まで辿り着くこと」 ではなく、 「機能追加によって昨日まで動いていたコードが動かなくなっていないか」といった、 今回の変更でのデグレが起きていないこと でした。

これは シナリオ主導型 のテストではチェックの漏れが起きやすく、条件を網羅しようとすればメンテナンスの工数が膨大になってしまいます。

また今まで見てきた シナリオ主導型 のテストは 「このシナリオ、どのテストファイルに記述すればいいんだっけ?」 とか、 「よく見たらテスト重複してるじゃーん」 のように、ファイルの置き場所に起因する混乱が生じやすいと感じました。


そのため、今回はシナリオ主導型のテストコードは採用せず、 ファイル対応型 のユニットテスト、機能テストを採用することにしました。

デグレを防ぐ目的のテストであれば、通常はこの方針を選択しておけば間違いないと思います。

TIP

ブラウザを利用してテストを行う E2Eテスト は、 シナリオ主導型 で記述するのが一般的なようです。 ですが 一般に倣って本当に実現したいことがブレてしまう のは本末転倒。

「そのプロジェクトにおいて何をテストしたいのか」を常に念頭に置いてテスト設計を行うことを推奨します。

# テストに利用されるライブラリ・ツール

さて、方針が定まったところで技術選定です。

ですがその前に、テスト用のツールの中にはいくつか専門的な用語が出てきますのでご紹介しておきたいと思います。

# ■ テストランナー

テストを実行する際の本体となるライブラリです。

  • テストコードを実行する
  • テストをファイル・ページ・検証内容といった単位で分割し、実行順序をコントロールする
  • アサーション(検証)を実施する
  • 検証結果を集計し、カバレッジを表示する

これらテストの基本となる機能を揃えたツールが テストランナー で、 多機能なものからシンプルなものまで様々な種類のライブラリが存在し、 技術選定で最も頭を悩ませる部分 でもあります。

代表的なライブラリに jest mocha jasmine ava などがありますが、 選択肢が多すぎて選べない人のためにあえて断定するなら、 多機能なjest単機能なava のどちらかを選んでおけば間違いないと思います。

# ■ モック

例えばコンポーネントのテストをする際、 Vuexなど外部への依存があるためにコンポーネント単体でテストが実施できない などの課題が出てくる場合もあるかと思います。

そんな時に活躍するのが モック です。

コンポーネント、メソッド、APIの戻り値のような、テストしたい対象が依存している存在のダミーを作成してくれるツールです。 単体で完結するコンポーネントのユニットテストなどでは不要です。

モックはライブラリごとにできること・できないことが明確に分かれているので、目的に応じて選択する必要があります。

代表的なライブラリとしては sinon があります。 その他にも、 jest にも強力なモック機能が備わっていたり、swagger-node ではAPIサーバのモックを建てることもできます。

# ■ ブラウザ自動化ツール、仮想ブラウザ

Webにおけるフロントエンドとは、ブラウザで表示できるものを指します。

ブラウザ自動化ツールを使用することで、ブラウザの起動やページアクセスと言ったブラウザ操作をプログラミングで制御することができるようになります。

仮想ブラウザ は実際の描画は行わずに出力されるHTMLやCSSを解析して検証を行うツールです。 CSSセレクタを利用してDOM要素を抽出したり、フォーム入力やボタンクリックなどの動作を検証することも可能ですが、 あくまで ブラウザを模したツール でしかないため、特に仮想DOMで描画を行うVueやReactでは、実際の動きとは異なる場合が出てきます。

仮想ブラウザ では jsdom というライブラリが有名ですね。

仮想ブラウザ に対して ブラウザ自動化ツール は、実際のブラウザを起動して描画を行う点で本物の動作を再現することができます。 また仮想ブラウザ同様に、CSSセレクタを使ったDOM要素の抽出、フォーム入力などももちろん行えます。

ただし、実際のブラウザを起動するために起動に非常に時間がかかるのがデメリットで、起動だけで数十秒〜1分以上かかる場合もあります。 (仮想ブラウザは数秒以内に起動します)

ブラウザ自動化ツール で有名なライブラリは Selenium Puppeteer Nighmare Cypress などがあります。

# 技術選定

それでは目的に合わせて使用するツールを選定していきます。

# ■ ユニットテスト

まず ユニットテスト ですが、ユニットテストではコンポーネントを単体で立ち上げて動作検証を行いますので、 基本的に テストランナー だけで事足りると思います。

機能的にもシンプルなテストランナーで十分だったため ava をおすすめしたいところなんですが、 後述の 機能テスト を構築する際に jest の方がやりやすかったので、共通のライブラリで通せるよう jest を採用しました。

その他にテスト用で使用しているライブラリは vue-test-utils です。 これはVueのコンポーネントをテストするためのツールで、Vueをテストするにはほぼ必須です。

ユニットテストで使用するライブラリ

# ■ 機能テスト

次に 機能テスト では、複数のコンポーネントを組み合わせた Webページ そのものをテストしたいため、ブラウザを導入する必要がありました。

仮想ブラウザは軽量ではありますが、jsdomではNuxtやVueRouterのライフサイクルフック(fetchやbeforeEnterなど)をエミュレートしてくれず断念。

Nightmareはメソッドチェーンで操作を記述する形式ですが、「複数の操作をメソッドチェーンでまとめて実行する」というこの記法はどちらかというと シナリオ駆動型 のテスト向きで、 パラメータを変えながら一つ一つの表示を確認するような動作にはあまり適していないと判断しました。

このようにいくつかのライブラリで試行錯誤をした結果、最終的にGoogleが開発している puppeteer を採用することにしました。

さらに、 puppeteerjest を連携するための jest-puppeteer というライブラリがあったのでそちらを使います。 ただの設定ファイル群ではありますが、環境構築の大変さを少しでも楽にしてくれるのはありがたい。


また、APIサーバからの戻り値によって変化する画面の検証を行うにあたり、 APIのモックサーバ を用意する必要がありました。

モックサーバに求められる要件は下記の通り。

  1. フロントのソースコードに手を加えずにモックへの差し替えができること
  2. テスト中に戻り値を書き換えられること(パラメータを変えながら検証をするため)

ここで試したのが jest のモック機能。

jestはソースコード内の import で読み込んでいるモジュールをファイルごと差し替える機能を持っており、 フロントのソースコードに手を加えずに差し替え ができます。

ですが、E2Eテストを行うにあたり、Nuxtのプロジェクトをビルド&サーバ起動する必要が出ました。

jest のモック機能では、ビルド済みのソースコードを差し替える機能は持っておらず、 またモックとして用意したモジュールはビルド後には外部から更新がかけられなかったりしたため、 テスト中に戻り値を書き換えられること の要件が満たせず利用を諦めました。

sinon も同様の理由から断念。


最終的にAPI仕様の定義に使われ、開発用APIモックサーバとしても有名な Swagger を導入することになりました。

これは設計フェーズ〜フロントエンド開発の際によく使われるツールなのですが、 API仕様を厳密に定義しつつモックサーバとしても機能する点から、ドキュメントとしても活躍が見込めるために採用しました。

こちらはモックと言えど本物のサーバを起動するため、ビルド済みNuxtとは完全に切り離してコントロールできるため、 APIの向き先を変えるだけで 1,2 どちらの要件もクリアすることができました。(APIの向き先は .env に記載しているので、ソースコードは修正不要)

欠点としては、単にJSONを返すだけのモックを作成するのに、yaml でAPIの仕様を定義し、 expressサーバのコントローラを記述するという作成コストの重さ。

これは正直ちょっと面倒ではあるので、他に便利なものを知っている方がいたら教えて下さい。

機能テストで使用するライブラリ

# まとめ

今回はテスト作成の方針と技術選定についてご紹介しました。

「テストがないコードはレガシーコード」とは有名な言葉ですが、 レガシーではないコードにするための準備もなかなかのハードルの高さがありました。

次回はいよいよ環境を構築していきたいと思います。お楽しみに!