Czです。
最近「バグ」みてますか?
えっ..?毎日見てる...って?
対象読者は以下の通り
- ユニットテストが必要だと感じつつも優先度が上がらなくて後回しになっちゃう人
- バグを減らしたい人
- 自分の実装したコードの品質に自信が持てない人
- みなさん開発中のテストはどうしていますか?
- ユニットテストのメリット
- 初期コストはかかってしまう
- とりあえず、まぁ、動かしてみよう
- とりあえず、まぁ、テストケースを考えてみよう
- とりあえず、まぁ、ユニットテストを書いてみよう
- 書いちゃいけない制約はない!どんどん書くべし!(要プロジェクトへの確認)
- 最後に
- 参考
みなさん開発中のテストはどうしていますか?
Excelに数百パターンのテストケースを書き、機能追加や変更のたびにアップデートして手動テストしている?
逆に数百パターンのテストケースに疲弊して、正常系ケースだけチャチャっと確認して終わり?
私はこの記事を書くにあたって今までのプロジェクトを振り返ってみたところ、実装中にユニットテストまでしっかり書いていたとこに所属したことはなかったことに気づきました。(テストもテスター部隊がやってくれていたなぁ)
なのできっとこれを読んでいる方の7割以上はユニットテストは作成せず、Excelなどのスプシを使って手動テストしているのではないかと勝手に思っています。
ユニットテストのメリット
- 一度作ってしまえば、前日飲みすぎたとしても全く同じ条件でしっかりテストしてくれる!
- 何千、何万回もテスト実行を依頼しても一切弱音を吐かない!
- 大量のテストケースを作っていたとしても、ちょっと席を外している間に完了している!
- CI(継続的インテグレーション)でコミットプッシュ時に実行されるように設定しておけば、レビュアーにレビュー依頼する前にデグレなどを発見することができ、レビュアーの貴重な時間を奪わなくなる!
初期コストはかかってしまう
みんな分かっているはずです。ユニットテストは必要だって。次の新規開発プロジェクトではユニットテストは導入したいってみんな言いますもん。 でもやっぱり次のプロジェクトでもユニットテストは書かなくなってしまう。
やってみるとわかるのですが結構大変。 というよりはユニットテストを書くための実装を行う必要があり、その難易度高い。 なので「今日からユニットテストも必須だからちゃんと書けよなー(C2カバレッジだからなー)」みたいなことをサラッといってしまっても反感を買うだけだし、恐らく誰もやらなくなる。(気がする)
また、テスト工数は開発工数の中でマージン程度に扱われ、実装が間に合わない場合はテスト工数を割いて実装に当ててしまうこともしばしばあると思う。 結果としてコストとの兼ね合いでユニットテストは現実的ではなくなる。
とはいえ
とりあえず、まぁ、動かしてみよう
書けない理由を並べてもしょうがないので、まずはやってみよう。
ユニットテスト実装のサンプルコード用意しました。 このコードを見ながらどうテストを実装していけば良いか簡単に説明していこうと思います。
GitHub - takamk2/mocha_vue_sample: mochaを使ったvueテスト実装サンプル
本サンプルアプリはシンプルなTODOアプリでざっくり仕様は以下の通りです。
(ざっくり)仕様
- 入力フォームに文字を入力すると追加ボタンが有効になり、押すとリストに追加される。
- 完了ボタンを押すと打ち消し線が入り、削除を押すとリストから削除される。
- 画面上部にはAPIで取得したアドバイスを表示する。
イメージ
早速コードをcloneして簡単に動作を見てみましょう。(まぁ正直見るほどじゃないので見なくても良いです)
// 準備 $ git clone https://github.com/takamk2/mocha_vue_sample.git $ npm install // サンプルアプリ起動 $ npm run serve
見れましたか? では早速コードを見ていきましょう!と行きたいところですが、今回の記事はユニットテスト布教記事なのでユニットテストを実行した結果を先に見ましょう。
$ npm run test:unit
実行すると次のような結果が表示されます。 ユニットテストを作っていく=以下の結果を増やしていくといった感じです。
MOCHA Testing... api/advice.js ランダムなアドバイスを取得: getRandomAdvice() ✓ api.adviceslip.comへのリクエストが実行される services/AdviceService.js ランダムなアドバイスを取得: getRandomAdvice() ✓ アドバイスが取得できる components/AdviceMessage.vue アドバイス読み込み成功 ✓ アドバイスが表示される アドバイス読み込み失敗 ✓ 読み込み失敗が表示される components/TodoContainer.vue テキストフォームが空 ✓ 追加ボタンが無効になる テキストフォームに文字列を入力 ✓ 追加ボタンが有効になる テキストフォームに文字列を入力して追加ボタンを押す ✓ todoモジュールのappendTodoItemが呼び出される components/TodoList.vue todoが1つもない ✓ TodoListItemコンポーネントが1つも表示されていない todoが2つ ✓ TodoListItemコンポーネントが2つ表示される ステータスtodoとdoneがそれぞれ1つずつ ✓ TodoListItemコンポーネントが2つ表示される 完了ボタンが押された ✓ todoモジュールのupdateStatusが呼び出される 削除ボタンが押された ✓ todoモジュールのremoveItemが呼び出される components/TodoListItem.vue ステータスがtodo ✓ Todoの内容が表示される ✓ doneクラスが付与されない ✓ 削除ボタンが表示される ✓ 完了ボタンが表示される ステータスがDone ✓ doneクラスが付与される ✓ 完了ボタンが表示されない 完了ボタンをクリック ✓ 完了ボタンクリックイベントを発火。引数にid=1を含む 削除ボタンをクリック ✓ 削除ボタンクリックイベントを発火。引数にid=1を含む 20 passing (161ms) MOCHA Tests completed successfully
テストの粒度はなかなか難しいところではありますが、個人的にはこれくらいの粒度や内容の方が見やすく、テスト結果から仕様の理解もできるので良いと思っています。
とりあえず、まぁ、テストケースを考えてみよう
ここからは実装コードを解析して、テストケースを考えていこうと思います。 すべてのコードを説明するのは大変なので「components/TodoListItem.vue」を例に説明しようと思います。
TodoListItemはTODOリストの1アイテムを表すコンポーネントとなっています。
https://github.com/takamk2/mocha_vue_sample/blob/master/src/components/TodoListItem.vue
Vueコンポーネントは「template」「script」「style」の3つで構成されており、テストケースのための情報をあつめるために「template」→「script」の順でコードを見ていこうと思います。
まずは「template」部分
動的な部分をマークします。基本的にここに対してテストコードを作成する感じです。
<template> <li :class="{ done: isDone }"> // 1, 2 <div class="left-item"> {{ item.content }} // 3 </div> <div class="right-item"> <button v-if="!isDone" // 4, 5 type="button" class="done-btn" @click="onClickDoneButton" // 6 > 完了</button ><button type="button" class="delete-btn" @click="onClickDeleteButton"> // 7 削除 </button> </div> </li> </template>
「template」部分だけで以下の7パターンのテストケースが作れそうなことがわかりました。
- isDoneがtrueの時は'done'クラスがセットされる
- isDoneがfalseの時は'done'クラスがセットされない
- item.contentの値を表示
- isDoneがtrueの時は完了ボタンが表示されない
- isDoneがfalseの時は完了ボタンが表示される
- 完了ボタンをクリックしたら何か処理が行われる
- 完了ボタンをクリックしたら何か処理が行われる
つづいて「script」部分
どんな入力データの時にどんな振る舞いになるかに注目してマークしていきます また、クリック時のイベントも同様にマークします
<script> export default { name: 'TodoListItem', props: { item: { type: Object, required: true, validator: (item) => { return ['id', 'content', 'status', 'createdAt'].every((key) => Object.prototype.hasOwnProperty.call(item, key), ) }, }, }, computed: { isDone() { // 1 return this.item.status === 'done' }, }, methods: { onClickDoneButton() { this.$emit('on-click-done-button', this.item.id) // 2 }, onClickDeleteButton() { this.$emit('on-click-delete-button', this.item.id) // 3 }, }, } </script>
- propsで受け取る値のitem.statusが'done'の時にisDoneがtrueになる
- onClickDoneButtonが呼ばれたら'on-click-done-button'イベントを発火する。引数はitem.id
- onClickDeleteButtonが呼ばれたら'on-click-delete-button'イベントを発火する。引数はitem.id
情報の整理
一通り情報が出揃ったのでこれらの情報を元にテストケースを考えます。 コツとしてはテストの流れを「Given/When/Then」の3つのセクションに分割します。
Given | 振る舞い実行前の状態。データの用意などを行う。 |
---|---|
When | 振る舞い。ボタンクリックなど。Vueコンポーネントのマウントもここに含める。 |
Then | 振る舞いの結果。Todoアイテムが完了になるなど。 |
例「ステータスがdoneなら完了ボタンを非表示にする」の場合
- Given:ステータスがdone(item.status === 'done')
- When:Vueコンポーネントをマウント
- Then:完了ボタンが非表示になる
といった感じに分けられます。
とりあえず、まぁ、ユニットテストを書いてみよう
テストケースの作り方がわかったところで実際にユニットテストコードを書いてみましょう。
https://github.com/takamk2/mocha_vue_sample/blob/master/tests/unit/components/TodoListItem.spec.js
例として「ステータスがdoneなら完了ボタンを非表示にする」のテストケースを抜粋します。
以下のコードはmochaというテストフレームワークで記述しています。 詳しい説明は省略しますが「describe」と「it」の部分に注目してみてください。 「describe」はグループ、「it」は結果のチェックする場所という認識で見ていただければ理解できるかと思います。
import assert from 'assert' import sinon from 'sinon' import { shallowMount, createLocalVue } from '@vue/test-utils' import TodoListItem from '../../../src/components/TodoListItem' const localVue = createLocalVue() describe('components/TodoListItem.vue', () => { beforeEach(() => { sinon.restore() }) describe('ステータスがDone', () => { let wrapper = null beforeEach(() => { // Given const propsData = { item: { id: 1, content: 'dummy content', status: 'done', createdAt: '2020/09/19 19:19:19', }, } // When wrapper = shallowMount(TodoListItem, { localVue, propsData, }) }) it('完了ボタンが表示されない', () => { // Then assert.ok(!wrapper.find('.done-btn').exists()) }) })
ちなみに上記ユニットテストコード実行した結果は以下のような出力になります。 「describe」が階層になっていて最後に「it」部分の結果がチェックマークで表現されています。
components/TodoListItem.vue ステータスがDone ✓ doneクラスが付与される
順番に見て行きますね。
describe('components/TodoListItem.vue', () => { beforeEach(() => { sinon.restore() })
まず一番外側のdescribeにはファイル名を記述しています。 どのファイルのテストを行なっているかわかりやすくなるのでおすすめです。
その下のbeforeEach()では「sinon.restore()」としています。 ここでは「sinon」というスタブ化ライブラリでスタブ化した状態をリセットしています。 このコード内ではスタブ化していないので一旦は無視してOKです。
beforeEach()は「it」の直前に呼ばれるので事前準備などを行いたい場合はここに記述します。
describe('ステータスがDone', () => { let wrapper = null beforeEach(() => { // Given const propsData = { item: { id: 1, content: 'dummy content', status: 'done', createdAt: '2020/09/19 19:19:19', }, } // When wrapper = shallowMount(TodoListItem, { localVue, propsData, }) }) it('完了ボタンが表示されない', () => { // Then assert.ok(!wrapper.find('.done-btn').exists()) }) })
次のdescribeの中にはbeforeEach()とit()が存在します。 個人的には「Given」「When」をbeforeEach()の中で行い、it()には「Then」しか書かないとしておくと良いかなと思っています。 (it()が複数あった時にスッキリかける!)
と、まぁ駆け足になってしまいましたがユニットテストの書き方のさわりがわかっていただければ嬉しいです。
書いちゃいけない制約はない!どんどん書くべし!(要プロジェクトへの確認)
今までユニットテストを書いてこなかった人は、いざ今日から書けと言われてもおそらく書けないかもしれません。でも書かないといつまでも書けないままなのは間違いないと思います。
なので私としては、 1 Function、1 Classだけでもいいので実装タスクを対応する際にユニットテストを書いてみる のもありなんじゃないかなと思います。 1 Functionなら工数もそんなにかからないでしょ? 色んな所から呼ばれる機能ならばきっと大きな効果を発揮してくれるはずです。
みんなが少しずつユニットテストを書くことによって、いつのまにか大量になったテストコードによって守られる未来が来るんだなぁって思うとなんだかワクワクします。
最後に
皮肉にもテストコードの書き方を調べている間に締め切りがきてしまいました... この無念は次のターンで晴らそうかと思います。
参考
Mocha - the fun, simple, flexible JavaScript test framework
vue-test-utils | Vue Test Utils
\\『真のユーザーファーストでマーケットを創造する』仲間を募集中です!! //