Jest を使って VueコンポーネントとVuexストアの テストコードを書いてみよう!

Jest を使って VueコンポーネントとVuexストアの テストコードを書いてみよう!

・テストは何のために書くのか
・Jest + vue-test-utilsセットアップ
・Vueコンポーネントの単体テスト書いてみよう
・Vuexストアの単体テスト書いてみよう

スライド内のコードは以下に置いてあります。
https://github.com/karamage/vue_jest_test

Transcript

  1. 18.

    Jest Facebook੡ͷJSςετϓϥοτϑΥʔϜ JSςετքͷγΣΞ཰φϯόʔ1 [ಛ௃] • ϒϥ΢βͷىಈ͕ͳ͍ͿΜܰշʹಈ͘(DOMͷΤϛϡ) • εφοϓγϣοτςετ(Ծ૝DOMΛJSONͰμϯϓͯࠩ͠෼ൺֱ)͕Ͱ͖Δ • ςετʹඞཁͳػೳશ෦ೖΓͰָʢςετϥϯφʔɺΞαʔγϣϯɺϞοΫɺΧόϨοδʣ

    • ઃఆҰͭͰΧόϨοδΛ؆୯ʹऔಘͰ͖Δ • ΢ΥονͰϑΝΠϧมߋ࣌ʹґଘؔ܎ͷ͋Δςετ͚ͩ૸Δɻݡ͍ɻ vue-test-utils vueͷςετ࣌ʹศརͳUtil vueίϯϙʔωϯτͷmountॲཧͯ͘͠ΕΔ Jestͱvue-test-utilsͱ͸? ศར͗ͯ͢ ࢖Θͳ͍ཧ༝͕ͳ͍
  2. 20.

    Jest ͱvue-test-utilsΠϯετʔϧ $ yarn add --dev jest vue-jest babel-jest @vue/test-

    utils babel-preset-vue-app "scripts": { ..., "test": "jest" }, QBDLBHFKTPO
  3. 21.

    Jest ͱvue-test-utilsηοτΞοϓ { "env": { "test": { "presets": [ [

    "@babel/preset-env", { "targets": { "node": "current" } } ] ] } } } CBCFMSD module.exports = { moduleNameMapper: { '^@/(.*)$': '<rootDir>/$1', '^~/(.*)$': '<rootDir>/$1', '^vue$': 'vue/dist/vue.common.js' }, moduleFileExtensions: ['js', 'vue', 'json'], transform: { '^.+\\.js$': 'babel-jest', '.*\\.(vue)$': 'vue-jest', }, "collectCoverage": true, "collectCoverageFrom": [ "<rootDir>/components/**/*.vue", "<rootDir>/pages/**/*.vue" ] } KFTUDPOpHKT
  4. 24.

    jest-babel, vue-jest ͷIsuueΛಡΜͰ babel-coreͷόʔδϣϯΛ ௐ੔ͨ͠Β͍͚ͨ $ yarn add --dev babel-jest

    babel-core@^7.0.0-0 @babel/core IUUQTHJUIVCDPNWVFKTWVFKFTUJTTVFT IUUQTHJUIVCDPNGBDFCPPLKFTUJTTVFT
  5. 26.

    ςετͷॻ͖ํͷجຊ@Jest export function sum(x, y) { return x + y

    } import { sum } from "@/logic/sum" test("1 + 2 = 3", () => { expect(sum(1, 2)).toBe(3) }) MPHJDTVNKT UFTUTVNUFTUKT
  6. 29.

    Vueίϯϙʔωϯτ(ςετର৅) <template> <div> <span class="count">{{ count }}</span> <button @click="increment">Increment</button> </div>

    </template> <script> export default { data() { return { count: 0 } }, methods: { increment() { this.count++ } } } </script> DPNQPOFOUT$PVOUFSWVF ϘλϯΛԡ͢ͱΧ΢ϯτΞοϓ͢Δ ̍
  7. 30.

    Vueίϯϙʔωϯτͷςετ import { mount } from '@vue/test-utils' import Counter from

    '@/components/Counter' describe('Counter', () => { // ίϯϙʔωϯτ͕Ϛ΢ϯτ͞Εɺϥού͕࡞੒͞Ε·͢ɻ const wrapper = mount(Counter) it('renders the correct markup', () => { expect(wrapper.html()).toContain('<span class="count">0</span>') }) // ཁૉͷଘࡏΛ֬ೝ͢Δ͜ͱ΋؆୯Ͱ͢ it('has a count label', () => { expect(wrapper.contains(‘.count')).toBe(true) }) // ϘλϯΛԡͯ͠Χ΢ϯτΞοϓ͢Δςετ it('button click should increment the count', () => { expect(wrapper.vm.count).toBe(0) const button = wrapper.find('button') button.trigger(‘click') expect(wrapper.vm.count).toBe(1) }) }) UFTU$PVOUFSTQFDWVF JE΍DMBTTͷ$44ηϨΫλΛࢦఆ͢Δ͜ͱ΋Մೳ )5.-Λจࣈྻͱͯ͠νΣοΫ͢Δ ͨͩɺ$44ʹґଘͨ͠ςετ͸ඍົɻEBUBUFTUଐੑΛ࢖͏΂͠ WNDPVOU͕Χ΢ϯτΞοϓ͍ͯ͠Δ͜ͱΛ֬ೝ
  8. 32.

    Vueίϯϙʔωϯτ(ςετର৅) <template> <div> <span class="count">{{ count }}</span> <button @click="increment">Increment</button> </div>

    </template> <script> import { mapGetters, mapActions } from "vuex" export default { computed: { ...mapGetters("count", ["count"]) }, methods: { ...mapActions("count", ["increment"]) } } </script> DPNQPOFOUT$PVOUFS7VFYWVF σʔλ͸ετΞʹ࣋ͭ
  9. 33.

    VuexετΞ(ςετର৅) export const state = () => ({ count: 0

    }) export const mutations = { setCount: (state, { count }) => state.count = count } export const getters = { count: state => state.count, } export const actions = { async increment({ commit, state }, {}) { commit("setCount", { count: state.count + 1 }) }, } TUPSFDPVOUKT
  10. 34.

    import Vuex from 'vuex' import * as count from '@/store/count'

    import { createLocalVue } from '@vue/test-utils' const localVue = createLocalVue() localVue.use(Vuex) let action const testedAction = (context = {}, payload = {}) => { return count.actions[action](context, payload) } describe('store/count.js', () => { let store beforeEach(() => { store = new Vuex.Store(count) }) describe('getters', () => { test('countͷ஋Λऔಘ', () => { store.replaceState({ count: 3 }) expect(store.getters['count']).toBe(3) }) }) describe('actions', () => { let commit let state beforeEach(() => { commit = store.commit state = store.state }) test('increment', async done => { action = "increment" await testedAction({ commit, state }) expect(store.getters['count']).toBe(1) await testedAction({ commit, state }) expect(store.getters['count']).toBe(2) done() }) }) VuexετΞͷ୯ମςετ UFTUTUPSFDPVOUTQFDKT ଞͷςετʹӨڹΛ༩͑ͳ͍Α͏ʹ ඇಉظͷςετ͸͜ͷΑ͏ʹॻ͘ EPOF๨ΕΔͳ ςετͷ౓ʹετΞΛॳظԽ TUBUF͕มΘͬͨͱ͖ HFUUFSͰऔಘͰ͖͍ͯΔ͔֬ೝ JODSFNFOUΛݺΜͩΒ Χ΢ϯτΞοϓ͍ͯ͠Δ͔֬ೝ
  11. 35.

    import { shallowMount, createLocalVue } from '@vue/test-utils' import Vuex from

    'vuex' import CounterVuex from '@/components/CounterVuex' const localVue = createLocalVue() localVue.use(Vuex) describe('CounterVuex.vue', () => { let store let countStoreMock let wrapper beforeEach(() => { //VuexετΞͷϞοΫΛ࡞੒͢Δ countStoreMock = { namespaced: true, actions : { increment: jest.fn(), }, getters : { count: () => 0, }, } store = new Vuex.Store({ modules: { count:countStoreMock } }) // shallowMountͩͱࢠίϯϙʔωϯτΛελϒʹΑͬͯඳը͠ͳ͘ͳΔ(ߴ଎Խ) wrapper = shallowMount(CounterVuex, { store, localVue }) }) VuexΛ࢖͏ Vueίϯϙʔωϯτͷ୯ମςετ UFTU$PVOU7VFYTQFDKT ɾɾˣ࣍ϖʔδʹଓ͘ lίϯϙʔωϯτzͷ୯ମςετͳͷ ͰɺετΞ͸ϞοΫʹஔ͖׵͑Δ
  12. 36.

    it('renders the correct markup', () => { expect(wrapper.html()).toContain('<span class="count">0</span>') })

    // ཁૉͷଘࡏΛ֬ೝ͢Δ͜ͱ΋؆୯Ͱ͢ it('has a button', () => { expect(wrapper.contains('button')).toBe(true) }) // ϘλϯΛԡͯ͠inclement͕ݺͼग़͞Ε͍ͯΔ͔ςετ it('button click should increment call', () => { expect(countStoreMock.actions.increment).not.toBeCalled() const button = wrapper.find('button') button.trigger('click') expect(countStoreMock.actions.increment).toBeCalled() }) }) VuexΛ࢖͏Vueίϯϙʔωϯτͷ୯ମςετ UFTU$PVOU7VFYTQFDKT ϘλϯΛԡͨ͠ͱ͖ϞοΫͷJODSFNFOU͕ ݺͼग़͞Ε͍ͯΔ͔֬ೝ