0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

TDDが設計を導く — 検証ではなく設計の手段

0
Posted at

はじめに

TDD(Test-Driven Development、テスト駆動開発)という言葉は、
多くの人が一度は耳にしたことがあると思います。
「テストを先に書くやつでしょ」と説明できる人も多いはずです。

ただ、実際にやってみると「順番を入れ替えただけで、いったい何が嬉しいんだろう?」という不思議な感覚に襲われます。
自分も最初はそうで、Red-Green-Refactorは知っているけれど、Refactorって結局「お掃除」なのでは、と思っていました。

この記事では、TDDの本当の価値が「テストを先に書くこと」そのものではなく、
別のところにあるという話をします。

TL;DR

  • TDDの本質は「テストを先に書く」ことではなく、「設計を発見する」ことにあります
  • Red-Green-Refactorの3段階は、それぞれ違う種類の設計判断を引き出す装置です
  • 「テストが書きにくい」は「設計が悪い」のシグナルとして読み取れます

1. 「テストファースト」という誤解

1.1 「テストを先に書く」だけでは何も変わらない

ちょっとした思考実験から始めます。
テストを先に並べるとしましょう
——自分で書くにせよ、AIに「先にテスト書いて」と頼むにせよ。
ただし、設計について何も考えず、いつもと同じように実装の都合を頭に浮かべながら並べるとしたら、どうなるでしょうか。

おそらく、書く順番が逆になっただけで、できあがるコードはほとんど変わりません。
実際、TDDの効果に関する学術研究はずっと結果が分かれてきました。

仮説 効いていそうなもの
テストを先に書くこと自体が効く 順番の入れ替え
TDDのリズムが効く 小さな単位で進める/こまめに振り返る

多くの研究が示唆するのは後者です。順番の入れ替えそのものではなく、「小さく進めて振り返る」リズムに価値がありそうだ、という見方です。

では、TDDの本質は何でしょうか。

1.2 視点の転換: テストは「検証の道具」ではなく「設計の道具」

ここで視点を切り替えてみます。

テストを「書いたコードが正しいか確かめるもの」と捉えると、テストは検証の道具です。
一方、テストを書く行為そのものが「コードはどうあるべきか」を決める判断を強制している、と捉えると、テストは設計の道具になります。

この違いを表で整理してみます。

観点 一般的な「テストファースト」の解釈 TDDで実際に起きていること
テストの役割 書いたコードの正しさを検証する コードが「どうあるべきか」を決める
「先に書く」が意味すること 書く順番が前 設計判断を実装より先に行う
効いているもの 順番の入れ替え 設計判断と実装判断の分離

順番が前か後かではなく、「設計判断を実装より先に行えているか」が効いているポイントです。
この視点を持てると、Red-Green-Refactorの各段階の意味も変わって見えてきます。

2. テストは「最初のクライアント」になる

では、「テストが設計の道具」とは具体的にどういうことでしょうか。少し丁寧に解きほぐしていきます。

2.1 コードを「使う側」から見るとき、設計が見える

ここで一つ用語を定義します。
プロダクションコード(テストではなく本番で動く実装コードのこと)を最初に呼び出すのはテストコードです。
本記事では、これを 「最初のクライアント」 と呼ぶことにします。

プロダクションコードを書くとき、人は「どう実装するか」に意識が向きます。内部のデータ構造、ループの書き方、例外の処理。どれも実装者の視点です。

ところが、実装する前に「どう使われるか」を考えると、別の視点が立ち上がります。
テストコードを先に書くとは、そのプロダクションコードの最初の使い手(クライアント)になるということです。

呼び出される側ではなく、呼び出す側からコードを眺める。この視点の差が、設計の見え方を変えます。

2.2 具体例: 設計が決まっていない関数を、テストから書き始める

抽象論だけだと分かりにくいので、簡単な例で見てみます。

お題: 商品の合計金額に対して、会員ランクに応じた割引を計算する処理

実装から書き始めると、こうなりがちです。「とりあえず関数を作って…引数は何だっけ…内部に状態を持つ?持たない?…」と、最初の一行を書く前に詰まります。

一方、テストから書き始めると視点が変わります。

# まずテストから書く(プロダクションコードはまだ存在しない)
from decimal import Decimal

def test_gold_member_gets_10_percent_off():
    discounted = calculate_discount(total=Decimal("1000"), member_rank="gold")
    assert discounted == Decimal("900")

このテストを書いた瞬間、API(メソッドの引数・戻り値の形)の全体像が使い手側から浮かび上がっています。
関数名は calculate_discount、引数は合計金額と会員ランク、戻り値は割引後の金額。
これは検証ではなく、設計の宣言です。

API形状の細部、たとえば「不正な会員ランクをどうするか」「副作用は持たせるか」といった判断は、次の章で改めて掘り下げます。

2.3 「最初のクライアント」が設計に効く理由

