テスト駆動開発を読む
「テスト駆動開発」を読みます
t_wada さんの講演を見て概要を把握し、そのうえで見様見真似で業務でやったことはあります。
しかし、ちゃんと本著を読んだことはなかったためきちんと入門します。
はじめに
TDD が目指すのは「動作するきれいなコード」を書くこと。
動作するきれいなコードは正確であり、読みやすく、メンテナンスしやすい。
動作するきれいなコードを最初から書くのは難しい。天才であれば行えるが。
TDD はこの動作するきれいなコードを誰でも書けるようにする方法論の一つである。
第一部
具体例として多国通貨を題材に TDD の流れを説明している。
TDD の流れ
- (Red)テストを始めに書いて、失敗させる
- (Green)テストが通るようにプロダクトコードロジックを書く
- (Refactoring)テストが通る状態を維持したままプロダクトコードとテストコードをリファクタリングする
ここで、最初のフェーズとして「プロダクトコードで実現したい仕様を箇条書きで表現する」がはじめに存在する。
最初に仕様を固めて、やることを明確にすることによって、あとから仕様が漏れてしまっていた...をなくす。
先にボトルネックとなる仕様をはっきりさせたうえでテストコードの実装にとりかかる。
ここで、すべての仕様をテストコードとして表現しなくていい。仕様をあとからも追加してもいい。
これは実装している段階で少しずつ増やしていく。
大事なのは「仕様を作る→Red→Green→Refactoring」のサイクルを小さく回して動作するきれいなコードを実装することになる。
仮実装
テストを書いたあと、とりあえずテストコードを通すためにプロダクトコードを一時しのぎで実装すること。
三角測量
仮実装を行ったとき、プロダクトコードに定数が埋め込まれたり null が埋め込まれたりする。
このような定数がそのまま出てきてしまうことを防ぐために、テストケースに異なる値を入力値として取るようにする。
これを三角測量とよぶ。
これによって、プロダクトコードに正しい実装をするように矯正でき、仮実装をなくせる。
明白な実装
三角測量を通じて、仮実装のコードを正しいコードに置き換えること。
リファクタリングをする過程で明白な実装に寄せていく。
疑問を語るテスト
テストコードを書いているときに、プロダクトコードの挙動について疑問を持つことがある。
(たとえば、 php のクラスで継承を利用したときに、子クラスからのメソッドを呼び出したら親クラスのメソッドも呼び出されるのか?など)
この疑問を説明するテストケースを実装するのがよい。
それによって、あとから他の人がそのテストケースを見たときに、この疑問を解消できる。
このようにテストケースを利用することで、プロダクトコードの意図を説明し、疑問を解消できる。
プロダクトコードはあくまで抽象的な実装ロジックであり、具体的な利用方法がテストコードに概する。
学習用テスト
はじめて利用するライブラリをプロダクトコードに組み込むことがある。
このライブラリの細かい挙動・使い方について理解するために、テストコードを実装してその疑問を解消することができる。
これを学習用テストとよぶ。
他のプロダクトコードテストと区別するために @group learning
のようにアノテーションをつけるとよい。
回帰テスト
本番環境のプロダクトコードをリリースした結果、バグが検知されて障害になってしまうことがある。
ここで、そのバグを修正するためにプロダクトコードを改善することになる。
ここで、プロダクトコードをいじる前に、回帰テスト
として新しいテストケースを追加するべきである。
これは、本番環境に出てしまったバグが存在するということは、本来テストすべきだったプロダクトコードのエッジケースのテストコード実装が漏れてしまっていたためである。
回帰テストのテストケースを実装して、まずは今のプロダクトコードがエラーを返すことを確認する。
そのうえで、プロダクトコードを変更して、これまでエラーだった回帰テストコードが通るようになることを確認する。
第二部
シンプルな xUnit テストフレームワークを TDD で開発することで、 TDD の流れをおさらいしている。
実際に手を動かして作ってみるのがよさそう。
第三部
第三部では、テスト駆動開発のパターン集を説明している。
第一部、第二部は 特定のサンプルを通じて TDD を実際に行っていたが、第三部はそれらで利用したプラクティスや経緯を説明している章になっている。
この Zenn Scrap ではそれらのうち、なるほど〜と思ったものだけ記載する。
TODO リストとテストファースト
TDD では、必ず先に テスト
を実装しレッド状態にする。そのうえでプロダクトコードを仮実装もしくは明白な実装を行ってグリーン状態にする。
このように、 TDD はテストファースト・アサートファーストを徹底することによって、利用者側からのふるまいを通じて綺麗な設計とコードを書くことを目指す。
TDD では何かしらの具体例のテストコードを通じて抽象的なプロダクトコードをテストする。これによって、利用者側からの API を必然的に意識することになり、名称や入出力の値、どこまでを public にするか?デザインパターンをどうするか?という意識が芽生える。
ここで、やみくもにテストを書いていくと、プロダクトコードやテストコードで何を書いているのか?なんの目的でテストをしているのか?がわからなくなっていくる。
そのため、先に TODOリスト
を用意して、簡単なプロダクトコードを書いて通すようにする。
その後なにか迷いが発生したり、新しい仕様の存在に気づいたら TODO リストを修正して、インクリメンタルにテストコードとプロダクトコードを書いていく。
良いテストの見分け方
前提として、良いテスト = 良いプロダクトコードの設計に紐づくものである。
良いテストが書けている場合、読みやすく重複が少なく可読性が高い。そのような場合のプロダクトコードはきちんとリファクタリングがされており、設計も十分に行われている可能性が高い。
以下の状態が発生している場合、テストや設計に問題があるので修正したほうがよい。
- 前準備のコードが長い
- 100 行を超えだしている場合、プロダクトコードのクラス分割や、テストコードの前準備の fixture によりリファクタなどを行った方が良い
- 脆いテスト
- 思わぬタイミングで失敗したり、他のコード修正でテストが通らなくなったりする場合、依存が深い状態なので分離させるなどの改善を行った方が良い
TDD の現在
モックオブジェクトによるダブルループ
ロンドン学派 = モック主義 TDD (Mockist TDD)
によって、依存する他クラスを積極的に Mock/Stub Object を利用するようになった。
この思想を先導した GOOS本
では以下の考え方を世に示した。
- ソフトウェア開発には半径の大きさが異なる 2 つのループが存在する。外側のループは外部の質を求めて、内側のループは内部の質を求める。
- モックオブジェクトを活用することで、外部のループを先に作れる。
- オブジェクト同士のやりとりのインタラクションをテストできるようになった。
モックオブジェクトを利用することで、外側からのオブジェクト同士のやりとりをテストできるようになった。これによって、まずは使い方を定義し、そのうえで内側の細かな実装を詰められるようになった。
ユーザエンドレベルの要件をまずは外側として定義し、このテストを短期的に通すことを目指すのが受け入れテスト(Acceptance Test Driven Development ATDD)と呼ばれる。
ATDD は TDD の一部であり、プロダクトの仕様と要件に寄り添ったテスト設計駆動開発になっている。
TDD はスキルであること
TDD は才能ではなく、スキルであり、後発的なものである。
そのため、開発時に TDD を利用することで、個人のプログラミングテクニックとして身につけられる。
どんどん TDD を利用して量をこなし、質を上げて TDD を理解する。
こうやって自分のなかで身につけたうえで、どこまで TDD を利用するのか、 BDD, ATDD まで意識するのかは各個人に任せられる。
ソフトウェアエンジニアとしてどこまで自身のスキルを使うのか、どこに専念するかは各個人で判断するべきである。
第一部と第二部については現時点では写経せず、読み物としてのみ留めておく。
実務上で TDD を取り入れることで、より規模の大きい環境で TDD を試して身につけていく。
TDD はスキルなのだ〜うぉ〜身につけるぞ〜