2020/08/01 TDDBootCamp 基調講演まとめ
2020年8月1日に開催されたイベントでの和田卓人さんによる基調講演のまとめ。下記アーカイブで視聴可能
https://www.youtube.com/watch?v=Q-FJ3XmFlT8&feature=youtu.be&t=1145
テスト駆動開発とは
hr.icon
TDDの目的 -> 動作するきれいなコード
凡人でも、動作するきれいなコードに向かって行ける手法がTDD
TDDのサイクル
目標を考える
目標を示すテストを書く
失敗させる -> Red
コードを書く
テストを成功させる -> Green
リファクタ -> Refactor
上記を繰り返していく
準備
実装したい内容を、箇条書きのTODOに分解していく。
箇条書きにしたTODOを、重要性 * テスト容易性の四象限で考える
重要かつテストが簡単なものと、テストが難しくて重要でないものに寄せていく。
リファクタとは
マーティン・ファウラーの定義 -> 外部から見た振る舞いを変えずに内部をきれいにしていくこと
ケント・ベックの定義 -> 成功しているテストが成功しているままで、コードを理解可能にすること
外部から見た振る舞い -> あいまい
テストの成功 -> 成功 or 失敗の2値しかない
FizzBuzzのライブコーディング
hr.icon
FizzBuzz
3の倍数のときは「Fizz」、5の倍数のときは「Buzz」、3と5の倍数のときは「FizzBuzz」、それ以外のときは数値をプリントするプログラムを書く
1
2
Fizz
4
Buzz
.....
14
FizzBuzz
「プリントする」というタスクについて
printというタスクが動いたことを検証するテストを書くのは難しい
mockやspyなどで標準出力を捕まえて云々…する必要がある
テストできてもあまり嬉しくない
-> 重要度が低く、テスト容易性も低いため、テストは書かない
プリントするというタスクを「数を文字列に変換する」というタスクに置き換えると、テストしやすくなる
テストの書き方
テストコードは準備・実行・検証のステップに分けられる
何を検証すべきテストなのかを見失わずにすむので、検証するコードから先に書いていくのがおすすめ
コンパイルエラーを利用して実装クラスの生成も済ませてしまうと楽
最初のテストを通すときには最高に雑な実装でよい
code:kotlin
//テストコード
@Test
fun 1を渡すと文字列1を返す(){
//準備
val fizzBuzz = FizzBuzz()
//実行
val actual = fizzBuzz.convert(1)
//検証
assertEquals("1", actual)
}
//実装
class FizzBuzz {
fun stringfy(): String {
return "1"
}
}
もし上記のようなテストに失敗したら、テストコードに誤りがあるか、環境構築に失敗している可能性がある。
次のテストを書く場合、assert文を増やすか、新しいテストメソッドを作るか?
基本的には新しいテストメソッドを作ったほうが良い
一つのテストに複数のassert文を書くと、
assert文が失敗するとそこで実行が止まって後ろに書かれたassert文が検証されない
テスト名とテストの実態が乖離してしまいがちになる
IDEのテスト結果出力が見にくくなり、どのassertで失敗したか確認するコストが増える
↑のようなアンチパターンを「Assertion Roulette」と言うこともある
UIテストなどでテストの実行が重くならなければ、極力テストは分けて書く
code:kotlin
@Test
fun 2を渡すと文字列2を返す(){
val fizzBuzz = FizzBuzz()
val actual = fizzBuzz.convert(2)
assertEquals("2", actual)
}
return "1"のような強引な仮実装を訂正するための上記のようなテストを「三角測量」と呼んでいる。
仮実装を実装に直すにあたっては、毎回三角測量を経なければならないわけではない。
テストの予想結果と実行結果が一致している場合、調子が良いと判断
調子によって以下の3つのギアを使い分けて歩幅を調整
仮実装→三角測量→実装(調子が悪いとき、不安なとき)
仮実装→実装(三角測量を経由しなくても書けそうなとき、テストコードのリファクタをするとき)
明白な実装(テスト方法にも実装方法にも不安がないとき)
テストコードの構造化とリファクタリング
hr.icon
val fizzBuzz = FizzBuzz() のような重複が出現した場合、共通化させる。
2つ重複した時点でアウトとする2アウト派と、3つ重複した時点でアウトとする3アウト派がいる。どちらが正解というわけではない
テストコードは動作するドキュメント、仕様書としての価値を持たなければならない
テストコードを構造化しておくと良い。
JUnit5では、テストコードをクラス内クラスにネストできる。
code:kotlin
lateinit var fizzBuzz: FizzBuzz
@Before
fun setUp(){
fizzBuzz = FizzBuzz()
}
@Nested
class 数を文字列に変換する{
@Test
fun 3の倍数のときはFizzに変換する(){
val actual = fizzBuzz.convert(3)
assertEquals("Fizz", actual)
}
@Test
fun 5の倍数のときはBuzzに変換する(){
val actual = fizzBuzz.convert(5)
assertEquals("Buzz", actual)
}
}
@Nested
class その他の数のときは数をそのまま文字列に変換する {
@Test
fun 1を渡すと文字列1を返す(){
val actual = fizzBuzz.convert(1)
assertEquals("1", actual)
}
}
こうしておくと、実行結果も構造化されて見やすくなる。
足りないテスト15の倍数のときにはFizzBuzzを返すが無いことが、あとから見た別の人にとってもすぐに分かる。
メンテナンスコストを削減するため、三角測量のためのテスト2を渡すと文字列2を返すを削除しておくとなお良い。
三角測量のためのコードが残っていると、後々テストが見にくくなってしまう。
書いた意図や仕様を覚えているうちに本人が削除しておくべき。他の人があとから見て三角測量テストと判断して削除するのはかなり大変
こうしておかないと、テストは不良資産になってしまう。
TDDに必要なスキル
問題を小さく分割
歩幅を調整
仮実装→三角測量→実装
仮実装→実装
明白な実装
テストの構造化とリファクタ
粒度がバラバラのテストが残ってしまうと、後で見た人にとってわかりにくくなる。書いた本人が覚えているうちに整理する。
テストを資産化する行為
感想・反省
hr.icon
タスクのTODOへの分解をおろそかにしがちだった
Assert文を書きすぎないようにしたい
Assertion Roulette回避
テストのリファクタという観点がなかった
何を検証しているかわかりにくいテストはドキュメントとしての価値がない
テストを書き始めるときはassertionから先に書いていきたい
テストの目的を見失わないため