2018/10/20のTDDBC 8thのレポート。
t_wadaさんの基調講演
テスト駆動開発とは
「動作するきれいなコード」がテスト駆動開発のゴール。
「動作するきれいなコード」はいきなり書けない。
まず動作するコードを書き、それからきれいにする。
テスト駆動開発を学ぶのは「テスト駆動開発」を手を動かしながら読むのが近道。
TDDの手順
- 作業項目のToDoリストを作る
- ToDoで作った項目に対するテストコードを書く
いきなり全部のテストを作るわけではなく、最初に倒す相手を決める 容易性で決めるのがお勧め
Javaであれば、パッケージ名、クラス名、ディレクトリ構成など決める必要があり、最初にやることは多い
タスク自体を簡単にしないと手が止まるので、難易度が高いものは重要であっても後にする - テスト実行して失敗することを確かめる
- 目的のコードを書く
- テストを成功させる(この時点ではコードは汚いまま)
- リファクタリングを行う
リファクタリング地獄にならないよう、以下のような条件でを終了する
- 時間(5分、10分など)でくぎる
- 重複の除去ができたら完了にする
- 次の相手を決めて相手する
ライブコーディング
FizzBuzzをお題にしたライブコーディング。
1から100までの数をプリントするプログラムを書け。ただし3の倍数のときは数の代わりに「Fizz」と、5の倍数のときは「Buzz」とプリントし、3と5両方の倍数の場合には「FizzBuzz」とプリントすること。
ToDoリスト作成
- 同値分割で考えればいきなり1から100までテストする必要はない。
- 正しく「プリントする」ことをテストする事は大変だが、そこにロジックはない。
Viewのテストはコアのテストとは分離する。 - 「ただし」の前には通常の処理があるはずなので、忘れずにToDo化する
コーディング
テストコードから書く。
class FizzBuzzTest { @Test void _1を渡すと文字列1に変換する() throws Exception { // 3. 前準備 FizzBuzz fizzbuzz = new FizzBuzz(); // 2. 実行 String actual = fizzbuzz.convert(1); // 1. 検証 assertEquals("1", actual); } }
「数を文字列に変換する」は具体的な値が書かれておらずToDoとして曖昧。 assertは具体的な値でしか書けない。assertを最初に書くことで具体的でなかったという事に気がつく。 ToDoが曖昧であることに気がついたら、ToDoに反映する
テストコードは、検証→実行→前準備と、ゴールから書く。
作ってから使うと、作ったものが使いにくいことに気が付きにくい。
作る前に使うことで、使いやすさを先に検証することができる。
assertのactualとexpectedの指定順はテストフレームワークにより異なるので最初に調べること。
最初はテストコードがfailになることを確認する。 正しくTestコードが実行されてエラーになることを確認する。
プロダクトコードはIDE任せの空実装。 この時点でテストコードを実行し、Redになることを確認する。
class FizzBuzz { public String convert(int i) { return null; } }
続いてテストをグリーンにするためにコードを修正。
class FizzBuzz { public String convert(int i) { return "1"; } }
グリーンになることを確認する。 ここでテストコードの正当性を確認する。
テストコードのテストは実装コードで行う。 テストコードを実装し、対応するプロダクトコードがまだできていない状況が、一番コストを低くテストコードの正当性を確認できるタイミング。
テストコードをリファクタリングする。
class FizzBuzzTest { @Test void _1を渡すと文字列1に変換する() throws Exception { // 前準備 FizzBuzz fizzbuzz = new FizzBuzz(); // 実行 & 検証 assertEquals("1", fizzbuzz.convert(1)); } }
三角測量のコードを書く。
class FizzBuzzTest { @Test void _1を渡すと文字列1に変換する() throws Exception { // 3. 前準備 FizzBuzz fizzbuzz = new FizzBuzz(); // 2. 実行 & 検証 assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2に変換する() throws Exception { // 3. 前準備 FizzBuzz fizzbuzz = new FizzBuzz(); // 2. 実行 & 検証 assertEquals("2", fizzbuzz.convert(2)); } }
Redになることを確認したら、プロダクトコードを修正する。
class FizzBuzz { public String convert(int i) { return String.valueOf(i); } }
Greenになることを確認したらリファクタリング。
class FizzBuzz { public String convert(int number) { return String.valueOf(number); } }
次のテストを追加。
class FizzBuzzTest { @Test void _1を渡すと文字列1に変換する() throws Exception { // 3. 前準備 FizzBuzz fizzbuzz = new FizzBuzz(); // 2. 実行 & 検証 assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2に変換する() throws Exception { // 3. 前準備 FizzBuzz fizzbuzz = new FizzBuzz(); // 2. 実行 & 検証 assertEquals("2", fizzbuzz.convert(2)); } @Test void _3を渡すと文字列Fizzに変換する() throws Exception { // 3. 前準備 FizzBuzz fizzbuzz = new FizzBuzz(); // 2. 実行 & 検証 assertEquals("Fizz", fizzbuzz.convert(3)); } }
テストコード間でコード重複が発生しているので、重複を除去する。
(重複の除去は2アウト派と3アウト派がいる)
重複している前準備のコードを別メソッドに切り出す。
テストコード間に依存関係は作らないこと。 JUnitのテストコードのテストメソッド実行順は上から順になっているわけではない。
class FizzBuzzTest { FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Test void _1を渡すと文字列1に変換する() throws Exception { // 2. 実行 & 検証 assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2に変換する() throws Exception { assertEquals("2", fizzbuzz.convert(2)); } @Test void _3を渡すと文字列Fizzに変換する() throws Exception { assertEquals("Fizz", fizzbuzz.convert(3)); } }
テストコードは具体例だけで、そもそもの仕様が抜けていることが多い。後から見たとき、そもそもの仕様がテストコードから読み取れない事がある。
仕様と具体例をネストして表現するのが良い。(JavaはJUnit5でできるようになった。テストフレームワークによってはできないものがあるかも)
@DisplayName("FizzBuzzクラス") class FizzBuzzTest { FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Nested class その他の数のときはその数を文字列に変換する { @Test void _1を渡すと文字列1に変換する() throws Exception { assertEquals("1", fizzbuzz.convert(1)); } @Test void _2を渡すと文字列2に変換する() throws Exception { assertEquals("2", fizzbuzz.convert(2)); } } @Nested class _3の倍数のときは数の代わりにFizzに変換する { @Test void _3を渡すと文字列Fizzに変換する() throws Exception { assertEquals("Fizz", fizzbuzz.convert(3)); } } @Nested class _5の倍数のときは数の代わりにBuzzに変換する { @Test void _5を渡すと文字列Buzzに変換する() throws Exception { assertEquals("Buzz", fizzbuzz.convert(5)); } } }
仕様で分けてみると、1と2の試験をやる意味がないことがわかる。
2のパターンは三角測量のために作ったもので、残しておく意味はないので消す。
「1~100まで」という条件なので、各ケースで最小値と最大値の試験を追加する。
@DisplayName("FizzBuzzクラス") class FizzBuzzTest { FizzBuzz fizzbuzz; @BeforeEach void 前準備() { fizzbuzz = new FizzBuzz(); } @Nested class その他の数のときはその数を文字列に変換する { @Test void _1を渡すと文字列1に変換する() throws Exception { assertEquals("1", fizzbuzz.convert(1)); } @Test void _98を渡すと文字列98に変換する() throws Exception { assertEquals("98", fizzbuzz.convert(98)); } } @Nested class _3の倍数のときは数の代わりにFizzに変換する { @Test void _3を渡すと文字列Fizzに変換する() throws Exception { assertEquals("Fizz", fizzbuzz.convert(3)); } @Test void _99を渡すと文字列Fizzに変換する() throws Exception { assertEquals("Fizz", fizzbuzz.convert(99)); } } @Nested class _5の倍数のときは数の代わりにBuzzに変換する { @Test void _5を渡すと文字列Buzzに変換する() throws Exception { assertEquals("Buzz", fizzbuzz.convert(5)); } @Test void _100を渡すと文字列Buzzに変換する() throws Exception { assertEquals("Buzz", fizzbuzz.convert(100)); } } }
ここまでテストケースで表現することで、仕様と具体的なテストケースを表現することができる。
(FizzBuzzになるケースはやってない)
テストコードのメンテナンスについて
テストコードを実装してきた多くの企業で、過去のテストコードのメンテナンスに時間を取られる状況が発生している。 これからの時代は、テストのメンテナンスコストも考える必要がある。
まとめ
TDDのスキル
- 問題を小さく分割する
- 歩幅を調整する
- テスト → 仮実装 → 三角測量 → 実装 (不安があるうちは歩幅を小さく)
- テスト → 仮実装 → 実装
- テスト → 明白な実装 (自信が出てきたら歩幅を大きく)
- テストの構造とリファクタリング
演習(ペアプロ)
今回はC# - MSTestで参加した。
1台のPCを2人で順番に使用するスタイルで実施。
以下、感想。
ToDoリストへのフィードバックは行わずに作業していたが、次の作業に進んでよいか、どこまで出来ているかの意識がペア間で曖昧になった。
どこまで出来たかをペアと意識合わせるためにもToDoで明示するのは大事。コピペでテストケースのメソッド名書いてたら、修正ミスが以外に多かった。
後々でテスト名だけ見たら何のテストか分からない事になるので注意が必要。
同じようなコード書くのでテストケースはコピペで実装してしまう事が多いが、コピペは止めた方がよいかも。テストケースのメソッド名に使える文字に制限あるのは辛い。(MSTestの場合) 言語仕様に抵触しないようにメソッド名をどう書くかという、本質でないところで結構悩んだ。
Equalsのnull値との比較やGetHashCodeなど、ボイラープレートのコードに対するテストコードを書く必要があるか悩んだ。
- 悩んだが、プロダクトコードの動作保証としては必要じゃないかと考えたので書いてみた。
- 開発を進めるためのテストコードと、将来的なメンテナンスを見据えて残すコードは別物と考えた方がいいのかも。
コードの「気持ち悪さ」をペアと共有するのは難しい。虚数単位の”i”がコード上に3回出てくるのが気持ち悪いかどうかで意見が合わなかった。
2人が半日で実装したプロダクトコードが50行程度。
仕事でこんなペースでコード書くのは許されないだろうな、、と思ったりした。レビュー受けたかった。。
コードは当日のペアの方のPCにしかないため、C#の勉強がてら自分で組み直してみた。
最後に
最近仕事でコード組んでおらす、久しぶりのコーディングだったが楽しかった。 運営の皆さん、演習のお題を考えてくれた@i_takehiroさん、ありがとうございました。