Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

ࣗݾ঺հ

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

͔͜͜Βຊ୊

Slide 6

Slide 6 text

࿩͍ͨ͜͠ͱɹ4ͭ • ςετ͸ԿͷͨΊʹॻ͘ͷ͔ • Jest ηοτΞοϓ • Vueίϯϙʔωϯτͷ୯ମςετ • VuexετΞͷ୯ମςετ IUUQTHJUIVCDPNLBSBNBHFWVF@KFTU@UFTU εϥΠυ಺ͷίʔυ

Slide 7

Slide 7 text

ςετ͸ԿͷͨΊʹॻ͘ͷ͔

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

JestΛΠϯετʔϧ͠Α͏

Slide 18

Slide 18 text

Jest Facebook੡ͷJSςετϓϥοτϑΥʔϜ JSςετքͷγΣΞ཰φϯόʔ1 [ಛ௃] • ϒϥ΢βͷىಈ͕ͳ͍ͿΜܰշʹಈ͘(DOMͷΤϛϡ) • εφοϓγϣοτςετ(Ծ૝DOMΛJSONͰμϯϓͯࠩ͠෼ൺֱ)͕Ͱ͖Δ • ςετʹඞཁͳػೳશ෦ೖΓͰָʢςετϥϯφʔɺΞαʔγϣϯɺϞοΫɺΧόϨοδʣ • ઃఆҰͭͰΧόϨοδΛ؆୯ʹऔಘͰ͖Δ • ΢ΥονͰϑΝΠϧมߋ࣌ʹґଘؔ܎ͷ͋Δςετ͚ͩ૸Δɻݡ͍ɻ vue-test-utils vueͷςετ࣌ʹศརͳUtil vueίϯϙʔωϯτͷmountॲཧͯ͘͠ΕΔ Jestͱvue-test-utilsͱ͸? ศར͗ͯ͢ ࢖Θͳ͍ཧ༝͕ͳ͍

Slide 19

Slide 19 text

NuxtʹJestΛೖΕͯΈΔ ಈ࡞؀ڥ ɾNuxt v2.8.1 ɾVue v2.6.10 ɾNode v10.15.3

Slide 20

Slide 20 text

Jest ͱvue-test-utilsΠϯετʔϧ $ yarn add --dev jest vue-jest babel-jest @vue/test- utils babel-preset-vue-app "scripts": { ..., "test": "jest" }, QBDLBHFKTPO

Slide 21

Slide 21 text

Jest ͱvue-test-utilsηοτΞοϓ { "env": { "test": { "presets": [ [ "@babel/preset-env", { "targets": { "node": "current" } } ] ] } } } CBCFMSD module.exports = { moduleNameMapper: { '^@/(.*)$': '/$1', '^~/(.*)$': '/$1', '^vue$': 'vue/dist/vue.common.js' }, moduleFileExtensions: ['js', 'vue', 'json'], transform: { '^.+\\.js$': 'babel-jest', '.*\\.(vue)$': 'vue-jest', }, "collectCoverage": true, "collectCoverageFrom": [ "/components/**/*.vue", "/pages/**/*.vue" ] } KFTUDPOpHKT

Slide 22

Slide 22 text

Jest ࣮ߦ! $ yarn test Θ͘Θ͘

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

ςετίʔυΛॻ͍ͯΈΑ͏

Slide 26

Slide 26 text

ςετͷॻ͖ํͷجຊ@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

Slide 27

Slide 27 text

Jestͷجຊ • test(name, fn)Ͱ୯ҰͷςετΛද͢ɻ • it(name, fn)Ͱ΋ಉ͡ҙຯɻrspecత • expect(value) Ͱςετର৅ͷ஋ΛೖΕΔ • toBe(value) Ͱ݁Ռͷ஋ͷݕূΛߦ͏

Slide 28

Slide 28 text

Vueίϯϙʔωϯτ ͷ୯ମςετΛॻ͜͏ Vuex࢖Θͳ͍৔߹

Slide 29

Slide 29 text

Vueίϯϙʔωϯτ(ςετର৅)
{{ count }} Increment
export default { data() { return { count: 0 } }, methods: { increment() { this.count++ } } } DPNQPOFOUT$PVOUFSWVF ϘλϯΛԡ͢ͱΧ΢ϯτΞοϓ͢Δ ̍

Slide 30

Slide 30 text

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('0') }) // ཁૉͷଘࡏΛ֬ೝ͢Δ͜ͱ΋؆୯Ͱ͢ 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͕Χ΢ϯτΞοϓ͍ͯ͠Δ͜ͱΛ֬ೝ

Slide 31

Slide 31 text

Vueίϯϙʔωϯτͱ VuexετΞͷ ୯ମςετΛॻ͜͏

Slide 32

Slide 32 text

Vueίϯϙʔωϯτ(ςετର৅)
{{ count }} Increment
import { mapGetters, mapActions } from "vuex" export default { computed: { ...mapGetters("count", ["count"]) }, methods: { ...mapActions("count", ["increment"]) } } DPNQPOFOUT$PVOUFS7VFYWVF σʔλ͸ετΞʹ࣋ͭ

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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ΛݺΜͩΒ Χ΢ϯτΞοϓ͍ͯ͠Δ͔֬ೝ

Slide 35

Slide 35 text

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ͷ୯ମςετͳͷ ͰɺετΞ͸ϞοΫʹஔ͖׵͑Δ

Slide 36

Slide 36 text

it('renders the correct markup', () => { expect(wrapper.html()).toContain('0') }) // ཁૉͷଘࡏΛ֬ೝ͢Δ͜ͱ΋؆୯Ͱ͢ 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͕ ݺͼग़͞Ε͍ͯΔ͔֬ೝ

Slide 37

Slide 37 text

·ͱΊ ɾςετΛॻ͍ͨίʔυ͸Ѫ͕͋Δ ɾJest ͸͍͍ͧ ɾετΞͱίϯϙʔωϯτ͸੹຿Λ੾Γ෼͚ͯ ୯ମςετ͠Α͏

Slide 38

Slide 38 text

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