📝

Nuxt.jsでjestを使ってテストを始める

2021/09/20に公開

jestとは

JestはJavaScriptの単体テストのフレームワークです。
単体テスト(ユニット(Unit)テストとも言います)とは、ボタンやセレクトボックスなど個々の機能を正しく果たしているかどうかを検証するテストの事を指します。
この記事はJavaScriptのテストフレームワークでよく使われているjestをNuxt.jsを使ってとりあえず試してみたものになります。

バージョン

nuxt.js: 2.15.8
node.js:16.6.0

nuxtcliでプロジェクト作成

yarn create nuxt-app nuxt_ts_jest
yarn create v1.22.5
[1/4] 🔍  Resolving packages...
[2/4] 🚚  Fetching packages...
[3/4] 🔗  Linking dependencies...
[4/4] 🔨  Building fresh packages...

success Installed "create-nuxt-app@3.7.1" with binaries:
      - create-nuxt-app

create-nuxt-app v3.7.1
✨  Generating Nuxt.js project in nxut_ts_jest
? Project name: nxut_ts_jest
? Programming language: TypeScript
? Package manager: Yarn
? UI framework: None
? Nuxt.js modules: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Linting tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Testing framework: Jest
? Rendering mode: Universal (SSR / SSG)
? Deployment target: Server (Node.js hosting)
? Development tools: (Press <space> to select, <a> to toggle all, <i> to invert selection)
? Continuous integration: None
? Version control system: Git

Nuxt.js modulesとLinting toolsとDevelopment toolsは選択せずにEnterで進める。

TypeScriptを追加する

nuxt-property-decoratorを追加する。

yarn add --dev nuxt-property-decorator

@components/にVueファイルを作る。

TargetEvent.vue
<template>
  <div>
    <p data-cy="countCheck">Count is: {{ count }}</p>
    <button @click="increment">増える</button>
  </div>
</template>
<script lang="ts">
import { Component, Vue } from "nuxt-property-decorator";

@Component
export default class Index extends Vue {
  count: number = 0;

  increment() {
    this.count += 1;
  }
}
</script>

TSでjestを記述する

@test/にjestのテストを書く。
中身の詳細は後述します。

target.spec.ts
import { mount } from "@vue/test-utils";
import App from "@/components/TargetEvent.vue";

describe("App", () => {
  it("click button count up", async () => {
    const wrapper = mount(App);

    // DOM更新系は即座にDOMに反映されないのでasync awaitを使う
    await wrapper.get("button").trigger("click");

    // findからのtextでタグの中身をとってくる
    const contain = wrapper.find('[data-cy="countCheck"]').text();
    console.log("contain内容チェック", contain);

    //タグの中身確認
    expect(contain).toContain("Count is: 1");
  });
});

TypeScriptにjestの型について怒られるので、@types/jestをインストールします。

yarn add --dev @types/jest

ts.config.jsonに追記してjestの型を認知してもらいます。

ts.config.json
 {
    /省略/	
    "types": [
      "@nuxt/types",
      "@types/node"
      "@types/jest" ←追記
    ]
 }

最後に、@typesファイルを作り、vueファイルにTSを使うことを宣言します。

vue-shim.d.ts
 declare module "*.vue" {
   import Vue from "vue";
   export default Vue;
 }

デフォルトであるファイルはいらないので削除します。

pages/index.vueを書き換えます

pages/index.vue
<template>
 <TargetEvent />
</template>

node.js立ち上げて確認

yarn dev

この様に見えればOK

準備完了です!!

jestを実行する

jestを実行するときは yarn test 実行したいファイル名で実行します。

❯ yarn test target.spec.ts
yarn run v1.22.5
$ jest
 PASS  test/target.spec.ts (6.888 s)
  App
    ✓ click button count up (50 ms)

  console.log
    contain内容チェック Count is: 1

      at Object.<anonymous> (test/target.spec.ts:13:13)

-----------------|---------|----------|---------|---------|-------------------
File             | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------------|---------|----------|---------|---------|-------------------
All files        |     100 |      100 |     100 |     100 |                   
 TargetEvent.vue |     100 |      100 |     100 |     100 |                   
-----------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        10.315 s
Ran all test suites.
✨  Done in 13.11s.

失敗した時は、テストがなぜ失敗したのかが赤文字で表示が出ます。

よくあるパターン

よく書いているパターンを参考でやってみました。

コンポーネントをいくつか作ってみました。

リポジトリ
https://github.com/kawa-t/nuxt_jest_typescript
デモページ
https://kawa-t.github.io/nuxt_jest_typescript/
それぞれのコンポーネントに対してテストを書いて実行していきます。

その1.表記があるかないか

一番基本的なやつで、ヘッダーに「Nuxt.jsでテスト」ときちんと記載されているかをテストで確認します。
テストを新しく追加するときはtest/ディレクトリに新しくファイルを作成します。

Header.spec.ts
import { mount } from '@vue/test-utils'
import Header from '@/components/Header.vue'

describe('Header', () => {
  it('タイトル名があって、変更されていないか', () => {
    const wrapper = mount(Header)
    // クラス名を取得
    const title = wrapper.find('.header__title')
    expect(title.text()).toBe('Nuxt.jsでjest')
  })
})

クラス名でテストしたいHTMLを取得するときは、wrapper.find('クラス名')で取得できます。

