LaravelとVue.jsのアプリでJestのテストを初めてやってみたので、振り返り

2021.04.29

見出し画像

LaravelとVue.jsでオリアプを作成しているんですけど、テストもちゃんと書かないと。。という事で初めてjestのテストを書いていきます。

備忘録的に書いていきます〜。モック?axiosのテストもするの??みたいな無知だった自分に向けて。。。(今わかっている範囲を)

実装する事

モックを用いたaxios通信や、メソッド発火のテスト

vuexのアクション発火のテスト

propsでデータを受け取れるのか

メソッドが実行されると表示が変わっているか?

などをテストしていきます。

大まかな流れ

Jestの導入(ハマった。。。)

propsで値を受け取れるか?インスタンスが存在するか?

モックを用いたaxios通信のメソッドが発火するか?

メソッドが実行されると表示が変わっているか?

vuexのアクション発火のテスト

この順番で実装していきます。

前提

laravel/vue.jsで複数の機能が完成されている。

Jestの導入(ハマった。。。)

導入記事1

導入記事2

なんか自分はbabel-coreの部分でハマったりして色々見ながらやりました。なのでハマった場合はググって他のも参考にしてみてください。

テストファイルはtests/Unitディレクトリに異彩していく形になると思います。

テスト実行コマンド

npm run test

ファイルを指定する場合

npm run test tests/Unit/taglist.test.js

propsで値を受け取れるか?インスタンスが存在するか?

propsで受け取る値を確認します。setProps()メソッドで設定し、expectで値が設定されているかを確認します。

Component

<script>
export default {
 props: ['item_id'],
},
</script>
Test
import { mount } from '@vue/test-utils';
import LikeTest from "../../resources/js/components/like.vue";
import { config } from '@vue/test-utils'
config.showDeprecationWarnings = false
describe('LikeTest', () => {

   const wrapper = mount(LikeTest});             1
   
   it('propsで値を受け取れること', () => {
       wrapper.setProps({                        2
           item_id: 1,
       })
       expect(wrapper.props().item_id).toBe(1)   3
   })
})

1,  vueコンポーネントを作成します。mount()メソッドの使い方

2,  setProps()メソッドを使用してwrapperのpropsにデータを渡しています。

3,  expect()メソッドを使用してwrapperに設定したpropsのitem_idが1になっているかを確認します。

モックを用いたaxios通信のメソッドが発火するか?(get)

2つのテストを書いてgetアクション時のaxiosを確認します。

少し複雑に見えるかもしれませんが、やっていることはシンプルです。期待する値の設定、メソッドの設定です。

特にポイントなのはcomponentの1とtestの6になります。

Component

