Upgrade to Pro — share decks privately, control downloads, hide ads and more …

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

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

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

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

More Decks by 柿本匡章 Masaaki Kakimoto@kara_mage

Other Decks in Programming

Transcript

  1. JestΛ࢖ͬͯ VueίϯϙʔωϯτͱVuexετΞͷ ςετίʔυΛॻ͍ͯΈΑ͏! karamage 2019.6.24 Ginza.js#2

  2. ࣗݾ঺հ

  3. ɾϑϦʔϥϯεͷΤϯδχΞʢΞϓϦ։ൃಘҙʣ karamage IUUQTUXJUUFSDPNLBSB@NBHF ݸਓΞϓϦଟ਺ϦϦʔε

  4. ”ָ͔͕ͯͨ͘͠͠ͳ͍”৬৔Ͱಇ͍ͯ·͢  IUUQTFOHJOFFSPDUDPKQ

  5. ͔͜͜Βຊ୊

  6. ࿩͍ͨ͜͠ͱɹ4ͭ • ςετ͸ԿͷͨΊʹॻ͘ͷ͔ • Jest ηοτΞοϓ • Vueίϯϙʔωϯτͷ୯ମςετ • VuexετΞͷ୯ମςετ

    IUUQTHJUIVCDPNLBSBNBHFWVF@KFTU@UFTU εϥΠυ಺ͷίʔυ
  7. ςετ͸ԿͷͨΊʹॻ͘ͷ͔

  8. JavaScriptͷ ςετίʔυॻ͍͍ͯ·͔͢?

  9. ΋͠ɺςετΛॻ͍ͯͳ͍ͱ

  10. ͕͢͞ʹౖΒΕΔ>< ৗࣝతʹߟ͑ͯςετॻ͔ͳ͍ͱϠόΠงғؾ

  11. ςετίʔυΛॻ͔ͳ͍ཧ༝ ɾॻ͖ํ͕Θ͔Βͳ͍ ɾΊΜͲ͍ ɾٛ຿ײɺ΍Β͞Εײ͕͋Δ ɾςετΛॻ͘ҙ͕ٛΘ͔Βͳ͍

  12. ɾ඼࣭Λ୲อ͢Δ ɾςετࣗಈԽͰίετΛԼ͛Δ ɾෆ҆ΛऔΓআ͘ ɾมԽʹڧ͘͢Δ ςετΛॻ͘ҙٛͱ͸ ɾɾɾ;Ή;Ήɹɹɹ ͍ͬΆ͏ɺt_wada͞Μᐌ͘

  13. Ѫͤͳ͍ίʔυΛॻ͘ʹ͸ ਓੜ͸͋·Γʹ΋୹͍ ࿨ా୎ਓ @t_wada

  14. ίʔυΛѪ͢ΔͨΊʹ ςετΛॻ͜͏

  15. ͍·ɺ͜ͷॠؒ ίʔυʹɺ޲͖߹͑Δ ػձʹɺͨͩɺͨͩ ײँ…

  16. Ѫ͕͋Δײँͷdeploy ςετΛॻ͘ͷ͸ɺѪͷ֬ೝ࡞ۀ

  17. JestΛΠϯετʔϧ͠Α͏

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

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

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

    utils babel-preset-vue-app "scripts": { ..., "test": "jest" }, QBDLBHFKTPO
  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
  22. Jest ࣮ߦ! $ yarn test Θ͘Θ͘

  23. BabelͷΤϥʔͰϋϚΔ ٽ͖ͦ͏ʹͳΔʼʻ

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

    babel-core@^7.0.0-0 @babel/core IUUQTHJUIVCDPNWVFKTWVFKFTUJTTVFT IUUQTHJUIVCDPNGBDFCPPLKFTUJTTVFT
  25. ςετίʔυΛॻ͍ͯΈΑ͏

  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
  27. Jestͷجຊ • test(name, fn)Ͱ୯ҰͷςετΛද͢ɻ • it(name, fn)Ͱ΋ಉ͡ҙຯɻrspecత • expect(value) Ͱςετର৅ͷ஋ΛೖΕΔ

    • toBe(value) Ͱ݁Ռͷ஋ͷݕূΛߦ͏
  28. Vueίϯϙʔωϯτ ͷ୯ମςετΛॻ͜͏ Vuex࢖Θͳ͍৔߹

  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 ϘλϯΛԡ͢ͱΧ΢ϯτΞοϓ͢Δ ̍
  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͕Χ΢ϯτΞοϓ͍ͯ͠Δ͜ͱΛ֬ೝ
  31. Vueίϯϙʔωϯτͱ VuexετΞͷ ୯ମςετΛॻ͜͏

  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 σʔλ͸ετΞʹ࣋ͭ
  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
  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ΛݺΜͩΒ Χ΢ϯτΞοϓ͍ͯ͠Δ͔֬ೝ
  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ͷ୯ମςετͳͷ ͰɺετΞ͸ϞοΫʹஔ͖׵͑Δ
  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͕ ݺͼग़͞Ε͍ͯΔ͔֬ೝ
  37. ·ͱΊ ɾςετΛॻ͍ͨίʔυ͸Ѫ͕͋Δ ɾJest ͸͍͍ͧ ɾετΞͱίϯϙʔωϯτ͸੹຿Λ੾Γ෼͚ͯ ୯ମςετ͠Α͏

  38. Thanks! ͝ਗ਼ௌ͋Γ͕ͱ͏͍͟͝·ͨ͠