上記ではVue.js側の<div class="px-5 header__title">Nuxt.jsでjest</div>を取得してexpect(title.text()).toBe('Nuxt.jsでjest')で文字列を確認することができます。ヘッダーの文字列がVue.jsでテストで違っている時にテストを実行すると、下のような感じでエラーになります。

❯ yarn test Header.spec.ts
yarn run v1.22.5
$ jest Header.spec.ts
 FAIL  test/Header.spec.ts
  Header
    ✕ タイトル名があって、変更されていないか (37 ms)

  ● Header › タイトル名があって、変更されていないか

    expect(received).toBe(expected) // Object.is equality

    Expected: "Nuxt.jsでjest"
    Received: "Vue.jsでjest"

       8 |     const title = wrapper.find('.header__title')
    >  9 |     expect(title.text()).toBe('Nuxt.jsでjest')
         |                          ^
      10 |   })
      11 | })

テストで記述している期待していたものに対して(Expected)、実際のコンポーネント側の記述が違っている(Received)事を教えてくれます。
expect(title.text()).toBe('表記名')と書けばテストできるので、まずはこれで慣れていきました。

その2.ボタンを押したときに表示がされているか

ボタンを押した時に、期待通りに動作しているか確認するときによく使う書き方です。
カウンターボタンはクリックすると数字が1増えるもので、ヘッダーにあるハンバーガーメニューはクリックするとメニューが表示されます。
カウンターボタンとほとんど同じですが、ハンバーガーメニューをクリックした時にメニューが表示されるかもjestで書いてみます。

menu.spec.ts
describe('Header', () => {
  it('クリックしたときにクラス名が変わっているか', async () => {
    const wrapper = mount(Header)

    // DOM更新系は即座にDOMに反映されないのでasync awaitを使う
    await wrapper.get('.header__btn').trigger('click')

    // クラス名で取得する
    const contain = wrapper.find('.block')

    // クラス名.block切り替わっているか
    expect(contain.exists()).toBe(true)

    // もう一度押す(メニューを閉じる)
    await wrapper.get('.header__btn').trigger('click')
    const Closecontain = wrapper.find('.block')

    // クラス名.blockは消えているか
    expect(Closecontain.exists()).toBe(false)
  })
})

ハンバーガーメニューでは、メニューを表示している時と非表示にしている時で、クラス名が動的に切り替わるので、クラス名が切り替わっていればメニューがきちんと表示されているとしています。
クラス名があるかないかでの確認はexpect(Closecontain.exists()).toBe(ture)で確認しています。
いくつかポイントです。

ボタンをクリックした時に切り替わっているかは、async awaitを利用する

menu.spec.ts
await wrapper.get('.header__btn').trigger('click')

クラス・DOMが切り替わるのは即座に切り替わる訳ではないので、async awaitを利用しないと、うまくテストすることができないので注意が必要です。

その3.セレクトボックスの単体テスト


選択するやつによって動的に右のセレクトボックスが変わるやつ。

書いてみた例です。

selectbox.spec.ts
import { shallowMount } from '@vue/test-utils'
import selectbox from '~/components/SelectBox.vue'

describe('selectbox', () => {
  it('セレクトボックスの確認', done => {
    const wrapper = shallowMount(selectbox)

    //大項目のセレクトボックス
    const options = wrapper.find('[data-cy="select"]').findAll('option')

    // セレクトボックスの2つ目(=1つめ)の項目を選択する
    options.at(1).setSelected()

    const contain = wrapper.find('option:checked').text()
    expect(contain).toContain('やさい')

    //詳細項目のセレクトボックス
    setTimeout(() => {
      const detailSelecter = wrapper
        .find('[data-cy="detailselect"]')
        .findAll('option')

      // 3番目のものを取得
      detailSelecter.at(2).setSelected()

      // 詳細を取得するのでfindをつなげて指定する
      const Detailcontain = wrapper
        .find('[data-cy="detailselect"]')
        .find('option:checked')
        .text()

      expect(Detailcontain).toContain('大根')
      done()
    })
  })
})

いくつかポイントです。

クラス名以外で要素を取得する

const contain = wrapper.find('[data-cy="select"]').text()

要素を取得するのにクラス名以外からでも取得することができます。

DOM更新イベントはSetTimeoutを利用する

項目が選ばれていないときは詳細項目である下記のDOMがない状態です。

selectbox.vue
<option v-for="(item, key) in SelectedItem" :key="key">
 {{ item.food_name }}
</option>

1つ目のセレクトボックスが選ばれない限り、2つ目のDOMが生成されないので、この時はSetTimeoutを利用してDOMが生成されてから取得するようにします。
また、setTimeoutを使うときはdone()を明示しておかないと、setTimeout内のテストコードが実行されないので、引数にdoneを指定しておきます。

findはつなげられる

ひとつ目のoptionを選択してしまうので、要素を取得してから、option:checkedを指定することで詳細項目のセレクトボックスを指定しています。

selectbox.spec.ts
// 詳細を取得するのでfindをつなげて指定する
const Detailcontain = wrapper
  .find('[data-cy="detailselect"]')
  .find('option:checked')
  .text()

最後に

まだまだ手始めの段階なので、練習も兼ねて、今後もJest試していきます!
(間違いなどありましたらご指摘いただければ幸いです)

Discussion