本文書はJestとVue Test Utilsでのテストに関する2回目の記事で2回目となる今回はテスト入門者にとって少しわかりずらいStub(スタブ)やMock(モック)、Shallow Mountに注目して説明を行っています。

Stub, ShallowMountとMountの違いを説明した後にVue.jsのHTTPリクエストで頻繁に利用されるaxiosのMock(モック)の方法についても説明を行っています。コンポーネントの単体テストでは、axiosやfetchのような外部へのアクセスを伴う機能を実装している場合モックを利用することで外部へのアクセスを行うことなくコンポーネントのテストを実施することができます。

VueでのTestingの経験が少ない人であれば先に前回公開した”【基本編】Jestを利用してVue コンポーネントをテストする方法(Unit Test)”を読んでおくことをお勧めします。

mountとshallowMountの違い

Vueのコンポーネントをテストする場合は必ずテストしたいコンポーネントをマウントする必要があります。

Vue Test Utilsにはコンポーネントをマウントする関数が2つあります。一つはmount関数でもう一つはshallowMount関数です。mountでは子コンポーネントを持っている場合に子コンポーネントも一緒に表示することができますが、shallowMountでは子コンポーネントは一緒に表示されません。

表示するしないという言葉よりも実際に動作確認をすることで簡単に理解をすることができます。mountとshallowMountの違いを確認するためにUser、UserListコンポーネントをcomponentsフォルダに作成します。Userコンポーネントは子コンポーネントにUserListコンポーネントを持ちます。


<template>
  <h1>Vue Test Utilsの使い方</h1>
  <user-list />
</template>
<script>
import UserList from "@/components/UserList"
export default {
  components:{
    UserList,
  }
}
</script>

<template>
  <h2>ユーザ一覧</h2>
</template>

tests/unitフォルダにuser.spec.jsファイルを作成してテストコードを記述します。一つ目のテストはmount関数を利用し二つ目のテストではshallowMount関数を利用しています。wrapperオブジェクトのhtmlメソッドでHTMLの中身を表示させることで違いを確認します。


import { mount, shallowMount } from '@vue/test-utils';
import User from "@/components/User.vue";

describe('User', () => {
  it("mount render a child component",() => {
    const wrapper = mount(User);
    console.log(wrapper.html())
  })
  it("shallowMount not render a child component",() => {
    const wrapper = shallowMount(User);
    console.log(wrapper.html())
  })
})

user.spec.jsファイルを作成後、npm run test:unitコマンドでテストを実施します。


 % npm run test:unit

> vue-unit-test@0.1.0 test:unit
> vue-cli-service test:unit

 PASS  tests/unit/user.spec.js
  User
    ✓ mount render a child component (34ms)
    ✓ shallowMount not render a child component (4ms)

  console.log tests/unit/user.spec.js:7
    <h1>Vue Test Utilsの使い方</h1>
    <h2>ユーザ一覧</h2>

  console.log tests/unit/user.spec.js:11
    <h1>Vue Test Utilsの使い方</h1>
    <user-list-stub></user-list-stub>

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        1.927s, estimated 2s
Ran all test suites.

shallowMountの場合は子コンポーネントのHTMLではなくstubタグuser-list-stubが表示されています。つまり子コンポーネントがダミーのuser-list-stubタグに置き換えられておりUserListコンポーネントの中に記述されているHTMLタグの内容は表示されません。mountの場合はUserListコンポーネントの中に記述されている内容(<h2>ユーザ一覧</h2>)がそのままUserコンポーネントの中身と一緒に表示されていることが確認できます。

console.logでHTMLの中身を確認しましたが、toContain関数(ユーザ一覧という文字列が含まれているかチェック)を利用してテストを実行することでテストへの影響も確認します。mount関数のテストは先ほど確認した通り子UserListの中身が表示されているのでPASS(成功)しますが、shallowMountはFAIL(失敗)します。