なぜ、最初のクライアントになることが設計に効くのでしょうか。
理由はシンプルで、コードを最初に「使う側」から眺めることで、使いにくさを早期に発見できるからです。誰がそのコードを書いたかには依存しません。

テストを書く順番 立ち上がる視点 発見できるもの
先に書く(最初のクライアント) 使い手の視点 引数の多さ・戻り値の扱いにくさ・依存の見えづらさ
後から書く 既存実装に合わせる視点 「いまある実装をどうテストするか」(設計の違和感は埋もれる)

後からテストを書こうとすると、すでに書いてしまったコードに合わせるしかなくなります。使い手の視点は失われ、設計の違和感は埋もれてしまいます。

3. Red-Green-Refactor で何を発見しているのか

第2章では「テストが設計の道具になる原理」を見ました。
本章ではそれをRed-Green-Refactorの3段階に分けて、各段階で引き出される設計判断を言語化します。
本章で見ていくのは次の問いです。

  • Red・Green・Refactor は、それぞれ何を判断する場面か
  • なぜ「3段階」に分かれているのか

3.1 Red-Green-Refactorとは

まず3段階を簡単に確認しておきます。

  1. Red: まだ存在しない(または未完成な)機能に対して、失敗するテストを書く
  2. Green: そのテストが通る最小限のプロダクションコードを書く
  3. Refactor: テストが通った状態を保ちながら、コードの構造を整える

これを小さな機能単位で繰り返すのが、TDDのリズムです。

ここからが本題です。3段階それぞれが、何を判断しているのかを並べてみます。

矢印に注釈として書いた通り、各段階は 異なる種類の設計判断 を引き出します。順に見ていきます。

3.2 Red段階で発生する設計判断: 境界・例外・前提の決定

第2章ではRedによってAPI形状の全体像が引き出されることを見ました。ここではさらに踏み込んで、Redでしか引き出せない粒度の設計判断に焦点を絞ります。

Red段階でテストを書く時点で、正常系以外のケースをどう扱うかを必ず判断する必要があります。具体的には次のような判断です。

  • 境界: 空リスト・null・0など、どこまでを正常入力として扱うか
  • エラー表現: 例外を投げるか、戻り値(「値が無いかもしれない」や「成功か失敗か」を型で表す仕組み)で表現するか
  • 副作用の有無: 戻り値だけで完結するか、状態を変更するか
  • 呼び出し前提: 呼び出し側に何を要求するか(事前条件)

別の例として、カート内の商品の合計金額を計算する関数を考えます。

# Red段階: まだ実装はない。境界・例外の方針をテストで宣言する
def test_empty_cart_returns_zero():
    assert calculate_total([]) == Decimal("0")

このテストを書いた時点で、「空のリストは例外を投げず0を返す」「副作用なし、戻り値のみ」という設計方針が宣言されています。仮に ValueError を投げる前提でテストを書いていたら、まったく別のAPIになっていたはずです。

こうした選択を意識的に行えるのがRed段階の価値で、実装を書き始めた後だと既存実装の都合に引きずられて素直にできなくなります。

3.3 Green段階で発生する設計判断: 最小実装の選択

なぜ、いきなり完璧な実装を書かないのでしょうか。最小実装には独立した価値があります。

Greenでは「テストを通す最小限のコード」を書きます。極端な場合、定数を返すだけでもよいのです。

# Green段階: まずはテストを通すだけのコード
def calculate_total(items):
    return Decimal("0")

「これでいいの?」と感じます。よいのです。次のテスト(たとえば「商品が1つだけ入っているカート」)を書いたとき、このコードは自然と拡張せざるを得なくなります。

最小実装に留めることで、先回りした抽象化——「将来必要かもしれない」という推測で導入する余計な構造——を防ぐ姿勢が自然と身に付きます。
Green段階での判断は「いま本当に必要な実装はどこまでか」「次のテストは何を引き出すべきか」です。
実装の最小性そのものが、設計判断の対象になっています。

3.4 Refactor段階で発生する設計判断: 責務と構造の整理

なぜ3段階目が必要なのでしょうか。動くコードと、構造的に整ったコードは別物だからです。

Refactorでは「テストを壊さずに構造を改善する」ことを行います。

# Refactor段階: 構造を整える(テストはすべて通り続ける)
def calculate_total(items):
    return sum((item.price * item.quantity for item in items), Decimal("0"))

「動くだけのコード」から「読める・変更しやすいコード」へ。重複の除去、責務(クラスや関数が担当する役割の範囲)の整理、抽象化の導入、命名の改善——Refactor段階で扱うのは、こうした 構造についての判断 です。

ここで重要なのは、テストがあるからこそ構造を変える勇気を持てるという点です。テストがなければ「動いているものを壊したくない」という心理的ブレーキで構造の改善は止まり、Refactor段階自体が成立しません。

3段階を一度俯瞰してみる

