🎉

Jest初心者が、VueにJestを導入〜単体テストを実施して、ハマった箇所と感想

2022/12/08に公開

この記事はTechCommit Advent Calendar 2022 10日目の記事です。

現在携わっているプロジェクトで、Vue.jsにJestを導入して、コンポーネントの単体テストを書いたので、Jest導入〜単体テストまでの経験を通してハマった箇所と感想をまとめてみたいと思います。

バックエンドのテストは書くけど、フロントエンドのテストは経験がないな、という方や、Jestは気になっているけど、どんな感じで書くの?どんなテストができるの? と思っているような方の参考になれば幸いです。

フロントエンドテスト全体として大事にすべき観点などは、TechCommit Advent Calendar 2022 の2日目に井上さんが書かれているJavaScript+Vue.jsの品質向上のためにしている基本的で大事な5つのコトの記事も参考にして頂ければと思います。
https://note.com/inodev/n/nc0f672a247ae

Jestを導入することになった背景

  • プロジェクトはバックエンドがRails、フロントエンドがVue.jsで構成
  • バックエンドはRspecのテストがあるが、フロントエンドはテストがなかった
  • フロントエンドのJavaScript関連のバグが発生することが多く、品質担保が目的

筆者のバックグラウンド

  • 主によく書くのはRails
  • Railsの開発において、Rspecテストは普段から書いている
  • Vue.jsも経験はあるが、歴が浅い
  • フロントエンドのテストは書いたことがない

そもそも、Jestとは?

  • Jestは、Facebookが開発しているJavaScriptのテストフレームワーク
  • Babel, TypeScript, Node, React, Angular, vueなど、フロントエンド開発で広く利用されている

https://jestjs.io/ja/

Jestの導入で行ったこと

環境・バージョン

  • vue: 2.6.10
  • Node: 16.18.0

Jestを利用するために、使用したライブラリ

Vue Test Utils

Vue.jsの公式が提供している、Vueコンポーネントをテストするための専用ユーティリティライブラリのVue Test Utilsを使用しました。

Vue Test Utilsは、Vueコンポーネントのコンパイル、コンポーネントのスタブ化やモックの注入など、テストに必要な機能を提供してくれます。

Vue Test Utilsのインストールは公式に従ってインストールしました。

https://v1.test-utils.vuejs.org/ja/

Jest導入でハマったこと

【1】ハマったこと: vue-jestのバージョンの依存関係でハマった

Vue Test Utilsを導入する際に、Jest に *.vue ファイルの処理方法を教えるために、vue-jest プリプロセッサをインストールする必要があります。

ただ、npm install --save-dev vue-jestでインストールしただけでは、テスト実行時に、下記のようにJavaScriptのsyntaxについて、Jestではサポートしていないと怒られてしました。

エラー内容

FAIL  app/assets/javascripts/vue/components/test.spec.jsTest suite failed to run

    Jest encountered an unexpected token

    Jest failed to parse a file. This happens e.g. when your code or its dependencies use non-standard JavaScript syntax, or when Jest is not configured to support such syntax.

    Out of the box Jest supports Babel, which will be used to transform your files into valid JS based on your Babel configuration.

    By default "node_modules" folder is ignored by transformers.

解決策

今回、vueのバージョン2の環境にJestを導入していた関係で、@vue/vue2-jestを指定してインストールする必要がありました。

vueとjestのそれぞれのバージョンに応じたvue-jestのバージョンを指定する必要があるので注意です

vue-jestのバージョン

https://github.com/vuejs/vue-jest

【2】ハマったこと:vue-test-utilsの利用に、jest-environment-jsdomが必要

エラー内容

[vue-test-utils]: window is undefined, vue-test-utils needs to be run in a browser environment. 
You can run the tests in node using jsdom 
See https://vue-test-utils.vuejs.org/guides/#browser-environment for more details.

jestのv28からjsdomが標準でインストールされなくなったため、別途jest-environment-jsdomをインストールする必要がありました。

Jestアップグレードガイド

https://jestjs.io/docs/28.x/upgrading-to-jest28#jsdom

解決策

  • jest-environment-jsdomを別途インストール
npm install --save-dev jest-environment-jsdom

Vueコンポーネントの単体テスト(お気に入りボタン)

今回はjestを使って、お気に入りボタンの単体テストを書きました
具体的には下記の2点のテストコードを作成しました。

1. コンポーネントのロジックをテスト
2. スナップショットテスト