describe('User', () => {
  it("mount render a child component",() => {
    const wrapper = mount(User);
    expect(wrapper.html()).toContain('ユーザ一覧')
  })
  it("mount render a child component",() => {
    const wrapper = shallowMount(User);
    expect(wrapper.html()).toContain('ユーザ一覧')
  })
})
//エラーメッセージの内容の一部
  ● User > mount render a child component

    expect(received).toContain(expected) // indexOf

    Expected substring: "ユーザ一覧"
    Received string:    "<h1>Vue Test Utilsの使い方</h1>
    <user-list-stub></user-list-stub>"

UserListコンポーネントの影響を受けずUserコンポーネントのみの単体テストを実施したい場合であればShallowMountを利用した方テストを実施したほうがいいことがここまでの動作確認で理解することができました。

Stub(スタブ)の設定方法

Vue Test UtilsではStubは実際のコンポーネントの代わりになるダミーのコンポーネントのことです。

mount関数でのStubの設定

shallowMount関数を使った場合は子コンポーネントは自動でStubになっていましたが、mount関数の場合はグローバルオプションを利用することで子コンポーネントをStubにすることができます。グローバルオプションの中で子コンポーネントを置き換えるStubにHTMLを設定することも可能です。


import { mount } from '@vue/test-utils';
import User from "@/components/User.vue";

describe('User', () => {
  it("mount render a child component",() => {
    const wrapper = mount(User,{
      global:{
        stubs:{
          UserList:{
            template:"<h2>Stubで置き換え</h2>"
          }
        }
      }
    });
    console.log(wrapper.html())
  })
})

テストを実行すると置き換えられた子コンポーネントのHTMLが表示されます。UserListには<h2>ユーザ一覧</h2>と記述されていたので全く異なるダミーの内容を表示することができます。


% npm run test:unit

> vue-unit-test@0.1.0 test:unit
> vue-cli-service test:unit

 PASS  tests/unit/user.spec.js
  User
    ✓ mount render a child component (27ms)

  console.log tests/unit/user.spec.js:23
    <h1>Vue Test Utilsの使い方</h1>
    <h2>Stubで置き換え</h2>

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.234s, estimated 2s
Ran all test suites.

StubにダミーのHTMLを設定しない場合は値にtrueを設定します。テストを実行すると子コンポーネントがuser-list-stubタグに置き換わっていることが確認できます。


import { mount } from '@vue/test-utils';
import User from "@/components/User.vue";

describe('User', () => {
  it("mount render a child component",() => {
    const wrapper = mount(User,{
      global:{
        stubs:{
          UserList:true
        }
      }
    });
    console.log(wrapper.html())
  })
})
// npm run unit:test実行時の一部
  console.log tests/unit/user.spec.js:24
    <h1>Vue Test Utilsの使い方</h1>
    <user-list-stub></user-list-stub>

shallowプロパティによる設定

マウントオプションにはshallowプロパティがありtrueに設定することですべての子コンポーネントがStubとなります。shallowMount関数を利用した時と同じ動作になります。


describe('User', () => {
  it("mount render a child component",() => {
    const wrapper = mount(User,{
      shallow:true,
    });
    console.log(wrapper.html())
  })
})

axiosのHTTPリクエストをMockする方法

UserListコンポーネントにユーザ一覧を表示するためには通常axios, fetch関数を利用して外部リソースにアクセスすることで取得します。

ここでは無料のJSONPlaceHolderを利用してaxiosのgetメソッドを利用してユーザ情報を取得します。JSONPlaceHolderではhttps://jsonplaceholder.typicode.com/usersにアクセスすると10件分のダミーのユーザ情報を取得することができます。


<template>
  <h2>ユーザ一覧</h2>
  <ul>
    <li v-for="user in users" :key="user.id">{{ user.name }}</li>
    </ul>
</template>
<script>
import axios from "axios"
export default {
  data(){
    return {
      users:[]
    }
  },
  mounted(){
    axios.get('https://jsonplaceholder.typicode.com/users')
    .then(response => {
      this.users = response.data;
    })
  }
}
</script>

作成したUser, UserListをブラウザ上で確認するとaxiosのgetメソッドで取得したユーザ一覧が表示されます。

