🍔

なに?テスト駆動開発やったことないのかい?まったくキミってやつは!

2021/10/04に公開2

ある日のパイセン「テスト書いておいてくれよ!」

そう言われたのだけど…
ボクはテストを書いたことのなかったのさ!

どうなったかって?

Oh...ジーザス

レビューで突き返されちまったよ!
そのうえリファクタリングまでされて。

最終的にパイセンのコードを見よう見まねで
テストを追加していって、マージはされたんだ

そこで、ボクは思ったよ

テスト書けるかどうか、ボクがテストされるべきだったんだなってね!

TDD本というバイブルがあるじゃないか!

テスト駆動開発をはじめよう、という人には
TDD本と呼ばれる『テスト駆動開発』というものがあるんだ!

どんな本買って?
あれはまさにバイブルさ。
(ゴシップ好きのトニーの話じゃ、今や机の引き出しに入れてるホテルもあるらしい!)

さっそくボクもTDD本を読んだから、テスト駆動開発というのを、キミに教えてあげようじゃないか!

環境は、vueCLIで、全部入れることができるものさ。

Vue.js
TypeScript
テストライブラリ:Vue Test Utils
テスティングフレームワーク:Jest

今回は、propsにデータを渡したら、
テーブルを表示してくれるcomponentのテストでも書いてみよう

ファイル構成はこんな感じだ

src
┣ components
┃ ┗ organisms
┃   ┗ SummaryTable.vue
┗ views
  ┗ Home.vue
tests
 ┗ unit
   ┣ Home.spec.ts
   ┗ SummaryTable.spec.ts

ようこそ、テスト駆動開発の世界へ。いいかい、ルールを守らないと罰則だよ!

環境は整った!さあ、次はなにをする?

まず手始めに、ページのタイトルが表示されるという、テストでも書いてみるかい?

// Home.vue
<template>
    <div>
        <h1>データ一覧</h1>
    </div>
</template>
// Home.spec.ts
import { shallowMount } from '@vue/test-utils'
import Component from '@/views/Home.vue'

describe('Testing Home Component', () => {
    it('renders page title', () => {
        const wrapper = shallowMount(Component)
        expect(wrapper.html()).toContain('<h1>データ一覧</h1>')
    })
})

このテストがどうなるかって?

もちろん通るに決まってる!楽勝さ!

おいおい、いきなりテスト書いちゃって。まだまだ子どもだね!

テストは通るさ!
でも、その前にやることがあるだろう?

忘れちゃだめさ。

テストパターンを書き出さないとな!
TODOリストみたいに書き出していって、テストが終わったものから消していくんだ!

いきなりテストを書くなんて、デートでいきなりホテルに行きたがるようなものさ
(ホテルにはTDD本があるだろうな!)
ガールフレンドに見放されちまうぜ?

もちろんボクたちは全知全能の神じゃない
最初から全部挙げるなんて無理さ!

テストを書いているときに、新しいテストパターンを思いついたら、追加で書き出せばいいんだよ!

テストパターン
[x] タイトルを表示する
[ ] テーブルデータが表示される

次は、テーブルデータが表示される、だな!
ユーキャンメイキット!(You can make it !)

// SummaryTable.vue
<template>
    <div>
        <div v-for="item in items" :key="item.id">
            <div v-for="key in Object.keys(item)" :key="key.id">{{ item[key] }}</div>
        </div>
    </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue'

export default defineComponent({
    name: 'SummaryTable',
    props: {
        items: {
            type: Array,
            require: true
        }
    }
})
</script>

テストが通るまでは別のコードを書くんじゃないよ!トーストを焦がされたいのかい?

それっぽいコードが書けたな!やるじゃないか!

ただねキミ
今やっているのがテスト駆動開発だ、ということを忘れちゃいないかい?

[ ] テーブルデータが表示される

をテストするのに、親componentから渡ってきた値を、SummaryTableで表示する必要があるのかい?

親componentから渡ってきた値が、SummaryTableで表示される
これは別のコードなんだから、別のテストであるべきだ

テストパターン
[x] タイトルを表示する
[ ] テーブルデータが表示される
[ ] 親componentから渡ってきた値が、SummaryTableで表示される

だから今回は、これだけで十分なのさ

// SummaryTable.vue
<template>
    <div>
	<div>
	    <div>id</div>
	    <div>name</div>
	    <div>email</div>
	</div>
        <div>
            <div>1</div>
            <div>Bob</div>
            <div>bob@email.com</div>
        </div>
        <div>
            <div>2</div>
            <div>Tom</div>
            <div>tom@email.com</div>
        </div>
        <div>
            <div>3</div>
            <div>Sam</div>
            <div>sam@email.com</div>
        </div>
    </div>
</template>

むむ、テーブルのヘッダー部分のテストパターンもあったほうがよさそうじゃないか?
忘れないうちに加えておこう

テストパターン
[x] タイトルを表示する
[ ] テーブルヘッダーが表示される
[ ] テーブルデータが表示される
[ ] 親componentから渡ってきた値が、SummaryTableで表示される

テーブルヘッダー、テーブルデータのテストは
Home.spec.tsと要領は同じだ

// SummaryTable.spec.ts
import { shallowMount } from '@vue/test-utils'
import Component from '@/components/organisms/SummaryTable.vue'

describe('Testing Render Summary Table', () => {

    it('render table heads', () => {
        const wrapper = shallowMount(Component)

        expect(wrapper.html()).toContain('id')
        expect(wrapper.html()).toContain('name')
        expect(wrapper.html()).toContain('email')
    })

    it('render table items', () => {
        const wrapper = shallowMount(Component)
	
        expect(wrapper.html()).toContain(1)
        expect(wrapper.html()).toContain('Bob')
        expect(wrapper.html()).toContain('bob@email.com')
	// TomとSamも同じようにね!
    })
})