<script>
const axios = require('axios');          1
export default {
 props: ['item_id'],
 data() {
   return {
     status: false,
   }
 },
 created() {
   this.like_check()
 },
 methods: {
   like_check() {
     const id = this.item_id
     const array = ["/items/",id,"/check"];
     const path = array.join('')
     return axios.get(path).then(res => {
       if(res.data == 1) {
         this.status = true
       } else {
         this.status = false
       }
     }).catch(function(err) {
       console.log(err)
     })
   },
 },
</script>

1,  axiosの読み込みを行う。これがないとtest自体が通らなかった。app.jsとかでaxiosを読み込んでいても、記載する必要がありました(自分だけ説濃厚)

Test

const axios = require('axios');     1
jest.mock('axios')                  2

describe('LikeTest', () => {
   const wrapper = mount(LikeTest, {
       propsData: {item_id: 1},
       methods: {
           like_check() {}          3
       },
   });   
   it('axiosテストget返り値有りであれば、statusがtrueになる', () => {
       const response = {
4          data: 1
       }
5      axios.get.mockResolvedValue(response);
6      const wrapper_after_created = mount(LikeTest)
7      return wrapper_after_created.vm.like_check().then(data => 
8          expect(wrapper_after_created.vm.status).toBe(true)
       );
   })

   it('axiosテストget返り値なしであれば、statusがfalseになる', () => {
       const response = {
9          data: null
       }
       axios.get.mockResolvedValue(response);
       const wrapper_after_created = mount(LikeTest)
       return wrapper_after_created.vm.like_check().then(data => 
10          expect(wrapper_after_created.vm.status).toBe(false)
       );
   })
})

1,  axiosの読み込みを行います。

2, 詳しくはわかりませんが、jestでaxiosを使いますよ〜的なことを行っているんだと思います。

3,  mount()メソッドでvueコンポーネントを作成する際にメソッドも追加しておきます。

4,  これが最初に言っていた期待する値の設定部分ですね。dataとして返り値が1になるように設定しています。

5,  4で定義したものを実際にaxios通信の返り値としてmockResolvedValue()メソッドを使用して設定しています。getの部分はその時のテストしたい通信アクションによって変更します。

6,  mountメソッドを使用してもう一度vueコンポーネントを作成します。これはcreated()時に発火されるメソッドとaxiosで呼ぶメソッドが同じ為エラーを防ぐために行っています。

7,  6で作成したvueコンポーネントでvmメソッドを使ってaxios通信メソッドを発火させます。vmは結構便利屋でコンポーネントのプロパティとメソッドにアクセスすることができます。

8,  プロパティのstatusの値がtrueになっていることを確認します。

9,  返り値にnullを設定した場合も同じようにテストしてみます。

10,  statusの値がfalseになっていることを確認します。

8や10の部分は適宜変更するところかと思います。何を確認したいテストなのかを分かっていればかける部分かと思います。

モックを用いたaxios通信のメソッドが発火するか(post)

こちらもぽとんどやっていることは同じです。こちらは処理後に別のメソッドが発火されることを確認していきたいと思います。

getと同じ記載の部分は省略しています。

Component

   like() {
     const id = this.item_id
     const array = ["/items/",id,"/likes"];
     const path = array.join('')
     return axios.post(path).then(res => {
       this.like_check()
     }).catch(function(err) {
       console.log(err)
     })
   }

今回はpostアクションが終わった後にlike_check()メソッドが呼び出されるかのテストになっています。

Test   
   it('axiosテストpost', () => {
       const response = {
1          data: 1                    
       }
2      const stub = jest.fn();
3      wrapper.setMethods({
           like_check: stub,
       });
4      axios.post.mockResolvedValue(response);
       return wrapper.vm.like().then(data => 
5          expect(stub).toHaveBeenCalled()
       );
   })

1,  axios通信の返り値を定義します。

2,  jest.fn()メソッドを使ってメソッドのモックを作成します。

3,  2で作成したモックをlike_checkメソッドとしてsetMethods()で定義します。

4,  1で定義したものをpost通信の返り値として設定します。

5,  2,3で設定したメソッドが呼ばれたかをtoHaveBeenCalled()メソッドを使ってテストしていきます。

メソッドが実行されると表示が変わっているか?

axiosの処理結果によって値が変更されるか確認します。

describe('CommentCountTest', () => {
 it('countGetメソッドaxios通信した後に値を取得、表示できる', done => {    1
   const wrapper = mount(CommentCount, {
     methods: {
       countGet() {}                                            2
     },
     propsData: {
       item_id: 1                                               3
     }
   });
   wrapper.setData({ comment_count: 2 })                        4
   expect(wrapper.vm.comment_count).toBe(2)                     5
   const response = {
     data: 4
   }
   axios.get.mockResolvedValue(response);                       6

   // 一旦マウントし直す必要がある
   const wrapper_after_created = mount(CommentCount)            7
   return wrapper_after_created.vm.countGet().then(data =>  {   8
     expect(wrapper_after_created.vm.comment_count).toBe(4)     9
     done()
   })
 })

})

1,  doneというのはaxiosの処理が確実に完了された時にテストの結果を受け取るようにするための記載になります。

2,  メソッドを1つ設定してライフサイクルメソッドのcreated()で呼ばれた時の処理に対応させています。

3,  propsデータとしてitem_idの値も設定します。

4,  setData()メソッドも使用して、dataの設定も行います。

5,  axios通信する前の値の確認です。

6,  返り値が4になってほしいテストなので、事前に定義したresponseを設定します。

7,  created()メソッドが実行された後のvueコンポーネントを作成します。

8,  axios通信でcountGet()メソッドを発火させます。

9,  axios通信の処理が終わった後、dataの値が変化しているかをvmを使用して確認します。

コンポーネントがマウントされた時に値によって表示が変わっているか?

データの値によって表示されるビューに違いが出るかを見ます。

<template>
 <div>
1  <button v-if="status == false" type="button" @click.prevent="like" class="btn btn-outline-warning">Like</button>
   <button v-else type="button" @click.prevent="like" class="btn btn-warning">Liked</button>
 </div>
</template>

<script>
const axios = require('axios');
export default {
 props: ['item_id'],
 data() {
   return {
     status: false,
   }
 },
 created() {
2  this.like_check()
 },
 methods: {
   like_check() {
     const id = this.item_id
     const array = ["/items/",id,"/check"];
     const path = array.join('')
     return axios.get(path).then(res => {
3      if(res.data == 1) {
         this.status = true
       } else {
         this.status = false
       }
     }).catch(function(err) {
       console.log(err)
     })
   },
 }
}
</script>

1,  statusの値によってviewの表示が切り替わるように設定します。

2,  ライフサイクルメソッドでlike_check()メソッドを動かしています。

3,  axiosの処理結果の値によって処理するアクションが変わるように条件分岐しています。

Test

describe('LikeTest', () => {  
1 it('statusがtrueだったらLikedボタンが見えること', done => {
2      wrapper.setData({ status: true })
3      expect(wrapper.vm.status).toBe(true)

4      return wrapper.vm.$nextTick().then(function() {
5          expect(wrapper.text()).toBe('Liked')
           done()
       })
   })

   it('statusがfalseだったらLikeボタンが見えること', done => {
6      wrapper.setData({ status: false })
       expect(wrapper.vm.status).toBe(false)

       return wrapper.vm.$nextTick().then(function() {
7          expect(wrapper.text()).toBe('Like')
8          done()
       })
   })
})

1,  doneを置いてaxios処理が終わった後にテストの結果が返ってくるようにします。

2,  setData()メソッドを使用して、dataの設定をstatusに対してします。

3,  axios通信前のdataの確認です。

4,  $nextTick()を使用してviewが描画された時にどちらのボタンが表示されているかを確認しています。

5,  今回はlikedが表示されていることを確認しています。

6, 2と同じようにdataのstatusの値を設定します。今回はfalseに設定します。

7,  5と同じ考え方で、 $nextTick()を使用して表示されているボタンが変化しているかを確認します。

8,  done()を最後に持って来ることで、axiosの処理や $nextTick()が完全に処理された後にテスト結果が表示される形になっています。

vuexのアクション発火のテスト

vuexのアクションを呼び出した時のテストを行います。基本的にイメージとしてはvuexのindex.jsに似せて書いていく形になると思います。なので先に今回使うindex.jsとコンポーネントを貼っておきます。

Vuex


import Vue from 'vue';
import Vuex from 'vuex';

Vue.use(Vuex);


const Follow = {
 namespaced: true,
 state: {
   follow_users: '',
 },
 。。省略
 actions: {
   get_follow_list({commit}, id) {
     commit('follow_list',id)
   },
   get_follower_list({commit}, id) {
     commit('follower_list', id)
   },
   follow_do({commit}, id) {
     commit('follow_action', id)
   },
   follow_check({commit}, id) {
     commit('follow_check', id)
   }
 }
}


export default new Vuex.Store({
 modules: {
   comment: Comment,
 }
})
Component


<script>
import {mapState} from 'vuex'
export default {
 data() {
   return {
     check: false
   }
 },
 props: ['user_id', 'login_user_id', 'csrf'],
 computed: {
   ...mapState("follow",['follow_users'])
 },
 created() {
   this.$store.dispatch('follow/get_follow_list', this.user_id)
   this.$store.dispatch('follow/get_follower_list', this.user_id)
 },
 methods: {
   send(id){
     this.$store.dispatch('follow/follow_do', id)
     this.$store.dispatch('follow/get_follow_list', this.user_id)
     this.$store.dispatch('follow/get_follower_list', this.user_id)
   },
 }
}

今回はactionsとstateのテストを行なっていきます。

全コードを貼って細かく分けてみていきます。

読み込み設定部分

import FollowListTest from "../../resources/js/components/FollowList";
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex'       1
import { config } from '@vue/test-utils'
config.showDeprecationWarnings = false
const axios = require('axios');
jest.mock('axios')

const localVue = createLocalVue()        2

localVue.use(Vuex)    3

1,  Vuexの読み込み

2,  他の影響に考慮したlocalVueの作成もっと詳しく

3,  2で定義したものにVuexを読み込ませます。

Vuex設定部分

describe('FollowListTest', () => {

 let store             1
 let follow            2
 let user              3

 beforeEach(() => {
   follow = {
4    namespaced: true,
5    actions: {
6      get_link: jest.fn(),
       follow_do: jest.fn(),
       get_follow_list: jest.fn(),
       get_follower_list: jest.fn()
     },
7    state: {
       follow_users: jest.fn(),
       url: jest.fn(),
     }
   },
   user = {
     namespaced: true,
     state: {
       show_items: jest.fn(),
       like_items: jest.fn()
     }
   }
8 store = new Vuex.Store({
     modules: {
       follow: follow,
       user: user
     }
   })
 })
})

1,  storeを定義イメージとしてはvuex本体。

2,  vuexの中にあるfollowモジュールを定義。

3, 2と同じくuserモジュールを定義。

4,  名前空間をつけて各メソッドたちがモジュールごとに扱えるようにしています。

5,  followモジュールのアクションとしていくつか定義します。

6,  実際のアクション名を使って、モック化していきます。

7,  stateも同様に必要な要素を書き込んでいきます。

8,  最後にモジュールごとにstoreとして読み込みテストで使用できるようにしています。

テスト部分

it('check_userメソッドでvuexのアクション3つが動く事', done => {
1  const wrapper = shallowMount(FollowerListTest, {
     store,
     localVue,
     methods: {
       first_check() {}
     },
     propsData: {
       login_user_id: 1,
       user_id: 2,
       csrf: 'bcrevbp3q494u3u'
     }
   });

2  wrapper.vm.send();
3  expect(follow.actions.follow_do).toHaveBeenCalled();
   expect(follow.actions.get_follow_list).toHaveBeenCalled();
   expect(follow.actions.get_follower_list).toHaveBeenCalled();
 })

1,  shallowMount()メソッドを使用してvueコンポーネントを作成します。shallowMount()とmountの違いについてはこちら。自分の理解だとmountは全ての要素表示、shallowMountは子コンポーネントがあればまとめてくれる(この”まとめてくれる”の部分をstubって言っているんだと思う)

2,  vmを使用してsend()メソッドを実行します。

3,  toHaveBeenCalled()を使用して以前設定したfollowモジュールのアクションが呼ばれているかを確認出来ます。他のアクションも同様です。

stateについても同様のfollow.state.hoge = ‘fuga’みたいな形で値を設定できたり出来ます。極端な例ですが、下記のようなイメージです。

follow.state.follow_users = 2

expect(follow.state.follow_users.length).toBe(2)

学んだ事

ライフサイクルメソッドのcreated()の中にメソッドがあると、axiosで呼び出した時にエラーになることがある。

vuexのテストもindex.jsの中身に構造を似せて描いていくような形だとわかりました。

まとめ

全くの最初からのスタートでしたが、axios通信やライフサイクル、vuexのテストなど色々出来るようになったので成長を感じます。苦しみましたがw

ただ、watchやvue router, echoがある場合のテストなどは全く触っていないので次の機会に学びたいと思います。

今日はこの辺で!

以上!