ブラウザ上にユーザ一覧表示
ブラウザ上にユーザ一覧表示

UserListコンポーネントでは、実際に外部リソースであるJSONPlaceHolderのサービスにアクセスを行ってユーザ情報を取得していますがテストで実際に外部リソースへのアクセスを行わない場合はmockを利用してテストを行うことができます。

axiosのmock設定

axiosのモジュールをmockするためにjest.mockを利用します。またUserListではaxiosのgetメソッドを使っているのでmock関数のjest.fnを利用します。jest.mockとjest.fnを利用することでテスト中にaxios.getメソッドを実行した場合は設定したMockのgetメソッドが代わりに実行されることになります。

axiosのgetメソッドは実行するとPromiseを返すのでmock関数でもPromise.resolveを利用してユーザの一覧をPromiseで戻しています。


jest.mock("axios", () => ({
  get: jest.fn(() => Promise.resolve({ data: [
        {id:1,name:"Leanne Graham"},
        {id:2,name:"Ervin Howell"},
      ]
  }))
})
);
Promise.resolveについてはnew Promise(() => resolove(…))の記述を簡略化したものです。

axiosをMockすることでPromise.resolveで設定したユーザ一覧が表示されるかwapper.html()を利用して確認します。


import { mount, shallowMount } from '@vue/test-utils';
import User from "@/components/User.vue";

jest.mock("axios", () => ({
  get: jest.fn(() => Promise.resolve({ data: [
        {id:1,name:"Leanne Graham"},
        {id:2,name:"Ervin Howell"},
      ]
  }))
})
);

describe('User', () => {
  it("mount render a child component",async() => {
    const wrapper = mount(User);
    console.log(wrapper.html())
  })
})

テストを実行するとexpect関数を利用していないのでテストにはPASSしますが、残念ながらulタグの中には何も表示されません。liにユーザ名が表示されないのはaxiosで設定したユーザ一覧がDOMに反映されていないためです。Mockで設定したデータを反映させるためにflush-promisesパッケージのインストールを行います。


 PASS  tests/unit/user.spec.js
  User
    ✓ mount render a child component (35ms)

  console.log tests/unit/user.spec.js:26
    <h1>Vue Test Utilsの使い方</h1>
    <h2>ユーザ一覧</h2>
    <ul></ul>

flush-promisesパッケージはnpmコマンドを利用してインストールを行います。


 % npm install flush-promises --save-dev

インストールしたflushPromisesをuser.spec.jsファイルに追加します。


import { mount, shallowMount } from '@vue/test-utils';
import User from "@/components/User.vue";
import flushPromises from 'flush-promises'

jest.mock("axios", () => ({
  get: jest.fn(() => Promise.resolve({ data: [
        {id:1,name:"Leanne Graham"},
        {id:2,name:"Ervin Howell"},
      ]
  }))
})
);

describe('User', () => {
  it("mount render a child component",async() => {
    const wrapper = mount(User);
   await flushPromises()
    console.log(wrapper.html())
  })
})

再度テストを実行すると今度はulタグの中にaxiosのMockで設定したユーザ名が表示されることが確認できます。


 PASS  tests/unit/user.spec.js
  User
    ✓ mount render a child component (36ms)

  console.log tests/unit/user.spec.js:26
    <h1>Vue Test Utilsの使い方</h1>
    <h2>ユーザ一覧</h2>
    <ul>
      <li>Leanne Graham</li>
      <li>Ervin Howell</li>
    </ul>

これでmockしたaxiosを利用することで設定したデータがテストの中で表示され、mockしたaxiosをテストで問題なく利用できることが確認できました。

jest.fn()の中身を変更

一部の読者の中にaxiosとPromiseのことがあまり理解できておらず、なぜPromise.resolveを利用しているのかPromise.resolveを利用せずにそのままユーザ一覧の配列を入れた場合はどうなるのかと疑問を持つ人もいるかと思います。理解を深めるために実際にPromise.resloveから配列に変更して再度テストを実施してみましょう。


