なに?テスト駆動開発やったことないのかい?まったくキミってやつは!
ある日のパイセン「テスト書いておいてくれよ!」
そう言われたのだけど…
ボクはテストを書いたことのなかったのさ!
どうなったかって?
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つ書く
- すべてのテストを走らせ、新しいテストの失敗を確認する
- 小さな変更を行う
- すべてのテストを走らせ、すべて成功することを確認する
- リファクタリングを行って重複を除去する
-> すべてのテストが通ったら、その機能の実装をする
さあ、やってみよう
こんな感じかい?
// 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])
})
})
})
})
どうやら、テストは通ってるみたいだな!
いよいよ最後のテストパターンか!
テストパターン
[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
この口調が好きなのは僕だけだろうか…
ありがとうございます…!
楽しい投稿していきます!