ここまで3段階を見てきました。引き出される設計判断を整理すると、それぞれが まったく異なる種類 であることが分かります。

段階 自分の立場 引き出される設計判断
Red 使い手 境界・エラー表現・副作用の有無・呼び出し前提
Green 実装者 いま必要な最小限の実装範囲
Refactor 整理者 責務の境界・重複・命名・抽象化

TDDが「3段階のサイクル」になっているのは、偶然ではありません。
それぞれが違う種類の設計判断を要求するから、3つに分かれているのです。
これが、TDDが「設計を発見する」と言われることの中身です。

4. テストが書きにくいときに何を聴くか

実際にTDDをやってみると、必ず「テストが書きにくい」場面に遭遇します。これをどう解釈するかが、TDDを設計手段として使えるかどうかの分かれ目になります。

4.1 「テストが書きにくい」は設計のシグナル

実装したコードに対してテストが書きにくいとき、選べる道は2つあります。

  1. 我慢してなんとかテストを書く
  2. 「コードの設計が悪い」というシグナルとして受け取り、コード側を直す

設計を発見する活動としてのTDDでは、後者を選びます。
これは 「テストの声を聴く」 とも呼ばれる考え方です。
テストを書こうとして発生する違和感が、コードの設計について何かを伝えようとしている、という捉え方です。

4.2 症状と設計問題の対応

実装したコードに対してテストを書こうとしたとき、次のような違和感に出会うことがあります。それぞれ、コードのどんな問題を示唆しているかを並べてみます。

なお、ここでは設計の評価軸として 疎結合高凝集 の2つを使います。
疎結合はモジュール同士の依存関係が少ない状態、高凝集は関連する処理が一つのモジュールにまとまっている状態を指します。

テストで起きている症状 示唆される設計問題
1つのテストで多くの前提条件を作る必要がある 対象が多くの依存を持ちすぎている(疎結合が崩れている)
検証したい挙動を取り出すのに、対象の内部状態を覗く必要がある 結果を返すのではなく、状態を持ちすぎる設計になっている
同じセットアップを使い回す巨大なテストクラスができる クラスの責務が広がりすぎている(高凝集が崩れている)
1つの機能を変えただけで多数のテストが壊れる 結合が強い(変更の影響範囲が広すぎる)

どの症状も、テスト側の問題ではなく、対象コードの構造から来ています。

4.3 直すべきはテストではなく、コードの方

ここで「テストが面倒だから、無理やりテスト側でなんとかする」を選ぶと、設計の改善機会を失います。

選び方 何が起きるか
テスト側で吸収する(巨大セットアップ等) 症状は抑え込めるが、設計問題は残る
コード側を直す 設計が改善され、テストも自然に書けるようになる

TDDが設計を導くのは、この「書きにくさ」をシグナルとして扱う姿勢があるからです。
逆に言えば、書きにくさを無視してテストだけ書いても、TDDの効果は得られません。
書きにくさは敵ではなく、設計の発見への入り口です。

5. それでもTDDを使わない場面

ここまでTDDの価値を語ってきましたが、TDDが万能だという論調にはしません。現実的な使い分けを最後に置いておきます。

5.1 TDDが効く場面と効きにくい場面

TDDが効きやすい場面 TDDが効きにくい場面
設計が見えていない問題に取り組むとき 解き方が完全に分かっており、ただ書くだけのとき
ビジネスロジックが複雑で、振る舞いを段階的に詰めたいとき UI寄りの試行錯誤・プロトタイピング段階
後で安心してリファクタリングしたいとき 一度きりの調査スクリプト・捨てる前提のコード

5.2 設計を発見する必要がない場面では使わなくてよい

TDDの価値は「設計を発見する」ことにあります。
発見すべき設計がない場面では、効果は薄くなります。
実装パターンが手に馴染んでいる定型処理、たとえばDBから値を取ってきて整形して返すだけの薄いハンドラなどでは、後でテストを書くほうが早いことも多いです。

重要なのは「テストを書かない」ではなく、 「設計の発見が要るかどうかで、テストを書く順番を選ぶ」 という姿勢です。
判断軸を整理すると次のようになります。

  • この実装で、まだ設計を発見しようとしているか? → YES なら先にテストを書く
  • 設計はもう見えていて、ただ書くだけか? → 後でテストを書くほうが早いこともある
  • 一度きりの調査スクリプトか? → そもそもテスト不要のことも多い

おわりに

TDDが伝えようとしているのは、「テストを先に書け」というルールではなく、「コードを書く前に、コードがどうあるべきかを判断せよ」という設計の姿勢です。
Red-Green-Refactorのサイクルは、その設計判断を3段階に分割するための装置でした。

「テストが書きにくいな」と感じた瞬間こそ、コードの設計について何かを発見しようとしている瞬間です。
その違和感を素直に聴くことから、設計を導くTDDが始まります。

0
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?