前々から興味のあったTDDについて本格的に学び始めました。
TDDを始めたくなった理由のひとつに、TDD Changed My Life という記事を読んだことがあります。
TDDには学習曲線があって、続けていると、テストを書いているにも関わらず、書かないよりも早く実装できるようになった
という体験が書かれていました。
TDD has a learning curve, and while you’re climbing that learning curve, it can and frequently does add 15% — 35% to implementation times. But somewhere around the 2-years in mark, something magical started to happen: I started coding faster with unit tests than I ever did without them.
https://medium.com/javascript-scene/tdd-changed-my-life-5af0ce099f80
これってムチャクチャ楽しそうじゃないですか?自分も是非やってみたいと思ったのです。
というわけで、「テスト駆動開発」を読んでいきます。
- 第1章 仮実装
- 第2章 明白な実装
- 第3章 三角測量
- 第4章 意図を語るテスト
- 第5章 原則をあえて破るとき
- 第6章 テスト不足に気づいたら
- 第7章 疑念をテストに翻訳する
- 第8章 実装を隠す
- 第9章 歩幅の調整
- 第10章 テストに聞いてみる
- 第11章 不要になったら消す
- 第12章 設計とメタファー
- 第13章 実装を導くテスト
- 第14章 学習用テストと回帰テスト
- 第15章 テスト任せとコンパイラ任せ
- 第16章 将来の読み手を考えたテスト
- 第17章 多国通貨の全体ふりかえり
- 第18章 xUnitへ向かう小さな一歩
- 第19章 前準備
- 第20章 後片付け
- 第21章 数え上げ
- 第22章 失敗の扱い
- 第23章 スイートにまとめる
- 第24章 xUnitの全体ふりかえり
- 第25章 テスト駆動開発のパターン
- 第26章 レッドバーのパターン
- 第27章 テスティングのパターン
- 第28章 グリーンバーのパターン
- 第29章 xUnitのパターン
- 第30章 デザインパターン
- 第31章 リファクタリング
- 第32章 TDDを身につける
第1章 仮実装
TDDの基本的な流れからスタートしています。
- まず、TODOリストを作る
- コードを書き始めるときに、実装ではなくテストから書き始める
- 「用途にあった完璧なインターフェイス」を想像する
- 外部から呼び出すときの振る舞いから逆向きに作業していく
- 「用途にあった完璧なインターフェイス」を想像する
- 汚いテストコード(実装のコードも)でも、とりあえずコンパイルを通す
- テストが失敗することを確認する
- テストが通る実装を書く
- リファクタリングを行い、重複を除去する
コラム「依存性と重複」で、依存性の問題がある場合、症状として、コードの重複が見られるとしていて、常に重複を取り除いていくことで、依存性が取り除かれ、将来の変更に強くなる確率を上げている。とのこと…
テストを先に書いていても、コードの重複は見逃しがちだったので、大事にしたいです…
第2章 明白な実装
第2章もテストを先に書いて、コードを実装します。
ただ、実装方法が明白にわかっているなら、第1章のようにreturnの値をベタ書きしたりせず「わかっている実装をそのまま書く」という内容です。
実装が簡単なら
- 「明白な実装」モードで進めて、難しくなったら
- 「仮実装」モードで少しずつ実装->リファクタリングで重複をなくしていく
第3章 三角測量
3章ではTDDで利用される「三角測量」というテクニックの説明のため、DollarオブジェクトをValue Objectパターンで実装していきます。
三角測量とは、複数のパターンのテストを書いて、コードの一般化をすすめる手法です。
- どうやってリファクタリングしたらいいかわからないとき使う
三角測量はなんだかんだでわかりやすかったので、Value Objectパターンのほうが気になってしまいました…。
Value Objectパターン
- メリット: 別名参照を気にする必要がなくなる
- 別名参照問題の回避はだいたいvalue objectパターンがよさそう…?
- Value Objectについて整理しよう
- 別名参照問題と値オブジェクト
- 制約: コンストラクタで設定したインスタンス変数の値がかわってはならない
- 操作: 新しいオブジェクトを返す
- 操作: equalsメソッドが実装されている
- 値の同一性比較のため
第4章 意図を語るテスト
テストを見て、対象の関数が何を返すかわかりやすいように修正していきます。
- テストが通っていたとしても、ドキュメント的な観点から「オブジェクトを返す」ということがわかるように修正した
- 副次的に、amountプロパティをprivateにすることで、不必要な変数を外部にさらさなくて良くなった
この章のまとめでは、「正しく検証できていないテストが2つあったらお手上げだが、そのリスクを受け入れて先に進む」ことを肯定していました。
TDDは銀の弾丸というわけではなく、テストそのものの質が重要そうです。
第5章 原則をあえて破るとき
TODOリストの「$5+10CHF=$10」の開発に取り掛かろうとします。
- 上記はTODOリストとして、粒度が大きいので、まずFrancクラスのテストを書くことにした
- 5CHF*2=10CHF
- 開発速度を得るために良い設計の原則を破る
- (=コピペする)
- 新しい機能がどの状態にあるのかわかるところまで
- (=以下の3ステップの間だけ)
- テストを書く
- コンパイラを通す
- テストを走らせ、失敗を確認する
第6章 テスト不足に気づいたら
DollarとFrancを共通化するため、Moneyを親クラスにして継承させる設計を考えつき、沿うようにコードを修正していきます。
- 修正の中で、Dollar同士の比較のテストはあるが、Franc同士の比較のテストが無いことに気づく
- (=テストの不足に気づく)
- 気づいた不足について、テストを追加する
- 必要があれば、後からテストを追加していく
- 追加したテストが通るように修正、一般化をすすめる
第7章 疑念をテストに翻訳する
コードを書いている際に疑問が出てきたら、それをテストにおとしこもう、というお話です。
- FrancとDollarが正しく比較できるのか、検証する
- 同一かどうかでいうと、Falseになってほしい
- が、実際Trueになってしまう
- 親クラス(Moneyクラス)で共通化した箇所なので、当然ですが…
- getClassメソッドで、呼び出しているクラスを取得し、一致するかどうかも同一かどうかの条件に含めることで、解決する
「モデルのコードにこんなふうにクラスが登場するのは少々不吉な臭いがする」というのがちょっとわかりづらかったのですが、その直後の「比較はJavaオブジェクトの世界ではなく、財務の世界の言葉で行いたい」ということから、ここでgetClassが出てくることが、何をやっているかわかりづらい(どのオブジェクトに呼び出されているかは実行しないとわからない)、ってことだろうと理解しました…
その直後の文脈からも、getClassではなく、Currencyを比較したいようなことが書いてありました。
第8章 実装を隠す
DollarとFrancのtimesメソッドを共通化していきます。
- Factory MethodとなるdollarメソッドをMoneyに定義する
- dollarメソッドがDollarインスタンスをNewして返す
- だからFactoryなんですね…
- dollarメソッドがDollarインスタンスをNewして返す
- テストからサブクラスの存在を切り離して「実装を隠」している状態にする
Factory Method。なんとなくわかっているけど、ちゃんとデザパタ勉強していないので、わかってないです。。。
オブジェクトの生成は親(抽象)クラス、処理は子クラスってイメージです。
第9章 歩幅の調整
サブクラス(DollarやFranc)を共通化したいので、色々細かく試します。
このへん、Flyweightパターンが突然出てきますが、GoFデザパタはエンジニアとして履修していて当然という空気怖い👺(冗談です
- Currencyメソッドを作ってみる: ひとまず通貨名(文字列)を返す
- DollarとFrancの実装を近づけていって同じものにしたい
- インスタンス変数に通貨名を持たせて、Currencyメソッドでそれを返すだけにする
- 上記で、Currencyメソッドの実装はDollarもFrancも全く同じになった
- ので、Currencyメソッドの実装を親クラスに引き上げる
- さらに、DollarとMoneyのコンストラクタがかなり似てきたので、この2つを共通化できそう
- Currency情報(現時点では、通貨の名前の情報)はコンストラクト時に親クラスで指定するようにすれば、子クラスのコンストラクタは共通化できる
- コンストラクタを共通化して、親クラス(Moneyクラス)へ移す
ここまででサブクラスのコンストラクタまで共通化することができました!
なんか残ってるtimesメソッドもほとんど同じに見えますね…。
かなり小さい歩幅で進んでいるように見えますが、それもあえてやっているようです。
「読者の皆さんが本当にこのような手順を踏むべきだと私が考えているかというと、答えはノーだ。私が伝えたいのは、ステップを小さくもできる、ということだ。」
第10章 テストに聞いてみる
サブクラス(DollarやFranc)をなくして、完全にMoneyひとつにしてしまいたく、timesメソッドの共通化に取り掛かります。
- timesの戻り値をインラインでnewしたMoneyクラスにしてみる
- 前章でファクトリーメソッドを利用したばかりだが…
- 上記のおかげでtimesメソッドも親クラスに引き上げられた
- 親クラスがabstractでなくなる
- エラーが出たときの例外的な対処
- デバッグ用にコードを書き換える(printなどのデバッグ出力)にはテストコードを書かないでよい
- toStringをオーバーライドして、デバッグ出力をわかりやすくしてみた
- 結果的にあまりわかりやすくなってないが…
- toStringをオーバーライドして、デバッグ出力をわかりやすくしてみた
- 原因: 以前実装したgetClass()により通貨が同じかどうかを確認していた箇所
- コードの設計を変えることで「通貨はクラスで指定する」という設計に依存していた箇所がエラーを吐いてしまった
- 対処: テストがレッドになった変更を巻き戻し、グリーンバーに復帰する
- 対処2: テストをもう一個書き、それを通る実装に変更する。
- 別々のクラスでもamountとcurrencyが一致すれば、同一のものであることを確認するテスト
- getClass()でクラスを比較するのではなく、currency()の値を比較するように実装変更
- デバッグ用にコードを書き換える(printなどのデバッグ出力)にはテストコードを書かないでよい
- 最終的に、サブクラスを消して、Moneyクラスひとつに集約することができた
この記事ではなるべく概要を書くようにしているのですが、
本書が「細かいリファクタリングのステップ」を重視しているので、
概要だけ見ると遠回りにコードを書いているように見えます…
エラーが出たところとか、どこまで戻すかパッと見わかりづらく、少し難しく感じました。
第11章 不要になったら消す
前章までのリファクタで不要になったサブクラスと関連するテストを削除します。
- Dollarクラスを削除する
- Francオブジェクトをreturnしていたfrancメソッドを検証するために書いていたテストを削除する
- サブクラスが複数存在することを前提として書かれたtestDifferentClassEqualityを削除する
- Francオブジェクトも削除する
- Francオブジェクトを参照していた上記テストがなくなったので
- DollarとFrancの掛け算はもともと別のクラスで行っていたが、同じクラスのcurrencyを利用するようになったので、testFrancMultiplicationも削除する
第12章 設計とメタファー
TODOリストがごちゃごちゃしてきたので、未解決の項目を新規リストへ移す。
- $5+$5=10のテストを書く
- 5$+10CHF=10$を実装するための小さな歩幅として
ここまでは簡単です…
Imposterオブジェクトを使い始めたところから難しく感じました…
- 「多国通貨間の計算をどう表現するか」がむずかしい
- 「複数の通貨を扱っていることをほとんど意識させないコードにしたい」という制約のため
- Imposterオブジェクトを利用する
- Moneyのように振る舞うが、2つのMoneyの合計を表現するようなオブジェクト
あるオブジェクトが望むように振る舞えないのならば、同じ外部プロトコル(メソッド群)を備える新たなオブジェクトを実装し、仕事をさせればよい
- ExpressionオブジェクトがImposterとなる
- Expressionオブジェクトが重要な機能になりそうだったので、なるべく肥大しないようにBankオブジェクトと分けた
- Bankオブジェクト: ここではreduceメソッドを実行するだけ
- reduceメソッド: Expressionに為替レートを適用するメソッド
第13章 実装を導くテスト
$5+$5=$10の実装を済ませるべく、前回べた書きしたBankオブジェクトの実装をしていく。
- 本来5$+5$の結果(10$のMoneyオブジェクト)を返す必要がある
- 今はMoney.dollar(10)(結果そのもの)を返すようベタ書きしている
- まずは$5+$5がMoneyオブジェクトを返すようなテストを書く
- 歩幅を小さくしている?
- testPlusReturnSum
- 外部から見たふるまいのテストではなく、内部の実装に深く関係したテスト
- すぐなくなる予定
- Moneyオブジェクトを返すplusメソッドを使っていたので、エラー出る
- plusメソッドがSumオブジェクトを返すように修正する
- もちろんSumオブジェクトは実装がないので、コンストラクタを定義する
- SumオブジェクトはExpression Interfaceの実装
- (Imposter(Moneyのように振る舞うが、Moneyの合計を表現するようなオブジェクト))
- plusメソッドがSumオブジェクトを返すように修正する
- reduceメソッドのテストを書く
- いったん足し算sumのテストに限定したtestReduceSumを作る
- 要件: 足し算の結果の金額を持ったMoneyを返すこと
- BankからSumにreduceの一部を移す
- このため、Bank.reduce(Money)のテストが必要になる
- ちなみに、Bank.reduceはExpressionのreduceのラッパー
- 「クラスの明示的なチェックはポリモフィズムに置き換えるべき」
- reduceメソッドをExpressionインターフェイスに加える
- このため、Bank.reduce(Money)のテストが必要になる
第14章 学習用テストと回帰テスト
いよいよMoneyオブジェクトを変換して換算していきます。
- addRateを作る
- これはひとまずUSDとCHFを登録するもの。この時点ではカラ。
- Money.reduce()の実装
- これも取り急ぎ、スイスフランからUSDへの変換の場合はrateが2にハードコード(P.102)
- 上記の実装だとMoneyオブジェクトが為替レートを知っていることになる
- 為替レートはBankオブジェクトの責務のはず
- 「為替レートを持っているBankオブジェクトをreduceの引数として渡す」が正しそう(オブジェクトの委譲かな?)
- rateはBankのrateメソッドとして作って、Moneyのreduce使用時にはBankにrateを問い合わせる形
- 為替レートはBankオブジェクトの責務のはず
- レートの計算に使った2がマジックナンバーなので、なくしていく
- ハッシュテーブルを使おうとしたが、いくつか不安点がある
- そもそも通貨名が2つはいった配列をハッシュテーブルのキーに使えるか?
- equalsメソッドが配列の中の要素の等価性比較を正しく行えるか
- 上記2点の確認のため、テストを書くが、結局失敗するので、Pairクラスを作成する
- Pairインスタンスをハッシュテーブルのキーとして使うには、equalsメソッドやhashcodeメソッドが必要になるが、リファクタリングの過程だから、Pairクラスのテストも書かない
- HashMapクラスを利用して、Pairオブジェクトをキーとしたレートのハッシュテーブルをつくった
- ここで予期せぬエラーが起きたが、リグレッションテストを書いてエラーを解決した
- ハッシュテーブルを使おうとしたが、いくつか不安点がある
第15章 テスト任せとコンパイラ任せ
やっと$5+10CHFのテストを書く段階に入ります
- 普通にテストを書くと、Moneyオブジェクトのplusメソッドでエラーが出る
- Expressionオブジェクトではじめ書いていたところをより具体的なMoneyオブジェクトに落として
ここでは「より具体的なテストを書いてまずは動作させ、そこから着実に一般化を開始する道」を選んでます。
- Sumオブジェクトのreduceメソッドの中で、被加算数と加算数がreduce(通貨の統一)されていないことが原因なので、修正する
- ここまでやると、Moneyで書いているけど、より抽象的なExpressionに置き換えられる箇所が出てくるので、置き換えていく
fiveBucks変数の型をExpressionに変えたとき、エラーが出ますが、ここでは「コンパイルを信頼し、自分がミスをしたら必ず教えてくれると考えてこのまま突き進む道」を選んでます。
MoneyをExpressionに一般化できそうです。
第16章 将来の読み手を考えたテスト
前章で空実装を行ったSumオブジェクトのplusメソッドをきちんと実装していく。その後はtimesメソッドも。
- plusメソッドのテストを書く中で、意図をわかりやすくするためにnew Sumしている。
- plusメソッドは内部でnew Sumしているので、あえてnew Sumをtest側でかく必要はない…
- 次に、timesメソッドを実装していく
- テストを書いて、Expressionインターフェイスにtimesメソッドを宣言
- $5+$5がMoneyを返す確認をするテストを書く
- が、instanceofを使ってクラスの状態そのものを見ている
- =実装に踏み入ったテストになってしまっている
- 「このタスクは実験」とのことで、いったんこのテストはなかったことにする…
- が、instanceofを使ってクラスの状態そのものを見ている
第17章 多国通貨の全体ふりかえり
ひとまず多国通貨が実装できたので、振り返ります。
- ここから先はどうなるか?
- 「テストが足りているか」という問いを投げることができる
- 「こう動いてはならない」テストを書いてみる
- TODOリストが空のときは、設計を見直すいいタイミング
- 設計の言葉と概念に齟齬はないか?
- 現在の設計では除去が難しい重複はないか?
- 「テストが足りているか」という問いを投げることができる
- メタファー
- 何度も同じ設計、実装を書き直すことで、新しい洞察や驚きを発見できるかもしれない
- 何度も書き直すことで「式(expression)のメタファー」ひらめいたよう
- 著者自身の体験から
- 何度も書き直すことで「式(expression)のメタファー」ひらめいたよう
- 何度も同じ設計、実装を書き直すことで、新しい洞察や驚きを発見できるかもしれない
確かに、expressionというクラスについてはかなり抽象化されている気がしました。(自分では考えつかない…)
逆にいきなりこれを言われても有効な設計かどうかわかりづらいとも思いました。
- 多国通貨サンプル作成を通してのJUnitの使用状況
- 125回テスト実行
- 1分に1回テスト実行
- コードメトリクス
- クラスやメソッドの行数など
- テストコードが長い(プロダクトコードの約2倍)のと、全体的に1メソッドの行数が短い
- プロダクトコードの1メソッドの行数が4.5
- テストコードの1メソッドの行数が7.8
- プロセス
- 小さいテストを追加
- すべてのテストを動かし、失敗があることを確認する
- 変更する
- 再びすべてのテストを動かし、全て成功することを確認する
- リファクタリングを行い、重複を除去する
- テスト品質
- 「ステートメントカバレッジはテスト品質を測るための十分な指標にはならないが、スタート地点になる」
- 欠陥挿入:「プロダクトコードの任意の行の意味合いを変えたら、テストは失敗しなければならない」
- 「テストを増やすことによってすべての入力の組み合わせをカバーするのではなく、同じテストのままでコードを減らすことによってより多くの組み合わせをカバーする」
- 最後のふりかえり
- 仮実装、三角測量、明白な実装
- テストとコードの間の重複除去
- テスト間のギャップを制御する能力
第18章 xUnitへ向かう小さな一歩
多国通貨オブジェクトの開発は17章までで終わり、ここからテストツールそのものの開発が始まります。
- テストメソッドが呼ばれたらtrueを出力し、呼ばれなかったらfalseを出力するプログラム
- テストケースにフラグを用意
- テストメソッドを走らせる前にフラグがfalseであることを確認
- テストメソッドが走ったらフラグがtrueであることを確認
- 名前はWasRun。メソッドが走ったかどうかを答えるテストだから
- 現時点ではPython組み込みのassertメソッドを利用して検証
- runメソッドまで実行できたら、リファクタリングしてみる
- といっているが、リファクタリングしてない?
- testMethodの動的な呼び出しに取り組む
- 「テストケース名を示す属性を問い合わせ、帰ってきたオブジェクトを関数のように呼び出せば、メソッドが呼び出される」
- Pluggable Selectorパターン…?
- メソッド名が入った変数に()をつけるだけでメソッドが実行される…
- あれっ動的だな。メタプログラミング感ある…
- PHPでもこういうのあった気がするけど、あまりおすすめされていなかった気がする…
- 「テストケース名を示す属性を問い合わせ、帰ってきたオブジェクトを関数のように呼び出せば、メソッドが呼び出される」
- もう1つリファクタリングできそう。
- TDDは「一般化対象の動作を実際に確かめながら進めるので、頭の中だけで悩まずに済む」
- WasRunクラスが2つの仕事を担ってしまったから
- メソッドが起動されたかどうかの記録する仕事
- テストメソッドを動的に呼び出す仕事
- 親クラスTestCaseに分離する
- name属性, runメソッドを親クラスに引き上げる
第19章 前準備
前準備として、テストのsetUpメソッドを作っていきます
- パフォーマンスと独立性を比べたとき、独立性を重視して、テストごとに新しいオブジェクトを作成(setUp)するよう、仕様を決める
「setUpメソッドを呼ぶ責務はTestCaseにこそあるべきだ」とあり、TestCase(WasRunの親クラス)にからのsetUpメソッドを作り、WasRunでオーバーライドする形を取っていますが、後で親クラスに集約したいのかな…?
この時点では後でどうするかはわからないです
- setUpメソッドを使ってWasRunクラスのコンストラクタをなくした(シンプルにした)
- setUpメソッドを使ってTestCaseTestでメソッドごとに都度WasRunクラスのインスタンスを生成していた箇所を共通化した
第20章 後片付け
setUpメソッドの他に、tearDownメソッドなど追加していくと、フラグだけでは管理が大変になります。そこで、ログに呼び出されたメソッドを記録していく形で実装します。
- WasRunクラスのsetUpメソッドに今やっていることをログに吐き出すように書く
- testSetupメソッドが複数のテストの内容を併せ持つようになったので、testTemplateMethodに変更
- tearDownのテスト->実装
- 親クラスのtestCaseクラスにtearDownを空実装しないとエラーが出るので、実装
- tearDownを利用するrunメソッドが親クラスにあるから、tearDownそのものも、親にないとエラーになる
第21章 数え上げ
テスト結果のレポート機能に着手していきます。
- エラーが出てもtearDownがきちんと動くように、例外をキャッチする必要がある
- 全部のテストケース結果を集めてレポートさせるのは範囲が大きすぎる
- 次善の策として、TestCaseクラスのrunメソッドがテスト結果を記録したTestResultを返すようにする
- TestResultクラスをつくってテストの実行結果を記録する
- TestCaseクラスのrunメソッドが上記を返すようにする
- runCount
- 実行回数
- testStarted
- 実行時にrunCountをインクリメント
- 上記で成功時のカウントはできるようになったが、失敗時のカウントも作る
- 例外をキャッチし、テストの失敗を記録したかったが、いったん棚上げする
- 「もう少し小さいテスト」を追加する
第22章 失敗の扱い
21章で棚上げした「もう少し小さいテスト」を書いていく
- 「テストが失敗しても期待した内容がきちんと出力されるかどうか」確かめる
- 「resultオブジェクトに対し、テスト開始時にtestStarted、テスト失敗時にtestFailedと順番にメッセージが送られて欲しい」
- 「結果がきちんと出力されたならば、それは正しい順番でメッセージが送られたということ」
- 正しい順番とは?
このあたり、ちょっと難しい。自分の理解では以下のようなこと…
- 小さいテスト->testFailedResultFormattingメソッド
- 大きいテストの復活->testFaildResultのアンコメント
- 小さいテストの際に導入した仕組み?
- summaryメソッドでreturnされるerrorCountのことかな…?
- 潜在的な問題->setUpメソッドでエラーが起きてもキャッチされないこと
第23章 スイートにまとめる
22章でテスト結果の出力部分が重複しまくっているので、重複をなくし、テストをまとめて実行する機能を作っていきます。
- TestSuiteクラス
- Compositeパターンを利用する
- 「個別の要素とコレクションが同じメッセージに応えられなければならない」
- =「同じシグニチャのメソッドが定義されていなければならない」
- CompositeパターンにはCollecting Parameterパターンが併用されることが多い
- 結果格納用のオブジェクトを渡す
- 結果が蓄積されるような場合によく使われる
- 「個別の要素とコレクションが同じメッセージに応えられなければならない」
- TestResultのインスタンス作成部分がTestCaseTest内で重複していたので、setUpメソッド内に共通化した
第24章 xUnitの全体ふりかえり
xUnit全体の振り返りを行っていきます。
- ここでは実装の詳細よりもテストケースが大事
- 自分でxUnitを実装するのが大事
- 熟達:「自分自身で実装することで自分が一番良く知っている道具が手に入る」
- 探索:新しい言語を触るときはxUnitの実装をしてみると、ある程度プログラミングに必要な機能は登場している
- アサーションの失敗とエラーには違いがある
第25章 テスト駆動開発のパターン
第3部に入っていきます。第3部は「TDDの「ベストヒット」パターン集」。
テストとは
- ソフトウェアのテストは自動テストを書く
- 「自動的に実行されるテスト」と「ボタンを適当に押して画面を見てみる」は違う
- 負のループ: ストレスがかかるとテスト実行が減る->エラーが増える->ストレスがかかる以下無限ループ
- テストを自動実行されるようにすれば負の循環から抜け出せる
- 急いでいるときでも、自動テストがあれば不安の度合いを制御するチャンスがある
独立したテスト
- テストは他のテストに影響を及ぼすべきでない
- 絶対に
- 副次的なメリット: 高凝集かつ低結合の設計にたどり着ける
TODOリスト
- コードを書く際はどこに行くべきかがわかるまでは、一歩も踏み出さないこと
- 「やるべきかもしれないことが増えると、今何をやっているのか見失いやすくなる」
- 新しく何かを考えついたときには「すぐやる」「あとで」のリストに加えるか、やる必要がないか判断すればよいだけ
- 「テストを一気に書き上げる方法は2つの意味でうまくいかなかった」
- 既に書かれているテストはリファクタリングの際にはある種の慣性を伴う
- 一気に変更しようとすると「面倒だな」と感じる
- 複数のテストが失敗しているなら、グリーンバーからかなり離れている
- グリーンバーから行える変更は1つだけという基本
- レッドバーの状態が長いとストレスになる
- 既に書かれているテストはリファクタリングの際にはある種の慣性を伴う
テストファースト
- いつテストを書くべきか?
- テスト対象のコードを書く前
- テストを先に書くとストレスを減らせる
- ストレス図の逆
アサートファースト
- いつアサーションを書くべきか?
- 最初に書く
- システム構築はどこからはじめるべきか?
- システム構築が終わったらこうなる、というストーリーを語るところから
- 機能はどこからはじめるべきか?
- コードが書き終わったらこのように動くという、テストを書くところから
- テストは?
- テストの終わりにパスすべきアサーションを書くところから
- アサートファーストで書くと、テストがシンプルになる
テストデータ
- テストには読み手がいることを考える
- 同じ値は1つのものを表すためだけに使う
- 本物に近いデータを使う
明示的なデータ
- 期待値と結果をテスト自身に含め、関係が明確にわかるようにする
- アサーションの中に式を書く
- わかりやすくなるなら、マジックナンバーも時には許容する
- ケースバイケース
第26章 レッドバーのパターン
「いつどこにテストを書き、いつやめるか」
- 一歩を示すテスト
- 「TODOリストから次に各テストを選ぶとき」
- 「そこから何かを学ぶことができ、かつ実装に自信を持てるようなテストを選ぼう」
- はじめのテスト
- 「この機能はどこに属するべきだろうか」
- 本物に近いテストはフィードバックを得るのに時間がかかりすぎる
- 博士論文が必要な難しい実装をテストファーストで書けるか?
- 「学ぶことがありそうで、かつ、すぐに書けそうなテスト」を選ぶ
- よくわかっている実装については、機能が1つか2つ必要になるテストを書く
- 自信と実績がすでにあるから
- 「この機能はどこに属するべきだろうか」
- 説明的なテスト
- 「他の人とテストコードの形で知識を共有するようにしよう」
- 学習用テスト
- よく知らないソフトウェアのテストを書くのは、
- そのソフトウェアの新機能を初めて使うとき
- メソッドを使おうとする代わりに、APIが期待どおりに動作するかテストを書いて確かめる
- もし学習用テストが失敗したら自分たちのコードのテストも動かないはず。
- 学習用テストが通れば自分たちのコードのテストも動くはず。
- よく知らないソフトウェアのテストを書くのは、
- 脱線はTODOリストへ
- 「会話を本筋から離れないようにすればするほど窮屈になり、優れたアイデアは生まれにくくなる」
- だが日常のプログラミングは平凡
- 最善の道は脇道にそれないこと
- アイデアは本来の仕事に割り込ませず、TODOリストへ
- 回帰テスト
- (完璧なプログラマなら、)回帰テストはコードを書いた時に同時に書かれるべきであったテスト
- 後から必要になるということは、システムからの「お前はまだ設計を終えていない」というメッセージ
- 休憩
- 時間単位、日単位、週単位で定期的に予定を入れたりして、休憩を取れるようにする
「疲れれば疲れるほど、疲労自体に気づきにくくなり、もっと作業を続け、さらに疲労する。」
身に覚えがありすぎて…
- やりなおす
- 手詰まりになったときはコードを捨ててやり直す
- 安い机に良い椅子
- いい椅子を買え
- あとハードウェアもね
第27章 テスティングのパターン
- 失敗する大きくなりすぎたテストは、小さく分割する
- Mock Objectパターン
- オブジェクトの可視性に留意するようになる
- コードの可視性の向上につながる
普通のオブジェクトのモックという認識ですね〜
- Self Shuntパターン
- 「オブジェクトが他のオブジェクトときちんとやり取りしていることをテストしたいとき」
- テストケース自身がモックオブジェクトのように振る舞う
わかりやすく説明しようとして難しくなっている気がしたのですが、MockObjectは作るまでもなく、テストケースそのものをMockObjectとすることで、簡潔に書いている、くらいの認識…
- Log Stringパターン
- 正しい順序でメソッドが呼ばれていることをテストしたいとき
- 記録用文字列にメソッド呼び出しのたびに追記
- Self Shuntパターンと相性がいい
- Mock Objectを兼ねるテストケースのクラスそのものがLogの機能を持つ実装ができるから
- Crash Test Dummy パターン
- 発生させづらいエラー処理部分のテストをしたい時に使う
- 例外を発生させる特別なオブジェクトを作る
- 無名内部クラスを使えば、いちいちダミー用のクラスを作らないでも、テスト時に本来の動きをするクラスを継承し、例外をスローするようにオーバーライドすることも可能。
- P.213
- 無名内部クラスを使えば、いちいちダミー用のクラスを作らないでも、テスト時に本来の動きをするクラスを継承し、例外をスローするようにオーバーライドすることも可能。
- 失敗させたままのテスト
- 個人開発では、コードを書くのを中断するときに、失敗するテストを残したままにしておくと戻りやすい
- チーム開発ではちゃんとテストを通るコードにしてコミットする
- チェックイン=コミットの理解
- 結合テストで失敗する場合、「手元の作業を捨ててはじめからやり直し」
ほんとのほんとに「手元の作業」をすべて捨てるのか…?
第28章 グリーンバーのパターン
- 仮実装を経て本実装へ
- 仮実装の有効性
- 心理的効果: バーがグリーンだと安心できる
- スコープ制御: 本質と関係ない問題に気を取られない
- 仮実装は「不要なコード書くべからず」に反しないか?
- リファクタの過程で取り除くから
- 仮実装の有効性
- 三角測量
- 仮実装の状態から、慎重に一般化を行う方法
- 「仮実装はテストコードとプロダクトコードの重複を意図的に発生させて開発を駆動する」
- 「一報三角測量のルールはシンプルだが、無限ループに入る可能性もある」
- 明白な実装
- 明白な実装だけだと、「自分自身が完璧でなければならなくなる」
- うまく行かないときは、仮実装や三角測量を利用して、ギアを下げる必要性
- 一から多へ
- コレクションを扱う操作を実装するときは一要素からはじめて、複数の要素に変更していく
この「一から多へ」は突然現れた気がしますが、この章のそれまでの部分を例で説明しているのだと解釈しました。。。
第29章 xUnitのパターン
- アサーション
- 判断は真偽値で行う
- 真偽値の調査はコンピュータが行う(assert関数)
- パブリックな振る舞いのみでテストを書くべき
- フィクスチャー
- setUpメソッドが使える
- メリット: テスト実行前の共通の処理をひとまとめにできる
- デメリット: テストコードを読み解く時に、setUpの内容を覚えておかなければならない
- setUpメソッドが使える
- 外部フィクスチャー
- フィクスチャーとして作成した外部リソースを解放するとき、tearDownを使う
- テストメソッド
- フィクスチャー: クラス
- テスト: メソッド
- テストメソッドは3行を目指す
- アウトラインを作るとわかりやすい
- /* ダブルスペースへの追加 */ みたいなコメント
- 例外のテスト
- 例外発生を期待するテストは
- 期待される例外をキャッチして握りつぶす
- 期待した例外が発生しなかったときだけテストが失敗するように書く
- fail()メソッド
- 「期待される例外だけをキャッチするようにくれぐれも気をつける」
- 例外発生を期待するテストは
- まとめてテスト
- collection parameterパターンでテストを追加している
今のxUnit系ツールではデフォルトである機能ですね
第30章 デザインパターン
- 「問題自体がいかに多様であり異なる背景を持っていようとも、実は問題は一般的であり、その下位も一般的であると期待できる」
- 「Command: 処理の異実行をただのメッセージではなくオブジェクトで表現する」
- 「Value Object」: 一度作られたら絶対に値が変わらないオブジェクトを作り、別名参照問題を防ぐ」
- 「Null Object:特殊な状況をオブジェクトで表現する」
- 「Template Method:処理の順序を抽象メソッドの並びで表現し、個別の処理は継承によって実現する」
- 「Pluggable Object: 二種類以上の実装を持つオブジェクトを呼び出すことでバリエーションを表現」
- 「Factory Method: コンストラクタではなくメソッドを呼び出してオブジェクトを作成する」
- 「Imposter: 既存プロトコルの新たな実装を作成してバリエーションを生み出す」
- 「Composite:オブジェクトたちの振る舞いの組み合わせを1つのオブジェクトとして表現する」
- 「Collection Paramater: 様々なオブジェクトから処理結果を集めるためのオブジェクトを引数に渡していく」
TDD本の本質とは関係ない気がして、さらっと読んでしまいましたが、今後ちゃんと勉強していきたいところ…
第31章 リファクタリング
- 差異をなくす
- よく似たコードを共通化するには、内容をだんだん近づけていって、完全に一致したらひとつにする
- ループ構造、条件分岐の中身、メソッド、クラス、など、似ているものを共通化できる
- 変更の分離。以下が利用できそう
- オブジェクトの抽出
- メソッドオブジェクト
- データ構造の変更
- 旧新、新旧のどちらのながれでも、変更できる
この部分、ちょっとよくわからない…
- メソッドの抽出
- 込み入った長いメソッドは、一部分を別のメソッドに分離し、そのメソッドを呼び出すようにする
- やりすぎに注意
- 行き詰まったら抽出したメソッドを「メソッドのインライ化」で元のメソッドに戻してみる
- メソッドのインライン化
- 単純なインライン化と違うようで、この部分も少し難しい…
- インターフェイスの抽出
- 名前つけに注意
- IFile(インターフェイス)->Fileは✕
- File(インターフェイス)->DiskFileは○
- 名前つけに注意
- メソッドの移動
- 1つのオブジェクトに対して2つ以上のメッセージ呼び出しが行われていたら、対象のオブジェクトにメソッドを移動することを考える
- 「目を見張るような結果になることが多い」
- メソッドオブジェクト
- 「複数のパラメータやローカル変数を必要とする込み入ったメソッドを表現する」
- 「システムに新しいロジックを導入する準備段階に有用」
- パラメータの追加
- 「まずは新しいパラメータを追加し、古いパラメータを使っていることろを消していき、最後に古いパラメータを消す。」
- メソッドからコンストラクタへのパラメータの移動
- 「あるオブジェクトの複数のメソッドに対して同じパラメータを渡している場合は、予めパラメータを一度だけ渡しておくことで重複を排除」できる
第32章 TDDを身につける
最後に詰め込み過ぎじゃありません?
- 一歩の大きさはどのくらいか
- 小さいのも大きいのもできるようになるべき
- 「リファクタリングを行う際には、ステップをたくさんの小さいステップに分割することを心がける」
- 「自動リファクタリング機能は開発を劇的に加速させる」
- テストしなくてよいものはあるか
- 「不安が退屈に変わるまでテストを書く」
- 答えを提示するなら、テストすべき対象は
- 条件分岐
- ループ
- 操作
- ポリモフィズム
- 上記のうち、自分が書いたものに限る
- 良いテストを見分けることができるか
- テストは設計の悪臭を察知して教えてくれる
- 「前準備に要するコードが長い」
- 前準備が100行以上になるなら、オブジェクトが大きすぎるので、分割するのがよい
- 「前準備コードの重複」
- 共通の前準備コードを配置する場所がすぐにみつからないなら、互いに密に関連し合うオブジェクトが多すぎる
- 「テスト実行時間が長い」
- 10分以内にしたい
- 実行の頻度が低くなるから
- 脆いテスト
- 思わぬタイミングで失敗するテストは、アプリケーションのどこかが意外な形で影響しあっている可能性
- 2つの部分の関係性を断つか統合する
- 思わぬタイミングで失敗するテストは、アプリケーションのどこかが意外な形で影響しあっている可能性
- 「前準備に要するコードが長い」
- テストは設計の悪臭を察知して教えてくれる
- TDDはどのようにフレームワークを導くか
- 「明日のためにコードを書き、今日のために設計しよう」
- 開放閉鎖原則はバリエーションの発生によって満たされる
- 「オブジェクトは利用に対して開かれていて、修正に対して閉じられているべき」
- 「行き着くところ、バリエーションの導入が早くなればなるほど、TDDは事前設計と見分けがつかなくなる」
ここは難しい…
- どのくらいのフィードバックが必要か
- =どのくらいのテストが必要か?
- MTBF(Mean Time Between Failures)平均故障間隔を思い浮かべる
- 動く期間に起こり得なそうなテストは書かないで良さそう
- ペースメーカーの開発をしていて、MTBFが10〜100年ならかなり細かいありえないような条件のテストも行う意味が出てくる
- =どのくらいのテストが必要か?
- どのようなときにテストを消すべきか
- テストの間に重複があったとき、どちらも残しておくべきか?
- 判断基準1: 自信
- テストを消すことで、振る舞いに対する自信が減るなら、消さない
- 判断基準2: コミュニケーション
- テストがコードの同じ部分を実行しているとしても、読み手には異なるシナリオと映るなら、消さない
- どちらの面でも重複があるなら役に立たない方を消す
- 判断基準1: 自信
- テストの間に重複があったとき、どちらも残しておくべきか?
- プログラミング言語や環境はTDDに影響するか
- TDDのサイクルを回しやすい環境だと、色々試したくなってくるし、それは開発効率を上げる
- 確かに自動テストを想定されているIDEだとテスト回したくなる
- TDDのサイクルを回しやすい環境だと、色々試したくなってくるし、それは開発効率を上げる
- 巨大なシステムをテスト駆動できるか
- 「機能の総量はTDDの有効性とは無関係であるように思われた」
- 250,000行のテストコード
- 4,000件のテストケース
- 20分以内に通る
- 1日に何回か全件実行されていた
- 「機能の総量はTDDの有効性とは無関係であるように思われた」
- アプリケーションレベルのテストで開発を駆動できるか
- 小さいテストで開発を駆動する場合、顧客が望んでいたものと、出来上がったものが全く別物だったというリスクがある
- 対策: 顧客にアプリケーションレベルのテストを書いてもらう
- 以下理由から、大変になりがち
- チーム客員の協調が必要
- 顧客にテストを書く責務が加わる
- この本のTDDはひとりでもできる技術
- 以下理由から、大変になりがち
- 途中からTDDに乗り換えるにはどうすればよいか
- 「最大の問題は、テストのことを考えずに書かれたコードは、えてしてテストが書きにくい」
- コードを書き直すのも自動テストがないので、リファクタリングには失敗がまっている
- 全てに対してテストを書いて、システム全体をリファクタリングするのは悪手
- 「新しい機能が増えないにもかかわらず、数ヶ月を費やす」
- 対策: 変更のスコープを狭める
- シンプルに変えられるとわかっていても、変更の予定がないのなら、そのままにしておく
- テストとリファクタリングの間のデッドロックを解消する
- テスト以外の手段でフィードバックを得る
- 例えば、ペアで慎重に作業する
- やがてよく変更される部分はテスト駆動で開発されているように見え始める
- テスト以外の手段でフィードバックを得る
- TDDは誰のためのものか
- エレガントさに癒やしを求める魂の持ち主
- コードに思い入れの深いギークたち
- エンジニアリングが成功に占める割合は20%
- 普通のエンジニアリングでも、残りの80%がしっかりしていれば、成功に導きうる
- この観点から見ると、TDDは「やりすぎ」
- 「TDDは「より良いコードを書けば、よりうまくいく」という素朴で奇妙な仮設によって成り立っている」
- TDDは初期状態に左右されるか
- 左右されない(と言っているように見える)
- チョットヨクワカラナイ
- TDDとパターンの関係
- パターンに従うと、はじめは調べるのに時間がかかるが、後から早くなる
- パターン駆動設計の実装手法としてTDDを利用できる
- なぜTDDは機能するのか
- TDDが欠陥を減らせるから
- 筆者による科学的根拠はない
- が事例証拠はある
- (あと、最近はデータとして証拠もあった気がする)
- TDDが欠陥を減らせるから
- 名前の由来は
- 「TDDは分析技法であり、設計技法であり、実際には開発のすべてのアクティビティを構造化する技法」
- TDDとXPのプラクティスとの関係
- TDDでもちゃんとXPはできるよ
- Darachの挑戦
- Darach EnnisさんはTDDの適用範囲を広げるように挑戦しているとのこと。例えば
- GUIもテストできる
- 分散システムの自動テストできる
- データベーススキーマのテストを先に書ける
- サードパーティのコードやツールで自動生成されてコードもテストする
- テストファーストでプログラミング言語のコンパイラやインタプリタを使えるレベルまで育てる
- Darach EnnisさんはTDDの適用範囲を広げるように挑戦しているとのこと。例えば
筆者の疑問のまま終わる箇所もあったので、少々抽象的ですが、
言いたいことはなんとなくわかった気がします
コメント
コメント失礼します。第31章の「データ構造の変更」は多分、インスタンス変数として持っている配列をリスト構造に書き換えたい場合のものだと思います。
private int[] numsをprivate LinkedList numsに書き換えたいといった感じだと解釈しています。
コメントありがとうございます!
自分でも読み直してみました。
> 第31章の「データ構造の変更」
256ページの「理由」の部分かと思いますが、
以下のようにデータ構造を変えることで、複数のテストを扱えるようにしたのだと理解しました。
> private int[] numsをprivate LinkedList numsに書き換えたいといった感じ
Javaについて浅学で申し訳ないのですが、どちらかというと、以下のような変化かと解釈しました。
何か誤解していたら教えていただければ幸いです。m(__)m
私もまだ完璧に理解したとは言えませんが、31章については「データ構造とアルゴリズム」にあるデータ構造を変える場合のものだと解釈しています。
配列はランダムアクセス( 例: nums[2] )は容易ですが末尾に追加するとなると結構面倒です。リスト構造はそれの逆でランダムアクセスはしづらいですが先端や末尾に追加する場合はそのままつなげればいいというデータ構造です。(ご存じかもしれませんが)
そのデータ構造を変える場合のパターンだと解釈しました。
@neko_machi様の「軽い気持ちでLinkedListを使ったら休出する羽目になった話」(Qiita)のようなシチュエーションを想定しているのだろうと捉えています。
testからtestsに変える(複数のテストをほぼ同時に行う)のは恐らく「23章 スイートにまとめる」の方で、テスト対象を単数から複数にする場合は「28章 グリーンバーのパターン」にある「一から多へ」に該当すると思われます。
従って、「データ構造とアルゴリズムでいう配列構造からリスト構造へ、スタック構造からキュー構造へとデータ構造を変更する場合」という解釈に至りました。
…と考えていたのですが読み返してみると確かに「一から多へ(p.222)」の意味も含んでいますね。
ただ、p.257に「例えばJavaのVectorやEnumerationからCollectionやIteratorに移行するときにも使える」ともあるのでwataro様の「一から多へ」を含んだ解釈とデータ構造とアルゴリズムで学ぶスタック構造をキュー構造に移行するといったシチュエーションでも利用できるようです。
なんか読み飛ばしていたようです…汗