$30 off During Our Annual Pro Sale. View Details »

1年間単体テストを書き続けた現場から送る Vue Component のテスト / Vue Component Test

1年間単体テストを書き続けた現場から送る Vue Component のテスト / Vue Component Test

Vue Fes Japan 2018 の 1年間単体テストを書き続けた現場から送る Vue Component のテスト
https://vuefes.jp/

Kazuyoshi Tsuchiya

November 03, 2018
Tweet

More Decks by Kazuyoshi Tsuchiya

Other Decks in Technology

Transcript

  1. ౔԰࿨ྑ(.01FQBCP *OD
    7VF'FT+BQBO
    ೥ؒ୯ମςετΛ
    ॻ͖ଓ͚ͨݱ৔͔ΒૹΔ
    7VF$PNQPOFOUͷςετ

    View Slide

  2. IUUQTUTVDIJLB[VOFU
    (.0ϖύϘ&$ࣄۀ෦ςΫχΧϧϦʔυ
    ౔԰࿨ྑ!UTVDIJLB[V
    ୲౰αʔϏεΧϥʔϛʔϦϐʔτ

    View Slide

  3. View Slide

  4. View Slide

  5. View Slide

  6. $PNQPOFOUͷςετ
    ॻ͍͍ͯΔਓ

    View Slide

  7. Α͘ฉ͘੠
    6*ͷςετ͸มߋ͕ൃੜ͠΍͘͢
    ίετʹݟ߹Θͳ͍ΜͩΑͶ
    େࣄͳϩδοΫ͸$PNQPOFOUͷ
    ֎ʹग़ͯ͠ςετ͍ͯ͠Δ͔Β
    ͍Βͳ͍͔ͳͱ

    View Slide

  8. ͦ͏͸ݴͬͯ΋
    ͔ͩΒ͔ͬ͠Γͱ
    ಈ࡞֬ೝ͠Α͏
    ςετ͕ͳ͍ͱ
    ҙਤ͠ͳ͍มߋ͕ى͖ͳ͍͔ෆ҆ʜ

    View Slide

  9. ָͰ͖Δͱ͜Ζ͸ָͯ͠
    ෆ҆ղফ͍͖͍ͯͨ͠

    View Slide

  10. ͦͷͨΊʹ
    ݱ৔Ͱ΍͍ͬͯΔ͜ͱ

    View Slide

  11. w$PNQPOFOUͷԿΛςετ͢Δͷ͔ʁ
    wͲ͏΍ͬͯςετ͢Δʁ
    w-JGFDZDMFฤ
    w1SPQT7VFY4UBUFฤ
    w6TFS*OUFSBDUJPOฤ
    "HFOEB

    View Slide

  12. ԿΛςετ͢Δʁ

    View Slide

  13. ֎෦͔ΒݟͨৼΔ෣͍Λ
    ςετ͢Δ

    View Slide

  14. DPNQPOFOUͷςετ΋
    ͦ͏Ͱͳ͍ςετͱ
    ߟ͑ํ͸Ұॹ

    View Slide

  15. !UXBEB
    2ϓϥΠϕʔτϝιουͷϢχοτςετ
    ͸ॻ͔ͳ͍΋ͷʁ
    ϓϥΠϕʔτϝιουͷϢχοτςετ͸ॻ͔ͳ͍΋ͷʁ2"!*5
    IUUQTRBBUNBSLJUDPKQR
    "୹͘·ͱΊΔͱɺϓϥΠϕʔτͳϝιο
    υͷςετΛॻ͘ඞཁ͸ແ͍ͱߟ͍͑ͯ·
    ͢ɻ
    தུ
    ϓϥΠϕʔτϝιου͸࣮૷ͷৄࡉͰ
    ͋ΓɺࣗಈςετͷλʔήοτͱͳΔʮ֎
    ෦͔ΒݟͨৼΔ෣͍ʯͰ͸͋Γ·ͤΜɻ

    View Slide

  16. ૊৫ʹςετΛॻ͘จԽΛࠜ෇͔ͤΔઓུͱઓज़IUUQTTQFBLFSEFDLDPNUXBEB
    TUSBUFHZBOEUBDUJDTPGCVJMEJOHBVUPNBUFEUFTUJOHDVMUVSFJOUPPSHBOJ[BUJPO TMJEF

    View Slide

  17. $PNQPOFOUͰ͍͏ͱʁ

    View Slide

  18. 1VCMJD*OUFSGBDF
    $PNQPOFOU
    -JGFDZDMF
    7VFY
    4UBUF
    1SPQT
    6TFS
    *OUFSBDUJPO

    View Slide

  19. 0VUQVU
    7VFY
    "DUJPO
    &WFOU
    )5.-
    $44
    $PNQPOFOU

    View Slide

  20. 7VFY
    "DUJPO
    &WFOU
    )5.-
    $44
    $PNQPOFOU
    -JGFDZDMF
    7VFY
    4UBUF
    1SPQT
    6TFS
    *OUFSBDUJPO
    7VF/:$$PNQPOFOU5FTUTXJUI7VFKT.BUU0$POOFMM:PV5VCFIUUQTXXXZPVUVCFDPNXBUDI W0*QG855IS,
    ςετλʔήοτ

    View Slide

  21. NPVOUPSTIBMMPXNPVOU
    $PNQPOFOU
    NPVOU
    $IJME
    $PNQPOFOU
    $PNQPOFOU
    $IJME
    $PNQPOFOU
    TIBMMPXNPVOU


    Parent





    Parent



    View Slide

  22. wجຊతʹNPVOU
    wʮ֎෦͔ΒΈͨৼΔ෣͍ʯ
    wʮઃܭͷՄಈҬΛ֬อʯ
    wର৅DPNQPOFOU
    wSPVUFS͔ΒಡΈࠐΉ͍ΘΏΔ1BHF
    $PNQPOFOU
    wෳࡶͳPSڞ௨Ͱ࢖༻͢Δ$PNQPOFOU
    w1BHF$PNQPOFOUͰΧόʔ͕໘౗
    ࠓͷϓϩδΣΫτͰ͸

    View Slide

  23. w$PNQPOFOUͷԿΛςετ͢Δͷ͔ʁ
    wͲ͏΍ͬͯςετ͢Δʁ
    w-JGFDZDMFฤ
    w1SPQT7VFY4UBUFฤ
    w6TFS*OUFSBDUJPOฤ
    "HFOEB

    View Slide

  24. Ͳ͏΍ͬͯςετ͢Δʁ

    View Slide

  25. +FTU
    .PDIB
    ,BSNB
    7VF5FTU6UJMT
    *TUBOCVM
    $IBJ
    QPXFSBTTFSU
    4JOPO
    Α͘࢖͏ςετπʔϧͷ
    ໾ׂΛ੔ཧ͠Α͏

    View Slide

  26. NPDLϥΠϒϥϦ
    ςεςΟϯά
    ϑϨʔϜϫʔΫ
    ςετϥϯφʔ
    ࣮ߦ؀ڥ
    "TTFSUJPO
    ࣮ϒϥ΢β KTEPN
    ,BSNB
    .PDIB
    +FTU
    $PWFSBHF
    4JOPO
    *TUBOCVM
    $PNQPOFOU
    5FTU6UJMJUZ
    7VF5FTU6UJMT
    $IBJQPXFSBTTFSU

    View Slide

  27. NPDLϥΠϒϥϦ
    ςεςΟϯά
    ϑϨʔϜϫʔΫ
    ςετϥϯφʔ
    ࣮ߦ؀ڥ
    "TTFSUJPO
    ࣮ϒϥ΢β KTEPN
    ,BSNB
    .PDIB
    +FTU
    $PWFSBHF
    4JOPO
    $IBJQPXFSBTTFSU
    *TUBOCVM
    $PNQPOFOU
    5FTU6UJMJUZ
    7VF5FTU6UJMT
    ࣮ϒϥ΢β͕
    ඞཁͳͱ͖
    ࣮ϒϥ΢β͕
    ෆཁͳͱ͖

    View Slide

  28. -JGFDZDMF
    ʹର͢Δςετίʔυ

    View Slide

  29. -JGFDZDMFͷςετ
    7VFY
    "DUJPO
    &WFOU
    )5.-
    $44
    $PNQPOFOU
    -JGFDZDMF

    View Slide

  30. wΑ͘ॲཧ͕ॻ͔ΕΔMJGFDZDMFIPPL
    wDSFBUFE
    wNPVOUFE
    w/VYUͳͲ443͍ͯ͠Δ৔߹
    wBTZOD%BUB
    wGFUDI
    -JGFDZDMFͷςετ

    View Slide

  31. ࣮૷ྫ
    -JGFDZDMF
    created () {
    this.loadPosts()
    },
    methods: {
    ...mapActions(['fetchPosts']),
    async loadPosts () {
    this.message = 'ಡΈࠐΈத...'
    try {
    await this.fetchPosts()
    } finally {
    this.message = ‘'
    }
    }
    },
    .FUIPE
    ݺͼग़͠

    View Slide

  32. ࣮૷ྫ
    -JGFDZDMF
    created () {
    this.loadPosts()
    },
    methods: {
    ...mapActions(['fetchPosts']),
    async loadPosts () {
    this.message = 'ಡΈࠐΈத...'
    try {
    await this.fetchPosts()
    } finally {
    this.message = ‘'
    }
    }
    },
    දࣔมߋ

    View Slide

  33. ࣮૷ྫ
    -JGFDZDMF
    created () {
    this.loadPosts()
    },
    methods: {
    ...mapActions(['fetchPosts']),
    async loadPosts () {
    this.message = 'ಡΈࠐΈத...'
    try {
    await this.fetchPosts()
    } finally {
    this.message = ‘'
    }
    }
    },
    "DUJPO
    EJTQBUDI

    View Slide

  34. ςετྫ
    -JGFDZDMF
    // ϩʔΧϧͳVueίϯετϥΫλʹVuexΛΠϯετʔϧ
    const localVue = createLocalVue()
    localVue.use(Vuex)
    beforeEach(() => {
    // fetchPostsΞΫγϣϯͷಈ࡞֬ೝͷͨΊͷVuexपΓͷઃఆ
    actions = {
    fetchPosts: sinon.stub() // fetchPostsΞΫγϣϯͷϞοΫ
    }
    store = new Vuex.Store({
    actions
    })
    wrapper = mount(Posts, {store, localVue })
    })
    ͓ܾ·Γ

    View Slide

  35. ςετྫ
    -JGFDZDMF
    // ϩʔΧϧͳVueίϯετϥΫλʹVuexΛΠϯετʔϧ
    const localVue = createLocalVue()
    localVue.use(Vuex)
    beforeEach(() => {
    // fetchPostsΞΫγϣϯͷಈ࡞֬ೝͷͨΊͷVuexपΓͷઃఆ
    actions = {
    fetchPosts: sinon.stub() // fetchPostsΞΫγϣϯͷϞοΫ
    }
    store = new Vuex.Store({
    actions
    })
    wrapper = mount(Posts, {store, localVue })
    })
    TUPSFͷϞοΫΛ
    ࡞੒

    View Slide

  36. ςετྫ
    -JGFDZDMF
    it('loadingදࣔ͞ΕΔ͜ͱ', async () => {
    expect(wrapper.text()).to.contain('ಡΈࠐΈத...')
    })
    it('fetchPosts͕dispatch͞ΕΔ͜ͱ', () => {
    sinon.assert.calledOnce(actions.fetchPosts)
    })
    it('dispatch੒ޭ͢Δͱɺloadingද͕ࣔফ͑Δ͜ͱ', async () => {
    actions.fetchPosts.resolves()
    await flushPromises()
    expect(wrapper.text()).not.to.contain('ಡΈࠐΈத...')
    })

    View Slide

  37. w࣮ࡍʹ͸ɺදࣔʹඞཁͳσʔλऔಘͷͨΊʹ
    EJTQBUDI͢Δ͙Β͍
    w͜ΕΛςετͯ͠΋ಘΒΕΔ҆৺ײ͸௿͍
    wଞͷςετΛॻ͍ͨ΄͏͕Α͍
    w-JGFDZDMFIPPL͸NPDLԽͯ͠ɺଞͷςετ
    ͷअຐʹͳΒͳ͍Α͏ʹ͢Δͷ΋ͭͷख
    -JGFDZDMFͷςετΛͯ͠Έͯ

    View Slide

  38. w$PNQPOFOUͷԿΛςετ͢Δͷ͔ʁ
    wͲ͏΍ͬͯςετ͢Δʁ
    w-JGFDZDMFฤ
    w1SPQT7VFY4UBUFฤ
    w6TFS*OUFSBDUJPOฤ
    "HFOEB

    View Slide

  39. 1SPQT7VFY4UBUF
    ʹର͢Δςετίʔυ

    View Slide

  40. 1SPQT7VFY4UBUFͷςετ
    )5.-
    $44
    $PNQPOFOU
    7VFY
    4UBUF
    1SPQT

    View Slide

  41. ࣮૷ྫ
    දࣔ

    ·ͩ౤ߘ͸͋Γ·ͤΜ



    λΠτϧ
    ಺༰




    {{post.title}}
    {{post.body}}




    ݅ͷͱ͖
    දࣔΛม͑Δ

    View Slide

  42. ςετྫ
    ୯७ͳBTTFSU
    describe('౤ߘ͕0݅ͷ৔߹', () => {
    beforeEach(() => wrapper.setProps({posts: []})
    it('0݅ͷද͕ࣔ͞Ε͍ͯΔ͜ͱ', () => {
    expect(wrapper.text()).to.contain('·ͩ౤ߘ͸͋Γ·ͤΜ')
    })
    })
    describe('౤ߘ͕1݅Ҏ্ͷ৔߹', () => {
    const post = {id: 1, title: ‘λΠτϧ’, body: ‘body’}
    beforeEach(() => wrapper.setProps({posts: [post]})
    it('౤ߘͷ಺༰͕දࣔ͞Ε͍ͯΔ͜ͱ', () => {
    expect(wrapper.text()).to.contain(post.title)
    })
    })

    View Slide

  43. w FYQFDU XSBQQFSUFYU

    UPDPOUBJO b·ͩ౤ߘ
    ͸͋Γ·ͤΜ`

    wදࣔมΘͬͨΒɺςετ΋௚͢
    w FYQFDU XSBQQFSUFYU

    UPDPOUBJO QPTUYY

    w Ͳ͜ʹදࣔ͞Ε͍ͯΔʁͬ͘͟Γ͗͢͠
    wࡉ͔ͨ͘͠Βϝϯςେม
    ୯७ͳBTTFSUͷͭΒ͞
    ˠ͜ΕΒΛ͍͍ײ͡ʹ΍Γ͍ͨ

    View Slide

  44. 4OBQTIPU5FTUJOH

    View Slide

  45. NPDLϥΠϒϥϦ
    ςεςΟϯά
    ϑϨʔϜϫʔΫ
    ςετϥϯφʔ
    ࣮ߦ؀ڥ
    "TTFSUJPO
    ࣮ϒϥ΢β KTEPN
    ,BSNB
    .PDIB
    +FTU
    $PWFSBHF
    4JOPO
    $IBJQPXFSBTTFSU
    *TUBOCVM
    $PNQPOFOU
    5FTU6UJMJUZ
    7VF5FTU6UJMT
    ͜Εͩͱ
    Մೳ

    View Slide

  46. w DPNQPOFOUͷ%0.ʢTOBQTIPUʣΛൺֱ͢Δ
    w ίʔυमਖ਼લͷ%0.Λظ଴஋ͱͯ͠ɺࠓͷ%0.
    ͱൺֱ
    4OBQTIPU5FTUJOH

    View Slide

  47. w ࠩ෼͕ͳ͚Ε͹TVDDFTT
    w ࠩ෼͕͋Ε͹Ұ౓͸GBJM
    w ։ൃऀ͕ࠩ෼Λ֬ೝ
    w ҙਤతͳมߋͰ͋Ε͹

    มߋޙͷ%0.Λ࣍ͷظ଴஋ʹ࢖͏Α͏ʹ

    VQEBUF͢Δ
    4OBQTIPU5FTUJOH

    View Slide

  48. w %0.ͷมߋ͸͍͍ײ͡ʹςετͰ͖ΔΑ͏ʹ
    ͳΔ
    w DPNQPOFOU͸ɺ)5.-$44
    w $44ͷςετ΋͍ͨ͠
    4OBQTIPU5FTUJOHͷͭΒ͞

    View Slide

  49. 7JTVBM5FTUJOH

    View Slide

  50. w 4OBQTIPU5FTUJOHͷը૾൛
    w ը૾ͷࠩ෼ݕग़πʔϧ͕ผʹඞཁ
    w ৭ʑͳํ๏͕͋Δ
    7JTVBM5FTUJOH

    View Slide

  51. ࢲ͕࠾༻ͨ͠
    ࠷ߴͷ7JTVBM5FTUJOHͷํ๏
    ʙ1SPQT7VFY4UBUFฤʙ

    View Slide

  52. View Slide

  53. w QSPQT΍7VFYTUBUFΛ༩͑ͯදࣔύλʔϯΛ֬ೝͰ͖Δ
    w DPNQPOFOU୯ҐͳͷͰΤοδέʔεʹ΋ରԠԽ
    w ؆୯ʹTDSFFOTIPU͕ͱΕΔ
    w ࠓ͸[JTVJΛ࢖͍ͬͯΔ
    4UPSZCPPLͱ͸
    IUUQTTUPSZCPPLTWVFOFUMJGZDPN

    View Slide

  54. 4UPSZCPPLͰQSPQTΛ౉͢
    storiesOf('propsΛड͚औΔcomponent', module)
    .add('ۭͷͱ͖', () => ({
    components: { PostsByProps },
    template: '',
    data: () => ({
    posts: []
    })
    }))
    .add('౤ߘ͕͋Δͱ͖', () => ({
    components: { PostsByProps },
    template: '',
    data: () => ({
    posts: [{id: 1, title: ‘title', body: 'ϘσΟ'}]
    })
    }))

    View Slide

  55. 4UPSZCPPLͰ7VFY4UBUFΛ౉͢
    storiesOf('vuex storeΛड͚औΔcomponentྫ', module)
    .add('ۭͷͱ͖', () => ({
    components: { PostsByStore },
    template: '',
    store: new Vuex({
    state: () => ({
    posts: []
    })
    })
    }))
    .add('౤ߘ͕͋Δͱ͖', () => ({
    components: { PostsByStore },
    template: '',
    store: new Vuex({
    state: () => ({
    posts: [{id: 1, title: 'λΠτϧ1', body: 'ϘσΟ'}]
    })
    })
    }))

    View Slide

  56. w7VFYʹґଘ͍ͯ͠ΔDPNQPOFOU͸TUPSFΛ४උ͢
    Δͷ͕໘౗
    w͚ͩͲɺ7JTVBM5FTU͍ͨ͠$PNQPOFOU
    w1SFTFOUBUJPOBMͱ$POUBJOFS$PNQPOFOUʹ෼
    ͚Δ
    w൚༻తͳϞοΫTUPSFΛ४උͯ͠ɺશͯͷ
    DPNQPOFOUͰ࢖͍ճ͢
    wࠓͷॴɺͬͪ͜Ͱ΍ͬͯΔ
    4UPSZCPPLॻ͍ͯΈͯ

    View Slide

  57. ݱ৔ͷίʔυ
    import { state, getters, mutations, actions } from ‘@/store'
    function createStubStore () {
    const store = new Vuex.Store({state, getters, mutations, actions})
    sinon.stub(store, 'dispatch').resolves()
    store.state.posts = [{id: 1, title: 'λΠτϧ1', body: 'ϘσΟ'}]
    // ࣮ࡍ͸͜͜ͷstate४උ͕͍ͬͺ͍͋Δ
    return store
    }
    const emptyPostsStore = createStubStore()
    emptyPostsStore.state.posts = []
    storiesOf('Posts', module)
    .add('ۭͷͱ͖', () => ({
    components: { Posts },
    template: '',
    store: emptyPostsStore,
    }))
    ຊ෺ͷTUPSFΛϕʔεʹ
    EJTQBUDINFUIPEͱ
    TUBUFΛϞοΫԽ͢Δ

    View Slide

  58. ݱ৔ͷίʔυ
    import { state, getters, mutations, actions } from ‘@/store'
    function createStubStore () {
    const store = new Vuex.Store({state, getters, mutations, actions})
    sinon.stub(store, 'dispatch').resolves()
    store.state.posts = [{id: 1, title: 'λΠτϧ1', body: 'ϘσΟ'}]
    // ࣮ࡍ͸͜͜ͷstate४උ͕͍ͬͺ͍͋Δ
    return store
    }
    const emptyPostsStore = createStubStore()
    emptyPostsStore.state.posts = []
    storiesOf('Posts', module)
    .add('ۭͷͱ͖', () => ({
    components: { Posts },
    template: '',
    store: emptyPostsStore,
    }))
    ൚༻తͳ
    TUPSFΛඍௐ੔ͯ͠
    TUPSZ΁

    View Slide

  59. w7JTVBM5FTUJOHΛ։ൃϑϩʔʹࡌͤΔͷʹඞཁ
    ͳػೳΛඋ͍͑ͯΔπʔϧ
    wը૾ͷࠩ෼நग़
    wࠩ෼ͷSFQPSUΛIUNMʹग़ྗͯ͠ɺT΁Ξοϓ
    ϩʔυ
    w(JUIVC΁ͷ௨஌
    SFHTVJUͱ͸

    View Slide

  60. w$PNQPOFOUΛमਖ਼ͯ͠GFBUVSFCSBODI΁QVTI
    TUPSZCPPLSFHTVJUͷ։ൃϑϩʔ
    IPPL
    QVTI

    View Slide

  61. JO$*
    TUPSZCPPLSFHTVJUͷ։ൃϑϩʔ
    TDSFFOTIPU
    CZ[JTVJ
    ෼ذݩͷը૾
    DPNQBSF
    GFBUVSFCSBODIը૾
    DPNNFOU
    SFHTVJU

    View Slide

  62. TUPSZCPPLSFHTVJUͷ։ൃϑϩʔ

    View Slide

  63. TUPSZCPPLSFHTVJUͷ։ൃϑϩʔ
    #FGPSF
    "GUFS
    ࠩ෼
    13"QQSPWF

    View Slide

  64. wDPNQPOFOU୯ҐͰͷςετ͕Մೳ
    w։ൃϑϩʔ͕ීஈͷ։ൃͱҰॹ
    wTUPSZCPPL͕؆୯
    wσβΠφ͞Μ΋ฤूͯ͘͠ΕΔ
    wແྉ
    w༗ঈπʔϧ΋͍ͬͺ͍͋Δ
    ࠷ߴϙΠϯτ

    View Slide

  65. wϦϑΝΫλ࣌΋ػೳ௥Ճ࣌΋ɺͱͯ΋҆৺
    wϨϏϡʔґཔ΍ϨϏϡʔͷෛՙ͕Լ͕Δ
    w7JTVBM5FTUͷ݁Ռ͚ͩͰͳ͘ɺͦΕͱ͸ผʹ
    $*Ͱ13͝ͱʹTUPSZCPPL΋σϓϩΠ
    wϨϏϡʔ࣌ʹຖճDIFDLPVU΍ɺಈ࡞֬ೝ༻
    ͷ؀ڥ͕ෆཁ
    7JTVBM5FTU΍ͬͯΈͯ

    View Slide

  66. wΫϩεϒϥ΢βରԠ
    wTUPSZCPPLͷTDSFFOTIPU͸QVQQFUFFS
    w༗ྉαʔϏε΋ݕ౼
    ͜Ε͔ΒͷվળϙΠϯτ

    View Slide

  67. w$PNQPOFOUͷԿΛςετ͢Δͷ͔ʁ
    wͲ͏΍ͬͯςετ͢Δʁ
    w-JGFDZDMFฤ
    w1SPQT7VFY4UBUFฤ
    w6TFS*OUFSBDUJPOฤ
    "HFOEB

    View Slide

  68. 6TFS*OUFSBDUJPO
    ʹର͢Δςετίʔυ

    View Slide

  69. 6TFS*OUFSBDUJPO
    7VFY
    "DUJPO
    &WFOU
    )5.-
    $44
    $PNQPOFOU
    6TFS
    *OUFSBDUJPO

    View Slide

  70. wϑΥʔϜೖྗ
    wϘλϯΫϦοΫ
    wεΫϩʔϧ
    w%%
    wεϫΠϓ
    wϐϯνΠϯɺΞ΢τ
    wFUD
    6TFS*OUFSBDUJPOͱ͸

    View Slide

  71. wϑΥʔϜೖྗ
    wϘλϯΫϦοΫ
    wεΫϩʔϧ
    w%%
    wεϫΠϓ
    wϐϯνΠϯɺΞ΢τ
    wFUD
    6TFS*OUFSBDUJPOͱ͸
    ςετ೉қ౓௿
    ςετ೉қ౓ߴ
    ͬͪ͜ͷΈΛςετ͍ͯ͠Δ

    View Slide

  72. ࣮૷ྫ
    6TFS*OUFSBDUJPO


    ৽ن࡞੒


    λΠτϧ
    v-validate="'required'">

    {{ errors.first('title') }}



    ಺༰
    v-validate="'required'">
    {{ errors.first('body') }}

    อଘ



    JOQVUUFYUY
    #VUUPOY
    γϯϓϧͳGPSN

    View Slide

  73. ࣮૷ྫ
    6TFS*OUFSBDUJPO


    ৽ن࡞੒


    λΠτϧ
    v-validate="'required'">

    {{ errors.first('title') }}



    ಺༰
    v-validate="'required'">
    {{ errors.first('body') }}

    อଘ



    WFFWBMJEBUF
    ͰඞਢνΣοΫ

    View Slide

  74. ࣮૷ྫ
    6TFS*OUFSBDUJPO
    export default {
    data: () => ({ post: { title: '', body: '' } }),
    methods: {
    async save () {
    const isValid = await this.$validator.validate()
    if (isValid) {
    await this.$store.dispatch(
    'createPost', { post: this.post }
    )
    this.$router.push({ path: '/' })
    }
    }
    }
    }
    WBMJEBUFͯ͠
    BDUJPOEJTQBUDI

    View Slide

  75. ςετྫ
    XSBQQFS४උ
    // ϩʔΧϧͳVueίϯετϥΫλʹVuex/VeeValidateΛΠϯετʔϧ
    const localVue = createLocalVue()
    localVue.use(Vuex)
    localVue.use(VeeValidate)
    beforeEach(() => {
    // Vue RouterͷϞοΫઃఆ
    $router = { push: sinon.stub() }
    // createPostΞΫγϣϯͷಈ࡞֬ೝͷͨΊͷVuexपΓͷઃఆ
    actions = { createPost: sinon.stub() }
    store = new Vuex.Store({
    actions
    })
    wrapper = mount(NewPost, { mocks: { $router }, store,
    localVue, sync: false })
    })
    TUPSFͷϞοΫΛ
    ࡞੒NPVOU

    View Slide

  76. ςετྫ
    ೖྗͯ͠DMJDLͨ͠ͱ͖
    describe('ೖྗͯ͠อଘϘλϯΛclickͨ͠ͱ͖', () => {
    beforeEach(async () => {
    wrapper.find('#title').setValue('title')
    wrapper.find('#body').setValue('body')
    wrapper.find('#create-button').trigger('click')
    await flushPromises()
    })
    it('ೖྗ͞Εͨ಺༰Ͱaction͕dispatch͞ΕΔ͜ͱ', () => {
    sinon.assert.calledWithMatch(actions.createPost, {},
    { post: { title: 'title', body: 'body' } })
    })
    })
    ೖྗޙʹϘλϯDMJDLͷͱ͖
    EJTQBUDI͞ΕΔ

    View Slide

  77. ςετྫ
    ೖྗͤͣʹDMJDLͨ͠ͱ͖
    describe('ະೖྗͰอଘϘλϯΛclickͨ͠ͱ͖', () => {
    beforeEach(async () => {
    wrapper.find('#create-button').trigger('click')
    await flushPromises()
    })
    it('validationΤϥʔ͕දࣔ͞ΕΔ͜ͱ', () => {
    expect(wrapper.text()).to.contain(
    'The title field is required.')
    })
    it('action͕dispatch͞Εͳ͍͜ͱ', () => {
    sinon.assert.notCalled(actions.createPost)
    })
    }) ະೖྗͰϘλϯDMJDLͷͱ͖
    WBMJEBUJPOΤϥʔදࣔEJTQBUDI͞Εͳ͍

    View Slide

  78. ςετྫ
    ೖྗͤͣʹDMJDLͨ͠ͱ͖
    describe('ະೖྗͰอଘϘλϯΛclickͨ͠ͱ͖', () => {
    beforeEach(async () => {
    wrapper.find('#create-button').trigger('click')
    await flushPromises()
    })
    it('validationΤϥʔ͕දࣔ͞ΕΔ͜ͱ', () => {
    expect(wrapper.text()).to.contain(
    'The title field is required.')
    })
    it('action͕dispatch͞Εͳ͍͜ͱ', () => {
    sinon.assert.notCalled(actions.createPost)
    })
    }) ද͕ࣔมΘͬͨ෦෼Λ
    ୯७ͳBTTFSUͯ͠Δ

    View Slide

  79. wྫ͑͹

    WBMJEBUJPOΤϥʔৄࡉΛ։͘ด͡Δ

    Ϙλϯԡͯ͠Ϟʔμϧ։͘FUD
    wཁ͸ςετͷதͰɺTOBQTIPUΛࡱΓ͍ͨ
    6TFS*OUFSBDUJPOޙͷը૾ΛࡱΓ͍ͨ
    wrapper.find('#create-button').trigger('click')
    screenshot(‘ະೖྗͰͷclickޙ.png’)

    View Slide

  80. NPDLϥΠϒϥϦ
    ςεςΟϯά
    ϑϨʔϜϫʔΫ
    ςετϥϯφʔ
    ࣮ߦ؀ڥ
    "TTFSUJPO
    ࣮ϒϥ΢β KTEPN
    ,BSNB
    .PDIB
    +FTU
    $PWFSBHF
    4JOPO
    $IBJQPXFSBTTFSU
    *TUBOCVM
    $PNQPOFOU
    5FTU6UJMJUZ
    7VF5FTU6UJMT
    ͜Εͩͱ
    Մೳ

    View Slide

  81. wLBSNBOJHIUNBSF
    wLBSNBͰOJHIUNBSF FMFDUSPO
    ্ͰςετΛ
    ࣮ߦͰ͖Δ΋ͷ
    wTDSFFOTIPU"1*Λఏڙͯ͘͠ΕΔ
    wը૾͕ࡱΕͨΒTUPSZCPPLͷը૾ͱҰॹʹ
    SFHTVJUͷϑϩʔʹࡌͤΔ
    5FTUதʹTDSFFOTIPUࡱΔʹ͸

    View Slide

  82. w؆୯ͳ6TFS*OUFSBDUJPOͷςετ͸େม͡Όͳ͍
    wॏཁͳ'PSN͚ͩͰ΋΍͓ͬͯ͘ͱ͍͍
    wͦΕҎ֎͸ఘΊ΋؊৺
    w6TFS*OUFSBDUJPOޙͷTDSFFOTIPUͱͬͯɺ7JTVBM
    5FTU͸΍ͬͺΓศར
    6TFS*OUFSBDUJPOͷςετͯ͠Έͯ

    View Slide

  83. ·ͱΊ

    View Slide

  84. 7VFY
    "DUJPO
    &WFOU
    )5.-
    $44
    $PNQPOFOU
    -JGFDZDMF
    7VFY
    4UBUF
    1SPQT
    6TFS
    *OUFSBDUJPO
    ςετλʔήοτ

    View Slide

  85. ͜͜·Ͱ঺հ͖ͯͨ͠
    ৭ʑͳςετΛ
    ݱ৔Ͱ͖ͯ͠·ͨ͠
    ͦͷ݁ՌɺҰ൪࠷ߴͳςετ͸

    View Slide

  86. )5.-
    $44
    $PNQPOFOU
    7VFY
    4UBUF
    1SPQT
    6TFS
    *OUFSBDUJPO
    ͜ͷ෦෼ͷ7JTVBM5FTU

    View Slide

  87. wTUPSZCPPLͰͷʮ1SPQT7VFY4UBUFˠදࣔʯ
    ͷςετ͚ͩͰ΋͋Δͱ҆৺
    wʮ6TFS*OUFSBDUJPOˠදࣔʯͷςετ΋͋
    Δͱߋʹ҆৺ͷ෯͕޿͕Δ
    w෭࡞༻ͱͯ͠ϨϏϡʔͷෛՙܰݮ
    w͜Ε͕ඇৗʹେ͖͍
    7JTVBM5FTU͸࠷ߴ

    View Slide

  88. ָͰ͖Δͱ͜Ζ͸ָͯ͠
    ෆ҆Λղফͭͭ͠
    ָ͘͠։ൃ͍͖ͯ͠·͠ΐ͏

    View Slide