46
33

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2021-10-03

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

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

どうなったかって?

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])
            })
        })
    })
})

どうやら、テストは通ってるみたいだな!
Screen Shot 0003-10-03 at 12.54.13.png

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

テストパターン
[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

46
33
3

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
46
33

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?