テスト対象のVueコンポーネント(お気に入りボタン)のロジック

  • お気に入りボタンのクリック時に、axiosを使ってバックエンドのAPIに対して、postとdeleteのリクエストを送り、データを更新
  • propsの値によってお気に入りボタンの表示を切り替える

【1】コンポーネントのロジックをテスト

Vueコンポーネント内で行っているAP通信に関しては、axios-mock-adapterを使ってAPI通信をmock化させてテストしました。

また、非同期動作のためにflushPromisesを利用しました。flushPromisesを使うと、非同期処理を全て流すことができます。

favorite_button.spec.js
  it('methods:createLike お気に入りのボタンをクリックすると、likedがtrueになること', async () => {
    const wrapper = mount(FavoriteButtton, {
      propsData: {
        id: 1,
        like: false
      }
    })
    const starButton = wrapper.find('.star-btn')
    const axiosMock = new MockAdapter(axios)
    axiosMock.onPost('/api/likes', { id: 1 }).reply(200)

    expect(wrapper.vm.$data.liked).toBe(false)
    starButton.trigger('click')
    await flushPromises()
    expect(wrapper.vm.$data.liked).toBe(true)
  })

https://v1.test-utils.vuejs.org/ja/guides/testing-async-components.html

【2】スナップショットテスト

スナップショットテストは、UIの変更前の出力結果と変更後の出力結果を見比べて、差分を検知するテストです。今回、propsに任意の値を渡した場合に、期待するDOMが生成されることをテストしました。

スナップショットでテストした内容

  • 任意のpropsを渡した場合に、期待するお気に入りボタンのclass名が存在すること
  • Jestのマッチャーとして提供されるtoMatchSnapshotを使用して、DOMの差異の有無を検知
favorite_button.spec.js
describe('props', () => {
  it('未いいねの場合、star-btnが表示されること', () => {
    const wrapper = mount(FavoriteButtton, {
      propsData: {
        id: 1,
        like: false
      }
    })

    expect(wrapper.find('.star-btn')).toBeTruthy()
    expect(wrapper).toMatchSnapshot()
  })

  it('いいね済みの場合、unstar-btnが表示されること', () => {
    const wrapper = mount(FavoriteButtton, {
      propsData: {
        id: 1,
        like: true
      }
    })

    expect(wrapper.find('.unstar-btn')).toBeTruthy()
    expect(wrapper).toMatchSnapshot()
  })
})

生成されるスナップショットファイル

toMatchSnapshotを使うと、初回テスト実行時にスナップショットファイルが生成されます。
2回目以降のテストでは、DOMの生成に変化があった場合に検知して、スナップショットテストが落ちるようになります。

favorite_button.spec.js.snap
exports[`props いいね済みの場合、unstar-btnが表示されること 1`] = `
<div class="badges__action">
  <div class="unstar-btn"><i class="fas fa-star"></i></div>
</div>
`;

exports[`props 未いいねの場合、star-btnが表示されること 1`] = `
<div class="badges__action">
  <div class="star-btn"><i class="fas fa-star"></i></div>
</div>
`;

【その他】package.jsonにnpm scriptsの使い方を記述

スナップショットの変更内容を確認して、更新するためのnpm scriptsをpackage.jsonに追加しました。今後、スナップショットが変更された時に、変更内容を確認した上で、更新を行うためのscriptsです。

  • test:watch→変更の差異を確認
  • test:update→スナップショットファイルを更新
package.json
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:update": "jest --updateSnapshot"
  }

Jestを初めて触ってみての感想

  • Jest関連のライブラリのバージョン依存などで、テスト実行できる状態にするまでの環境設定に時間がかかった
  • テストの書き方は、RailsのRspecテストのコードと似ており、とっつきやすかった
  • Jestのドキュメントが充実していて、公式見ながらやればテストコードを書くこと自体に苦はなさそうと感じた
  • 導入するまでの腰は重いが、もっと早めに導入できれば良かったなと思う
  • 既存のフロントエンドのコードにもどんどんテストを追加して、品質を担保していきたい

Jest導入時に参考にしたサイト・記事

https://v1.test-utils.vuejs.org/ja/
https://lmiller1990.github.io/vue-testing-handbook/ja/#vue-jsテストハンドブック
https://zenn.dev/tentel/books/08b63492b00f0a/viewer/27c963
https://zenn.dev/keita_hino/articles/488d31e8c4a240

Discussion