jest.mock('axios', () => ({
  get: jest.fn(() => [
   {id:1,name:"Leanne Graham"},
   {id:2,name:"Ervin Howell"},
 ])
}))

テストを実行すると下記のエラーが発生してテストが途中で失敗します。


    TypeError: _axios.default.get(...).then is not a function

しかし、ライフサイクルメソッドのmountedをaync, await関数を利用して書き換えるとmock関数でPromiseを利用しなくてもユーザ一覧は表示されます。awaitではPromiseが戻ってくることを待っていますが、Promiseが戻ってこない場合はそのままの値を返します。


async mounted(){
  const response = await axios.get('https://jsonplaceholder.typicode.com/users')
  this.users = response;
}

mockのaxiosのテスト方法

UserListコンポーネントで実行されるaxiosについてmatchers関数を使ってテストを行うことができます。

matchers関数であるtoHaveBeenCalledTimes関数ではテスト中にaxiosが何回呼ばれたかのテストを実行することができます。テストを実行するとライフサイクルフックでaxiosは1度だけ呼ばれるのでtoHaveBeenCalledTimesの引数に1と回数を設定することでテストにPASS(成功)します。


describe('User', () => {
  it("mount render a child component",async() => {
    const wrapper = mount(User);
    await flushPromises()
    expect(axios.get).toHaveBeenCalledTimes(1)
  })
})

axios.getメソッドが呼ばれたかどうかの確認であればtoHaveBeenCalled関数を利用することができます。


describe('User', () => {
  it("mount render a child component",async() => {
    const wrapper = mount(User);
    await flushPromises()
    expect(axios.get).toHaveBeenCalled()
  })
})

もしaxios.getメソッドが呼ばれなかった場合は下記のエラーが表示されます。


 FAIL  tests/unit/user.spec.js
  User
    ✕ mount render a child component (16ms)

  ● User › mount render a child component

    expect(jest.fn()).toHaveBeenCalled()

    Expected number of calls: >= 1
    Received number of calls:    0

      19 |     // console.log(wrapper.html())
      20 |     // expect(axios.get).toHaveBeenCalledTimes(1)
    > 21 |     expect(axios.get).toHaveBeenCalled()
         |                       ^
      22 |   })
      23 | })

      at Object.<anonymous> (tests/unit/user.spec.js:21:23)

axios.getメソッドで指定した引数をチェックしたい場合にはtoHaveBeenCalledWith関数を利用することができます。URLのスペルが間違っている場合にはエラーがテストでFAIL(失敗)します。


expect(axios.get).toHaveBeenCalledWith('https://jsonplaceholder.typicode.com/users')

Mockで設定したデータを使ってテストも行うことができます。findAllメソッドli要素を取得して要素の数をtoHaveLength関数でチェックしています。


const users = wrapper.findAll('li')
expect(users).toHaveLength(2)

findAllではDomWrapperオブジェクトを取得することができるのでtextメソッドを利用して要素のコンテンツを取得し、toContains関数で含まれている文字列のチェックを行います。


const users = wrapper.findAll('li')
expect(users[0].text()).toContain('Leanne')
expect(users[1].text()).toContain('Howell') 

動作確認を行ったテストは下記の通りです。


import { mount, shallowMount } from '@vue/test-utils';
import User from "@/components/User.vue";
import flushPromises from 'flush-promises'
import axios from "axios"

jest.mock("axios", () => ({
  get: jest.fn(() => Promise.resolve({ data: [
        {id:1,name:"Leanne Graham"},
        {id:2,name:"Ervin Howell"},
      ]
  }))
})
);

describe('User', () => {
  it("mount render a child component",async() => {
    const wrapper = mount(User);
    await flushPromises()
    expect(axios.get).toHaveBeenCalledTimes(1)
    expect(axios.get).toHaveBeenCalled()
    expect(axios.get).toHaveBeenCalledWith('https://jsonplaceholder.typicode.com/users')
    const users = wrapper.findAll('li')
    expect(users).toHaveLength(2)
    expect(users[0].text()).toContain('Leanne')
    expect(users[1].text()).toContain('Howell')    
  })
})