これで問題なく通るだろう!
イッツアピースオブケーク!(It's a piece of cake !)

次のテストを書く前に、コーヒーブレイクでもどうだい?リファクタリングという名のね!

テストパターン
[x] タイトルを表示する
[x] テーブルヘッダーが表示される
[x] テーブルデータが表示される
[ ] 親componentから渡ってきた値が、SummaryTableで表示される

さあ、残り1パターンだ!
(実際はもっとあるだろうが、そこは目を瞑ってくれると信じてるぜ、ブラザー)

しかし次のテストを書く前に、リファクタリングをしなきゃいけないのさ!

え、あとでもいいんじゃないかって?

後回しにしてもいいのは、夏休みの宿題だけだぜ!

実際、TDDの進め方に、リファクタリングが入っているんだ!

テスト駆動開発の進め方

  1. まずはテストを1つ書く
  2. すべてのテストを走らせ、新しいテストの失敗を確認する
  3. 小さな変更を行う
  4. すべてのテストを走らせ、すべて成功することを確認する
  5. リファクタリングを行って重複を除去する

-> すべてのテストが通ったら、その機能の実装をする

さあ、やってみよう
こんな感じかい?

// SummaryTable.spec.ts
describe('Testing Render Summary Table', () => {
    const items = [
        { id: 1, name: 'Bob', email: 'bob@email.com' },
        { id: 2, name: 'Tom', email: 'tom@email.com' },
        { id: 3, name: 'Sam', email: 'sam@email.com' }
    ]

    it('render table heads', () => {
        const wrapper = shallowMount(Component)

        Object.keys(items[0]).forEach((key) => {
            expect(wrapper.html()).toContain(key)
        })
    })

    it('render table items', () => {
        const wrapper = shallowMount(Component)

        items.forEach((item) => {
            Object.keys(item).forEach((key) => {
                expect(wrapper.html()).toContain(item[key])
            })
        })
    })
})

どうやら、テストは通ってるみたいだな!
passed!

いよいよ最後のテストパターンか!

テストパターン
[x] タイトルを表示する
[x] テーブルヘッダーが表示される
[x] テーブルデータが表示される
[ ] 親componentから渡ってきた値が、SummaryTableで表示される

テスト駆動開発は、2度コードを書く。そんなふうに思ったらキミのテストが間違ってる!

最後のテストパターンはこうだ

[ ] 親componentから渡ってきた値が、SummaryTableで表示される

テストを書く前に、これまでのテストコードを見返してみてくれ
驚くべきことに気づくはずだ
(きっとキミがアニメの主人公なら、目が飛び出してしまうよ!)

// SummaryTable.spec.ts
describe('Testing Render Summary Table', () => {
    const items = [
        { id: 1, name: 'Bob', email: 'bob@email.com' },
        { id: 2, name: 'Tom', email: 'tom@email.com' },
        { id: 3, name: 'Sam', email: 'sam@email.com' }
    ]
    // これはもうpropsに渡せる状態だ!

    it('render table heads', () => {
        const wrapper = shallowMount(Component)

        Object.keys(items[0]).forEach((key) => {
            expect(wrapper.html()).toContain(key)
        })
	// これをtemplateに組み込めば、itemのkeyが変わっても大丈夫そうだ!
    })

    it('render table items', () => {
        const wrapper = shallowMount(Component)

        items.forEach((item) => {
            Object.keys(item).forEach((key) => {
                expect(wrapper.html()).toContain(item[key])
            })
        })
	// これをtemplateに組み込めば、itemのkeyが変わっても大丈夫そうだ!
    })
})

そう、もうキミはここまでで、次のテストに使えるコードを書いてきているんだ!
もはや、テストコードは、componentのpropsにitemsをを渡してあげるだけでいい!

// SummaryTable.spec.ts
const wrapper = shallowMount(Component, { propsData: { items } })
// SummaryTable.vue
<template>
    <div>
        <div>
            <div v-for="key in Object.keys(getItems[0])" :key="key.id">{{ key }}</div>
        </div>
        <div v-for="item in getItems" :key="item.id">
            <div v-for="key in Object.keys(item)" :key="key.id">{{ item[key] }}</div>
        </div>
    </div>
</template>

<script lang="ts">
import { defineComponent, PropType } from 'vue'
interface Item {
    id: number
    name: string
    email: string
}
// 略
    props: {
        items: {
            type: Object as PropType<Item[]>,
            require: true
        }
    },
    computed: {
        getItems(): Item[] {
            return this.items
        }
    }
// 略

なんて素晴らしいんだ!
ちゃんとリファクタリングをしてきたから、
ロジックを新しく考える必要などなく、最後のテストパターンも通すことができた!

テストパターン
[x] タイトルを表示する
[x] テーブルヘッダーが表示される
[x] テーブルデータが表示される
[x] 親componentから渡ってきた値が、SummaryTableで表示される

テスト駆動開発、翼をさずける!

ここまで読んでくれたキミ、ありがとう!

ボクはテスト駆動開発と出会って、翼をさずかった気分だよ。

きっと飛び方をマスターすれば、
どこへでも、何にもぶつからずに、行くことができるさ!

もちろんまずは、
飛んで行っていいかどうか、
テストを通してからにしてくれよ?

Follow ME !!!
I'm sure to follow you back!
twitter: @marty_ojiya

Discussion