TDD Boot Camp 2020 Online #1 基調講演/ライブコーディング
tddbc主催、t_wadaさん による 「TDD Boot Camp 2020 Online #1 基調講演およびライブコーディング視聴」 に参加しました。 これは当日取ったメモです。
※ 独自解釈と感想が入り乱れているため、動画を見ないとなんのことだかよく分からない内容だと思います。
世の中には一発で「ちゃんと動く綺麗なコード」を書く人たちがいる。
そんなことは凡人にはできない。しかし、プログラマはすべからく「ちゃんと動く綺麗なコード」を書くべきなのである。
そこで活用されたい手続きが TDD である。
つまり、TDD の目的は「ちゃんと動く綺麗なコード」を実現することである。
なお、「綺麗な」とは「簡潔で読みやすい」とする。
仕様書を読み取りTODOリストを書いていく
- red, green, refactoring のサイクルを進める前に、箇条書きでテストケースを列挙していく
- テストが難しいなら → 分割する
- 分割できたら「うまみのあるところ」を優先的にやると吉。
テストをやるべき対象
- テスト容易性: 高
- 重要度: 高
の順で、その中でも
- 正常系
- 準正常系
の順にテストを書いていく。わかりやすい正常系のテストは書きやすい。
スタートラインに立つための準備, 土台づくり
設計的要素があり、骨が折れるがここを固めておくと後で楽。 満たすべき仕様をテストケースの箇条書きに直していく。
- ついでに仕様の表現の揺れや粒度で気がついたところを揃える
- テストケースの名前は XXXTestにする (最近のトレンド)
- テストケース名は可能なら日本語にすると良い
テストケースの実装ブロックの並び
- 準備 Arrange
- 実行 Act
- 検証 Assert
- 後片付け
"4 Phases Test" とも呼ばれる。 4の後片付けはフレームワークの仕組みを利用することが多いので、基本的には 1-3 を書くことになる。 1-3のAをまとめて "3Aパターン" と呼んだりもする。
TDD では下から書いていく (ゴールから書いていく)
3A を上から書いていくと、ようやっと検証を書く段階になってから
「はて、何を検証したいんだっけ??書き始める時はイメージがあったのだが...」
と道に迷ってしまうことがあるある。そうならないように、 assert から書いていく。
テストを書き始める時または書き進めていく中で手が止まったら?
早めに手が止まるのは悪いことじゃない。いいこと
↑早めにまずい状況が得られた、ということだから。後になって押し寄せるより格段にマシ。
手が止まったら → 具体化すること
例:
- 「数を文字列に変換」
- 具体化 → 「1を渡すと文字列1を返す」
- つまり、抽象度を下げている
- 具体化 → 「1を渡すと文字列1を返す」
「うまくいってないことがある」ということが認識できることが大事。
うまくいってないのに、それが認識できてない場合がある。それはまずい。
認識できるようにするためにも、具体例を求めざるを得ないTDDは有効。
TDDを進めるにあたって
- テストコードから書いていく。機能実装コードはその後。
- まだ実装していないメソッドも「先に使って」テストを書く。
- 「作る前に使う」
- 「使う側の気持ち」に先になれる
- → API デザインが良くなる
- 「使う側の気持ち」に先になれる
- テスティングフレームワークの expected と actual の順を押さえておくことは意外と大事。
- エラーメッセージを読む時、毎回、困らなくて良い
容易性が高いものから取り組む理由
- ゼロからやることは設計的。
- 「設計的な作業」いうことは「重たい作業」ということ。
- スラスラ書けるテスト容易性の高いものを例題として扱うことで、設計面に注力できる → 1つのことだけに集中できる
他人を疑う前に自分の行動や成果を疑え
「意図せずテストが通った/通らなかった」こういう場合、「本当に対象のテストが動いてるのか?」と不安になることがある。
- 欠陥挿入 (defect injection): 動かない壊れたもの (存在しないメソッドとか) を入れることで「ちゃんと失敗する」ことを確かめる
- 変異検査 (mutation testing): パスしそうもない値に assertion を変えることで「ちゃんと失敗する」ことが確かめられる
- if を逆にする、という手法もある
テストを書いていく
- 仮実装: ベタな値を返す (t-wadaさんが「茶番」とネタにしていたやつ)
- テスト側である値を期待するテストを書く。
- テストコードの内容は例えば
assert(1, func())
となっているとする。- この場合、func() の実装として、
return 1
だけを書いておくもの。本来ならこの実装は何かしらの処理過程を経て、任意の入力に対して対応する出力が返され実装になっているべきなのだが、「テストコードを書いた後、最速でテストを通す」(Red -> Greenの最初のサイクル) を実現するために、仮実装をすると有効な場合がある。
- この場合、func() の実装として、
- なお、「三角測量」としてこの後に追加するテストケースには assert(2, func()) と書かれた場合、テストを実行すると失敗するようになる。そこで、初めて本実装が始まる。
- 一次変数を持たなくて良いものはインライン化してすっきりさせる
- bar = func()
assert(expected, bar) - → assert(expected, func())
- bar = func()
- 1テストケース内にテストアサーションを縦に並べる
- テストのリファクタ
テスト実装3パターン = TDDの3つのギアで歩幅を調整
- テスト→仮実装→三角測量→実装
- 小さな歩幅
- テスト→仮実装→実装
- テスト→明白な実装
- テストケースを追加するときなど、機能を満たすために次に実装すべき処理がわかり切っている場合、仮実装も三角測量もスキップして実装
- より「大股で進む」高いギア
TDDのサイクル
1.次の目標を考える 2.その目標を示すテストを書く 3.そのテストを実行して失敗させる(Red) 4.目的のコードを書く 5.2で書いたテストを成功させる(Green) 6.テストが通るままでリファクタリングを行 う(Refactor) 7.1~6を繰り返す
テストコードは「動く仕様書」であって欲しい
- 仕様と同じ構造になっている
- 粒度が揃っている
- 必要最低限の量
- 「このテストケースは何のために書かれているの?」が理解できるように
その他
- 抽象度を下げたら後で戻す
- 三角測量が他のテストケースにもないのであれば、粒度を揃えるために役目を終えた後は消しておこう
- 自分がいなくなった後に引き継ぐ人のことを想像して、その人が「消す判断がつかない」コードは消しておくこと。
- テストもメンテナンスが必要 → 必要最小限が望ましい。
- 「テストを減らす」のはテストを書いた本人しかできない。
- → 本人がいる間に、消せるものは消しておこう
- 「理解容易性」が低いテストは最も避けたいもの ← 三角測量である意図明確でないテストコード
- → 不良資産になる
- 三角測量が他のテストケースにもないのであれば、粒度を揃えるために役目を終えた後は消しておこう
- テストメソッド名を「仕様___具体例」に直す
- 仕様がネスト構造を持っていれば、テストコードも入れ子にしてブロック構造を持とう
- リファクタリングはいつまでもやれてしまう
- ので、5-10minやったらやめる ... 案1
- 重複を除去できたら終わる ... 案2