Mock関数のメソッドとプロパティの確認

Jestのドキュメントを確認するとMock関数のメソッドの一覧が表示されていますが初めてMock関数を利用する人にとってはどのように使うのかわかりにくいと思います。axiosのmockを利用しながら各メソッドの利用方法を確認しながら理解を深めていきましょう。

Mock関数のメソッド一覧
Mock関数のメソッド一覧

UserListのコンポーネントのライフサイクルフックmountedで実行されているaixosのコードは以下の通りです。


mounted(){
  axios.get('https://jsonplaceholder.typicode.com/users')
  .then(response => {
    this.users = response.data;
  })
}

mockImplementationOnceメソッド

mockImplementationOnceメソッドを使ってテストコードを下記のように書き換えることができます。書き換えを行ってもテストの結果は変わりません。


jest.mock("axios", () => ({
   get: jest.fn().mockImplementationOnce(() => Promise.resolve({ data:[
   {id:1,name:"Leanne Graham"},
   {id:2,name:"Ervin Howell"},
 ]})
 )
})
);

axiosモジュールをmockした後にaxios.getメソッドの戻り値を別の行として記述することもできます。


jest.mock("axios")
axios.get.mockImplementationOnce(() => Promise.resolve({ data: [
      {id:1,name:"Leanne Graham"},
      {id:2,name:"Ervin Howell"},
    ]
  })
)

mockResolvedValueOnceメソッド

mockResolvedValueOnceメソッドを利用するとPromise.resolveを省略してもPromiseを戻してくれます。


jest.mock("axios", () => ({
   get: jest.fn().mockResolvedValueOnce({ data:[
   {id:1,name:"Leanne Graham"},
   {id:2,name:"Ervin Howell"},
 ]})
})
);

mockReturnValueOnceメソッド

UserList.vueで実行しているaxiosのgetメソッドはPromiseを戻す必要があるのでそのままの値を戻すmockReturnValueOnceメソッドではテストでエラーが発生します。Promiseを戻す必要のたまMock関数であればmockReturnValueOnceメソッドを利用することができます。


jest.mock("axios", () => ({
   get: jest.fn().mockReturnValueOnce({ data:[
   {id:1,name:"Leanne Graham"},
   {id:2,name:"Ervin Howell"},
 ]})
})
);

mock関数のプロパティの確認

mock関数はメソッドだけではなくプロパティも持っています。持っているプロパティは.mockで確認することができます。axios.getの場合はaxios.get.mockと設定します。


describe('User', () => {
  it("mount render a child component",async() => {
    const wrapper = mount(User);
    await flushPromises()
    console.log(axios.get.mock)   
  })
})

テストを実行するとmockプロパティはcalls, instances, invocationCallOrder, resultsの4つのプロパティから構成されていることが確認できます。


{
  calls: [ [ 'https://jsonplaceholder.typicode.com/users' ] ],
  instances: [ { get: [Function] } ],
  invocationCallOrder: [ 1 ],
  results: [ { type: 'return', value: [Promise] } ]
}

callsはgetメソッドの引数に入っている文字列を取得することができるのでcallsを利用してテストを実施することができます。


expect(axios.get.mock.calls[0][0]).toBe('https://jsonplaceholder.typicode.com/users');

callsプロパティの配列はaxios.getが呼ばれる度に要素が追加されるので呼ばれる回数はcalls.lengthと一致します。呼び出される回数でテストを実施したい場合は下記のようにテストすることができます。


expect(axios.get.mock.calls.length).toBe(1);

本文書と前回公開した”【基本編】Jestを利用してVue コンポーネントをテストする方法(Unit Test)”を一通り読んだ人であれば、Vue.jsのVue Test Utils、Jestの公式ドキュメントを読みこなせるようになっていると思います。実際に手を動かしてテストを実行して、テストの自分なりの方法論を考えバグの少ない高品質なコードを記述できるようになってください。