1.3 テスト駆動開発(TDD)の概念

テスト駆動開発(TDD: Test-Driven Development)とは、コードを書く前に、そのコードが満たすべきテストを先に書く開発の進め方である。「動くものを作ってから、後でテストする」という普通の順番を、あえて逆にする。本ページでは、なぜわざわざ順番を逆にするのか、その逆転が何を生むのかを見ていく。

なぜテストを「先に」書くのか

テストを先に書く最大の理由は、コードを書き始める前に「完成の定義」を確定させるためである。後でテストを書く場合、テストは「すでに書いたコードに合わせる」ものになりがちだが、先に書けば、テストは「これから書くコードへの注文書」になる。

ここでいうテストとは、「ある入力を与えたら、こういう結果が返るべき」という期待を、機械が自動で確認できる形にしたコードである。たとえば「2 と 3 を足す関数に 2, 3 を渡したら 5 が返る」と書いておく。

後でテストを書くと、何が起きるか。すでに動いているコードを目の前にすると、人は「今ある実装で通る範囲」を無意識にテストしてしまう。バグがあっても、その挙動をそのまま「正しい」と書いてしまいやすい。これではテストが品質の番人にならない。

先に書けば、立場が逆になる。まだコードは存在しないので、テストは「あるべき正しい振る舞い」だけを根拠に書ける。実装の都合に引きずられない。さらに、テストを書く過程で「この関数は何を入力に取り、何を返すのか」を先に決めることになる。これは設計を先に固める行為でもある。TDD が「テスト手法」であると同時に「設計手法」と呼ばれるのは、このためだ。

Red-Green-Refactor のサイクル

TDD は、Red → Green → Refactor という3ステップを小さく繰り返す。先に失敗するテストを書き(Red)、それを通す最小限のコードを書き(Green)、動いたまま中身を整える(Refactor)。この順番自体が、品質と変更しやすさを生む仕組みになっている。

flowchart LR
    R["Red
失敗するテストを書く"] --> G["Green
テストを通す最小の実装"] G --> F["Refactor
動いたまま中身を整える"] F --> R
  • Red(赤) — まず、まだ存在しない機能に対してテストを書く。当然テストは失敗する。多くのツールが失敗を赤色で示すので Red と呼ぶ。この一見ムダな失敗には意味がある。「テスト自体がちゃんと失敗を検知できる」ことを確認できるからだ。最初から成功するテストは、何も確かめていない壊れたテストかもしれない。

  • Green(緑) — 次に、そのテストを通すための「最小限の」コードを書く。ここで欲を出して機能を盛り込まない。とにかくテストが通る(緑になる)ことだけを目指す。最小限に留めるのは、テストで守られていないコードを増やさないためである。

  • Refactor(リファクタ) — テストが通った状態を保ったまま、コードの中身を整理する。リファクタとは、外から見た振る舞いを変えずに内部構造だけ改善することだ。重複を消す、名前を分かりやすくする、といった掃除をする。ここで安心して掃除できるのは、直後にテストを走らせれば「振る舞いを壊していないか」がすぐ分かるからである。

この3つを大きな機能で一気にやるのではなく、ごく小さな単位で何度も回すのが要点だ。常に「直前まで全部通っていた」状態を保てるので、もし失敗したら原因は「たった今書いた数行」に絞り込める。これがデバッグ(不具合の原因探し)を劇的に楽にする。

簡単な関数でサイクルを回す

題材として、「機器名のリストから、指定した接頭辞で始まるものだけを返す関数」を作るとする。TDD なら、まず関数の中身ではなくテストから書く。

# Red: まだ存在しない関数 filter_by_prefix への注文書を書く
def test_filter_by_prefix():
    devices = ["router1", "switch1", "router2"]
    assert filter_by_prefix(devices, "router") == ["router1", "router2"]

assert は「次の条件が成り立たなければ失敗とみなす」という命令だ。この時点で filter_by_prefix は存在しないので、テストは失敗する(Red)。失敗を確認できたら、それを通す最小の実装を書く。

# Green: テストが通るだけの最小実装
def filter_by_prefix(devices, prefix):
    result = []
    for d in devices:
        if d.startswith(prefix):
            result.append(d)
    return result

テストを走らせて緑になったら、振る舞いを保ったまま整理する。

# Refactor: 内包表記で簡潔に。テストは通ったまま
def filter_by_prefix(devices, prefix):
    return [d for d in devices if d.startswith(prefix)]

整理の前後でテストが通り続けていれば、書き換えが安全だったと分かる。もし接頭辞が空文字のときの扱いなど、新たな条件が気になったら、それも先にテスト(Red)として書き足し、同じサイクルを回す。仕様が増えるたびにテストが積み上がり、それが将来の変更を守る安全網になっていく。

なぜネットワーク自動化で TDD が効くのか

ネットワーク自動化では、コードの誤りが「設定ミスによる通信断」に直結する。だからこそ「変更しても壊れていないと自動で確認できる」TDD の考え方が強く効く。

自動化スクリプトは、機器の設定を生成したり、API を呼んで状態を変えたりする。手で1台ずつ確認していた頃と違い、1つのスクリプトが何十台もの機器に同じ変更を一斉に適用する。便利な反面、誤りも一斉に広がる。「設定を生成する関数が、入力に対して正しい設定文字列を返すか」をテストで先に固めておけば、誤った設定が機器に届く前に検知できる。

さらに自動化のコードは、機器の追加や仕様変更で頻繁に手が入る。1.3 で見たサイクルの強みがここで活きる。テストという安全網があれば、「触ると何が壊れるか分からない」という恐れなしに変更できる。変更しやすさは、現実のネットワークが変わり続ける以上、自動化にとって品質そのものと言ってよい。

Note

TDD は「テストを書けばバグがゼロになる」という魔法ではない。テストは「想定した条件」しか確認できないので、想定外の入力までは守れない。TDD の本当の価値は、バグの撲滅そのものより、「いつでも安心して変更できる状態」を保てることにある。完璧な検証ではなく、変更を支える土台と捉えるのが正しい。


本ページはCisco DevNet Associate(200-901) Exam Topics v1.1を学習範囲の根拠として参照。文章・図表はすべて新規作成。