【レビュー】テスト駆動開発

どうも、oskn259です。

今更読了シリーズその2はテスト駆動開発です。
(その1はこちら: https://blog.oskn259.com/article/review_agile_samurai)

ツイッタ上でもよく話題になる、あのテストの方が翻訳した本ですね。

テスト駆動開発を採用した場合の細かい手続きの解説は本に譲るとして、
よくある誤解、僕自信が誤解していたことや、すぐに使えると思った知識に絞って解説していきます。

テスト駆動開発の進め方

名前だけは聞いたことある開発法として僕自信も知ってはいましたが、
その時の僕の認識としてはこうでした。

  • テストを始めに全て書く
  • それをパスするようなコードを以って完成とする
  • 仕様をテストという形で記載するので、開発者感での認識の齟齬を防止できる

実はこの認識こそ、

TDDのごく一側面をバズりやすいように切り取った代物


だったのです。

こういう体験をするたび人類の愚かさを感じてしまう…

開発の進め方

フィボナッチ数列を生成するメソッドを作ってみましょう。

まずは、これぐらいなら即作れるやろというレベルのテストを書きます。

1
2
3
4
5
describe('fib()', () => {
it('フィボナッチ数を返す', async () => {
expect(fib(0)).toBe(0);
});
});

0を入力したら、そのフィボナッチ数である0が返るというテストです。
これを満たす実装は以下の通りです。

1
2
3
function fib(n: number) {
return 0;
}

はい、簡単ですね。


そんなわけねぇだろぇ!!!

いや、TDDではこれで良いんです。
常にテスト結果をグリーンに保ち、その後、すぐにグリーンにできる一歩を踏み出すのです。
はじめの一歩としてfib(0) === 0を完成させたわけです。

では次なる一歩としてfib(1) === 1となるテストを追加しましょう。

1
2
3
4
5
6
describe('fib()', () => {
it('フィボナッチ数を返す', async () => {
expect(fib(0)).toBe(0);
expect(fib(1)).toBe(1);
});
});

追加した部分のテストが落ちてしまうようです。
実装を修正しましょう。

1
2
3
4
function fib(n: number) {
if (n === 0) return 0;
return 1;
}



・・・

すいません、でもTDDではこれで良いんです。
常にテスト結果をグリーンに保ち、その後、すぐにグリーンにできる一歩を踏み出すのです。
同じように進めていきます。

1
2
3
4
5
6
7
8
describe('fib()', () => {
it('フィボナッチ数を返す', async () => {
expect(fib(0)).toBe(0);
expect(fib(1)).toBe(1);
expect(fib(2)).toBe(1);
expect(fib(3)).toBe(2);
});
});
1
2
3
4
5
function fib(n: number) {
if (n === 0) return 0;
if (n <= 2) return 1;
return 2;
}

次なる一歩として、fib(2), fib(3)を追加しました。

テストが揃ってきたので、ここでリファクタを行います。
TDDにおけるリファクタとは、

重複を排除する

ことです。
このケースにおける重複とはなんでしょうか?

例えば、テスト上の2という数字実装での2という数字が重複しています。
このように、実装とテスト両方にわたって観察し、重複を探し出します。
では、重複を解消するにはどうするのでしょうか?

まず、実装上のreturn 2の2とは何でしょうか?
これはフィボナッチ数を計算する機能ですから、
この2はそれ以前のフィボナッチ数である11を加算したものとなります。

1
2
3
4
function fib(n: number) {
if (n === 0) return 0;
if (n <= 2) return 1;
return 1 + 1;

このときreturn 1 + 1の前者の1は、
expect(fib(1)).toBe(1);としてテスト上に記載されているfib(1)の結果をベタ書きしたものです。
後者の2についてはfib(2)の結果です。
つまり、これらの1は重複しているのです!
重複しない形に書き直しましょう。

1
2
3
4
function fib(n: number) {
if (n === 0) return 0;
if (n <= 2) return 1;
return fib(n-2) + fib(n-1);

より小さなステップでTDDを進めたければ、
return fib(2) - fib(1);
を挟んでも良いですが、ここでは少し大股に歩を進めました。

このようにして、フィボナッチ数を計算する機能がテストと共に完成します。

これまでの認識との差

先のフィボナッチ数のケースが示す通り、
TDDにおいてテストと実装はどちらが先ということはなく、両輪で進めていくものです。

これは僕が知っている 「テストファースト」 の概念を大きく修正してくれました。
要件全てをテストとして表現してそれをパスするコードを模索するというのは、
(やり方としてアリだとは思いますが)それはTDDではないようです。
最初の一歩を踏み出すために小さなテストを書くことこそがテストファーストなのです。

またこれまで、完成したコードをより良いものにしていくという意味でリファクタという言葉を使っていましたが、
TDDの文脈においてリファクタとは、重複を排除する開発過程のことを指しています。

言うなれば、TDDは単なる制約や手続きではなく

思考のプロセス

と言った方が近そうです。

こうした思考プロセスがなぜ有効なのかは分かりませんし、
本書でも明確にはされていませんが、TDDのサイクルを回すことで優れた設計に辿り着いたという例は多いようです。

得た知識

振る舞い駆動開発(BDD) と テスト駆動開発(TDD)

そもそもテストとは、エラーになるであろう操作を意図的に行ってそうしたケースの存在を示すことですが、
TDDにおけるテストでは現状の立ち位置を確認する役割と言えそうです。
それは本当にテストか?
間違った理解を世に広めていないか?
という議論があり用語が一新されました。

その結果生まれたのがBDDで、assertion -> expect
test class -> specのように、振る舞いを定義するという前提の単語が使用されるようになっています。
機能として両者は等しく、用語のみが入れ替わっています。

TDDなのでテストは完璧、ではない

リリースのために必要なテストは、 以下の軸に沿って4象限に分類できます。

  • 技術面 <-> ビジネス面
  • チームの支援 <-> 製品の批評

TDDで記述するテストは、技術面でチームを支援するものと言えそうです。
要するに、まだ3種類のテストが残っているのです。
具体的には、以下のようなテストが残されています。

  • セキュリティ検査
  • 負荷検証
  • 顧客による手動受け入れテスト
  • 自動化されたストーリーテスト

二層構造のテスト

フィボナッチ数の例で記述したようなユニットテストだけでなく、
受け入れ条件のような大きい単位に対してもTDDは可能であるとされています。
アジャイルにおけるユーザーストーリーをテストとして記述するのです。

この場合、ユーザーストーリーを表す 「外側」のテスト と、
コーディング時に並行して記述する 「内側」のテスト の2つが存在することになります。
外側テストをパスするために、内側テストと実装の両輪でコードを書き進めるというイメージですね。

この「外側」テスト向けフレームワークとして、cucumberなどが挙げられます。
https://cucumber.io/


fig.1 キューカンバ・ドラゴン

熟練が必要

今となっては随分浸透したTDDですが、
今回本を読んでみて、簡単に習得できるものではないなという感想でした。
感覚や経験の占める割合が多いと思います。
フィボナッチ数の例にしたって、

  • 一歩の大きさはどれぐらいが適切か?
  • そもそもその一歩として何を選ぶべきか?
  • どのタイミングでリファクタを始めるのか?

ということについて決まった方法がなく、
コーダーの経験に委ねられているのです。
この感覚を掴んでTDDの恩恵を真に受けるためには、いくらか経験が必要そうです。

まとめ

本書にも記載がありましたが、重要なのは自分やPJにとっての最適解を見つけることです。
TDDでないからクソプロジェクトだ! ではありませんし
足し算のメソッドに100個のテスト を書くのが適切とも思えません。

何の説明をするにしてもですし、先のフィボナッチ数の例もそうですが、
TDDの説明にあたりそれが有効な例を引っ張ってきています。
要するにストローマン論法です。
これは説明を分かりやすくするために必要ですが、
いつでもどこでもそうなると言っているわけではありません。


fig.2 ジャパニーズ・トラディショナル・ストローマン

みんなが言ってるからとか有名コテ(古いか)が言ってるからではなく、
現状に最適な使い方を模索しましょう。
その結果、TDDは今回は不要とかも全然あり得るでしょうが、
一つのスキルとして持っておくことは重要です。