テストコード初心者にテストの書き方を教えるときのポイント

最近、JavaScriptでテストコードを書けるようになろう!というお題でモブプログラミング形式の勉強会をやっている。 遂に実務でテストケース + 実装でPull Requestを出せる人が出てきた。これだけで50%ペイした気持ちになっている。

詰まってしまう点とフォローの仕方を忘れないようにメモしておく。

前提

ここでのテストとはテスト駆動開発(形式)とする。テスト駆動開発1(著者Kent Beck 著/和田 卓人 訳 2017年)の定義を引用すると、下記2点のルールに従ってコードを書くスタイルである。

  • 自動化されたテストが失敗したときのみ、新しいコードを書く。
  • 重複を除去する。

言語はJavaScript2、テストフレームワークはJestとする。お題はCodingDojoからFizzBuzzを選択した。

最初にTODOを書かせる

テストコードを書く習慣がない場合、いきなり要件を実現するコードを書こうとしてしまう。すると、手が止まってしまう。FizzBuzzの仕様だけでも

- 3の倍数なら`Fizz`
- 5の倍数なら`Buzz`
- 3と5の倍数なら`FizzBuzz`
- それ以外は数字をそのまま出す
- 1から100まで実施する

これだけの要素がある。いきなり仕様を全て実現しようとし、「どうしよう…」と途方に暮れてしまうのである。

よって、まずは「FizzBuzzのルールを書き出して、それをTODOにしようね」と声をかける。大きな問題は小さく分割して解決する。小さい解決を積み上げ、要件を全て達成する。この点を繰り返し説明する。

要件を整理してもらい、TODOリストを作る。GitHub Issueを使っているのならチェックリストにしてコメントしてもらう。

// 終わったらチェックをつける。進んでいる感じはやる気を維持する要素である。
- [ ] 3の倍数なら`Fizz`
- [ ] 5の倍数なら`Buzz`
- [ ] 3と5の倍数なら`FizzBuzz`
- [ ] それ以外は数字をそのまま出す
- [ ] 1から100まで実施する

TODOリストをそのままテストケースに書き起こす

次に、TODOをテストケースに変換してもらう。関数3IN/OUTはTODOリストで整理済みなので、それをそのままお題にしてもらう。ここで手が止まってしまうようであれば、具体的な値をTODOリストに追加するよう促す。

- [ ] 3の倍数なら`Fizz`
    - 9のときは`Fizz`
- [ ] 5の倍数なら`Buzz`
    - 10のときは`Buzz`
- [ ] 3と5の倍数なら`FizzBuzz`
    - 15のときは`FizzBuzz`
- [ ] それ以外は数字をそのまま出す
    - 1のときは`1`

これをテストケースに書き起こすと次のようになる。等価のテストケースは公式ドキュメントに例がある。それか、サンプルを目の前で一度書いて見せれば後続に続きやすい。

test('3の倍数なら`Fizz`を返す', () => {
  expect(fizz(9)).toBe('Fizz');
})

初心者が手が止まってしまうポイントは大きな要件を小さく分解するところにある。よって、あらかじめ小さく分解できれば手は自然と動く。記法がわからない点はモブプログラミングパワーでフォローできる。

このとき、TODOを全てテストケースに書き写す人もいる。『テスト駆動開発』では始めにテストケースを全て書くことをおすすめしていない。テストが失敗している状況が続くと精神衛生上良くないことと、後から設計を変更したとき修正の負荷が重くなるためである。

ただ、慣れていない人にあれこれ指示してしまうと萎縮して手が止まってしまう。重要なことはテストを書くことなので、テストを先に書き切ってしまうかは判断を任せた方が良い。4

まず「なんでもいいから」テストを通す

さて、コードを実装するぞ!となるとまた手が止まってしまう。これは

  • 一度に全てのテストケースを
  • 完璧な設計で遠そうとしてしまう

からである。「まずはどんな手段でもいいから一つずつテストケースを遠そう」と声をかける。手が止まってしまう原因は大きな問題を一度に全部解こうとするから。だから、問題は分割して解くように繰り返し声をかける。

fizzのテストケース例であれば、最初はただ単にfizzreturnしてテストケースを通す。

const fizz = (n) => {
  return 'Fizz'
}

これが通れば関数のimport/exportはできていると判断でき、進捗していると言える。たったこれだけだが、達成感がある。手が止まってしまうことを防げる。

次に「3の倍数ってどうやって判断する」かを考えてもらう。

3の倍数 === 割り算した余り(余剰)の数が0

ここまでわかればコードに変換するだけである。JavaScriptで余剰を計算する方法が分からなければドキュメントを示して手助けする。

// あまりが0 === 3で割り切れる
const fizz = (n) => {
  return n % 3 === 0 : 'Fizz' ? n
}

この時点ではテストが通れば良いため、余りが0以外のときの処理は必要ない。とりあえずテストを通すことに集中する。

1つテストケースが通ったら、TODOをテストケースに変換するテストケースを通す工程を繰り返す。

最後に重複を削る

ある程度記載できたら重複がないか確認することを忘れないようにする。そうしないと開発のコードでもテストが通ったままで放置する習慣がついてしまう。「被ってるところを探してまとめよう、テストケースが通っているうちは安心できるよね」という理屈である。

ちなみに後から加えたテストケースが別のケースと同じ内容をテストしている場合もメンテナンスする。同じケースが複数あると仕様が変わったとき大量に修正する羽目になって大変でしょ、という話である。

const fizz = (n) => {
  return n % 3 === 0 : 'Fizz' ? n
}

const buzz = (n) => {
  return n % 5 === 0 : 'Buzz' ? n
}

const fizzbuzz = (n) => {
  return n % 3 === 0 && n % 5 === 0 ? 'FizzBuzz' : n
}

とりあえずテストケースを通した結果、fizzbuzz()の計算式はfizz()buzz()と一緒となる。じゃあfizz()buzz()の計算式だけ使いまわしたいよね…となる。

const fizz = (n) => {
  return n % 3 === 0
}

const buzz = (n) => {
  return n % 5 === 0
}

// テストケースで検査する関数はfizzbuzzのみにしておく
const fizzbuzz = (n) => {
  if (fizz(n) && buzz(n)) {
    return 'FizzBuzz'
  } else if (fizz(n)) {
    return 'Fizz'
  } else if (buzz(n)) {
    return 'Buzz'
  } else {
    return n
  }
}

FizzBuzz判定ができた後、ループの処理を書き加える。こうすると問題が小さくなるので取り組みやすい。最後にテストケースも整理すると「いつの間にか終わっている」状況が作り出せる。

とにかく大きな問題は小さくちぎる

一度に全部解決するのではなく、小さく分割する。とにかく通してまた改善する。最後に全部できてればOKの精神を持ってもらえるように後押しする。

これができると、実際の処理を作るときもこのパターンはどうなるんだっけ…?と考える癖ができる。結果、仕様の考慮漏れも減るしテストは整備できるしで良いことづくめである。


  1. https://shop.ohmsha.co.jp/shopdetail/000000004967/
  2. 理由はエントリー参照
  3. 状況や言語次第でクラスになるか?とにかく処理する何かのこと
  4. 慣れてくると1つか2つテストを書いて通すようになる