テスト駆動開発(TDD Boot Camp 2020)のメモ
YouTubeでTDD Boot Camp 2020 Online #1 基調講演/ライブコーディング、
t-wada(ケントベックの「テスト駆動開発」書籍の訳者)さんのLIVEを視聴。https://www.youtube.com/watch?v=Q-FJ3XmFlT8&feature=youtu.be
#TDDBC-Onlineチャネルです。
さらっとでてくるノウハウがとても美しかったので、メモです。
お気づきの点はぜひTwitter(@yb300k) へ。
TDDのサイクル
ざっくり言うと
実装する仕様を細かく区切った目標を決める→テスト書く→失敗させる→最短で成功するコードを書く→成功する→リファクタリング
をぐるぐる回す。
その流れをざくっと紹介。
1・問題を細かくわけて、個別に撃破。最初は聞いた仕様をそのまま書いてみるが、それはそのままToDoリスト(あるいはテスト項目)にはならない。
FizzBuzz問題(1から100までの数をプリントするプログラムを書け、ただし3の倍数なら数のかわりに「Fizz」とプリント、5の倍数なら「Buzz」プリント、3と5両方の倍数は「FizzBuzz」プリント)
上記はこのままだとToDoやテスト項目にはなってない。→ 後述します
ToDoリストができたところでどこから手をつけるかというと・・
1周目は重いので、軽いところからやるべし。(環境もファイルも何もなく、設計の意味合いが強いので)
テスト容易
| ①
需要度低い ーー |ーー 重要度高い
|
テスト難しい
2.3.最初にテストを書いて、失敗(Red)になってから
4.コードを書く
5.Greenにするためにはコピペなどキタナイ手を使ってもかまわない。(キタナイコードでも動作すればよい)
6.成功しているテストが成功しているままコードをきれいにするリファクタリングを行う。(外部からのふるまいがかわらない、ではない)
リファクタリングのやめ時は、
案1)時間。5分とか。
案2)重複をなくす。共通の処理を関数に切りだすとか。
ここまできたら、1.に戻ってToDoリストを見直す。
※ここからデモになっていったので、その流れにあわせて以下ノウハウを書いていきます。
個別に撃破するタスク分解の考え方の流れメモ
★観測が容易 なようにタスクを分割するのがポイント
・1から100まで素直にやるんじゃなくて、どこまでやればひと区切りできるだろう?(1,2,3,4,5,6,7,8・・・100まで全部書く人はプログラマじゃない)
・「プリントする」のテストの方法は?
→ プリント・・・目で見る?テストの仕組みが複雑であるわりに、あまり意味がない。テストやりにくい。→あとまわしだ
・「プログラムを書け」→これは自明だからいいや
・3の倍数、5の倍数、3と5両方 のToDo項目の書き方をあわせてわかりやすくする。
※プリントする、変換する、出力する、などごちゃごちゃにせず、一貫性を持った記述にする
3の倍数のときは数の代わりに「Fizz」とプリントする
を分解して、
3の倍数のときは数の代わりに「Fizz」に変換する +プリントする と分解。
5 でも同じように書く。
・最初に手を付けられる、テスト容易性:高、重要度:高 のToDoを作りたい
・「ただし」という文言があると、通常ではない振る舞い
正常系 があって、 ただし で異常系を作っているはずだ
1から100までの数をプリントする が 正常系だと思い出して・・・
対称な形になるように、正常系の文言をつくる
→1から100までプリント
を
(正常系として) 数を文字列に変換する というように分解すればOKだ
タスクの整理は、テストの書きやすい、書きにくいがわかるまで、トライアンドエラー
観察のしやすさ、十分小さいか、オブザビリティ
『クリーンアーキテクチャ』という本はテスト容易性は「入出力からの遠さ」と表現している。このように書籍の知識を得るのもおすすめ。
最初にできあがったToDoリスト
テスト環境の最初の準備
JUnitでテストを書き始める
テスト対象とテストユニットを対応させて名前を決めて、、「FizzBuzzTest」
まず fail("hoge") で、 FailureTraceにきちんと”hoge”が出ることを確認すること。
#JUnitつかえてないとか、環境構築がミスってた、でエラーになってたんだ!時間無駄した!!を防ぐために有効。
日本語でテストコードを書くと、わかりやすいのでおすすめ。
なぜなら、動作するドキュメント(仕様書)であってほしいから。
テストの書き方
4フェーズテスト 準備、実効、検証、後片付け のステップになる。
Javaだとガベージコレクションで後片付けがいらないので、
Arrange Act Assert (3A)ともいう
そして、一番下のAssert(検証)から書いていく。複雑なケースでも単一のゴールをぶらさないために、下から書いていく(上のArrangeから超大作を書いていくといろいろ混ぜたくなり、ぶれる)
Assertは 値の検証が基本(期待値 expectedになること をチェックする)
数を文字列に変換する()ってテストできる?
・・・・で、expectedは 何になるのか?!がわからなくなる
どうすれば数が文字列になったと言えるのだろう??
と手が止まったら、これはよく考えられていなかった ということに気づける(早いうちに気づいたほうがいい。これがテスト駆動開発のメリット)
ToDoに戻り、抽象度をさげて
「1を渡すと、文字列1を返す」に言い換える。こういう具体化抽象化の繰り返しもテスト駆動化初の特徴。
assertEquals("1",actual); ← ツールや言語によって、引数の順番をよく確かめること。大量にテストコード作った後で、逆だったと気づくと、とってもつらい
テストコードだけ書いて Runするとエラーになる。(コンパイルエラーもredとカウントする。)
#エラーが原動力になるので、これは悪いことではない。
コードを書き始める。
コードを書く時の心得
作る前に、「使う」 という意識でコードする。
コードの作りやすさと、使いやすさというのは一致しない。
今コードを書いている自分が書きやすいではなく、後で見た誰でも使いやすい、読みやすいほうが大事。
#わかりにくい関数名にしないとか、引数の並びが異常なのは止めようとか・・・
fizzbuzzクラスの、convert という関数ならわかりやすいなと考えて
Runすると、convert(1)からNullが返ってくるので、Redになった。
最短距離で走るために、まずconvert(int i) に return "1" を入れる。
ひどい茶番に見えるが、
この絶対に間違っていない茶番コードで、テストがGreenにならないとしたら、テストコードが間違っているということになる。
defect insertion
#テストコードにバグがあったらどうするの?という議論があったが・・・
テストコードのテストコードのテストコード・・・を書き続けるわけにはいかない。
初期段階でわざと判別可能な誤りを入れてみて、予想通りにテストが失敗することを確かめる。
★ convert関数のコードを、return "0"; に変えてみて、redになることをチェックする。
#mutation testing というテストの仕方もある。いじれるところ全部変更してテストし続けるが、恐ろしく時間がかかる。
テストが本当にredになることもあるかチェックしてみて、最終的にコードがGreenになるように修正、確認。
プロダクトコード優先でリファクタリング、その後 テスト コードもリファクタリングして1周目を完了とする。↓変数に関数の戻りを入れてから使っていたのを修正し、コメントも直している
三角測量
ToDoリストに戻り、三角測量 として、 次のToDo
2を渡すと文字列2を返す を増やしてテストを書き始める。
ここは最初の段階なので歩幅を小さめにしている。あまりジャンプしない。
テストコードのかたまりの書き方
テストを書くときに一つのメソッドにAssertを二つ並べるか?テストメソッドを増やすか? → 増やしていくべき。
なぜなら、2行assertEqualsを並べると、最初に失敗するまでの上にあるAssert行しかチェックしない(一度エラーになると、それより下は実行しない。テスト全部Greenで通るか見たいのに。)し、テストメソッド名と、テストの内容が一致していないことになるから。
アンチパターン:アサーションルーレット <大量にAssertが並んでいるテストコード
Greenになったら、リファクタリング。
リファクタリング(重複除去)のタイミング
テストコードのリファクタリングとしてnew FizzBuzz()の重複除去・・するか・・?
#2アウト派、3アウト派がいるので、上記2個の時にどうするかは迷うが、3個まで放置してもいい
リファクタリングしたらToDoリストに戻る。
メソッド内に複数のAssertionになってしまうときは分解が間違ってるか?
→まとめてAssertしないといけない状況であれば仕方ない。
でも分解ができてない状況もあり得るので確認のこと。
実行の重いEnd2Endのテスト、インテグレーションテストであれば、Assert1つの原則は外してもいい。
その形でくりかえす・・
最短距離でGreenにして、リファクタリングに進む という選択肢もあり。
newが重複している問題に対応
→前準備の重複を排除
テストメソッド間の依存関係
テストメソッドはどこから動作するかわからない(あえて、テストツール作成者が上から順に実行しないように散らしている)
テストメソッド同士の間に依存関係を作ってはいけない。
テスト1の次にテスト2が動くことを前提に、テスト1に前準備を入れたりしてしまうようになるから。すべてのテストを一気に流すとき分散型で処理することもあるので、依存関係があるとエラーが出まくってテストができないという実害がある。
@BeforeEachに前準備をまとめた結果がこれ。
そろそろテスト側の誤りに不安がなくなってきたので、コードをまともなものにしていく。リファクタリングも完了し、実装にも不安がなくなってきたら明白な実装をいきなり始める。
仮実装・三角測量・明白な実装の3つのギアで、歩幅を調整しながら進んでいる。
『実践テスト駆動開発』という本もバイブル
受入テストを先にするというやり方が書いてあるので、それをどうぞ
テストの構造化とリファクタリング
数年後・・・人がかわってこのコードとテストコードを引き継いだら?
上記のままだとテストを見てGreenで動くのはわかるし、ふるまいはわかるが、何をしたかったのかがよくわからない。
仕様レベルの言葉がないので、逆アセンブルして仕様を読み取ることになってしまう。
テスト駆動開発の成果が、動作するドキュメントになっていない。
なんでこうなった?
・抽象度を下げてしまってテストや実装を進めたので仕様が伝わらなくなっている。
→ テスト名を長くするというのがひとつの伝統的な解。
→ 他には、実装がツリー構造になっているところに目を付けて、テストコードの構造化が解になる。
抽象的な仕様と具体的な実装がツリー構造のToDoリストになっていることに目を付けて、ToDoリストのインデントを、テストコードに反映する
JUnitだと、@Nestedを使うことでネスト構造を作れる。
これでテスト結果 を見てみると
階層化のレベルがちぐはぐになっていることに気づく。
・補集合があったことに気づき、以下のようにきれいにする(ドメイン理解を進める)
以下のように書いておけば引き継いだ人も理解できる!
なぜ三角測量してるんだろう・・??
「テストを減らすことができるのは書いた本人だけ」
不要なテストは減らして人に渡す。
動くドキュメントとして使えるように、テストの仕様の構造、テストの中身が頭に残っているうちに、構造化・リファクタリングすることを通して、資産にすること。
#テストを不良資産にしないこと。使い続ける人がテストのメンテを引き受けないといけない。つらい。
参考にできそうなリンク
・t-wadaさんの 「私にとってのテスト」
https://www.slideshare.net/t_wada/testing-casual-twada
・タスクの分解に関しての知見
kyon mmさんの「テストとリファクタリングに関する深い方法論」
https://www.slideshare.net/KyonMm/wewlcjp
参考図書(として紹介されていた書籍)