NTTテクノクロス Advent Calendar 2025 シリーズ1の6日目の記事になります。
こんにちは。NTTテクノクロスの金野です。
今回が初めての投稿となるので、好きな技術を楽しみながら執筆したいと思います。
私は現在3年目のエンジニアで普段はアプリケーション開発に携わっています。
テスト駆動開発の実践をこれから増やしていくため自身の備忘もかねて執筆します。
テスト駆動開発とは
早速タイトルにもあるようにテスト駆動開発について深掘っていきます。
テスト駆動開発(Test-Driven Development: TDD)はソフトウェア開発で用いられ、「テスト→実装→リファクタリング」を何回も繰り返しながら開発していく手法です。以下の三つのフェーズを繰り返します。このサイクルを高速に回すことで、設計の改善やバグの早期発見につながります。
基本サイクル
Redフェーズ:仕様に沿ったテストを書く
これから実装する機能に対するテストコードを作成する。この時点で対応する機能のコードはまだ存在しないためテストは必ず失敗する。
Greenフェーズ:テストを成功させる
Redフェーズで作成した失敗するテストを成功させるための最小限のコードを実装します。ここでの目的は、コードの綺麗さや効率さよりもまずテストをパスさせるコードを実装します。
Refactorフェーズ:コードを改善する
テストが成功している状態(Green)を維持しながら、コードの品質を向上させます。このフェーズでは、外部から見た時の振る舞いを変更せずに内部の構造を改善します。
※ このサイクルの準備としてテストリスト(仕様から必要なテストを設計)の作成が必要になります。
実際に試してみる
今回はテストフレームワークではJestを使用し、TypeScriptのテストを書いていこうと思います。
お題は文字列で与えられた数字の計算結果を加える関数addを作成
仕様としては下記になります。
準備(ステップ0)
テストリストの作成。仕様を整理し、テスト駆動開発のサイクルを回すために設計をしていく。
- 空文字列が与えられたら、0を返す
- '1'のように数字が1つだけ与えられたら、その数値を返す
- '1, 2'のようにカンマ区切りで二つの数字が与えられたらその合計を返す
このような仕様でTDDを試していきたいと思います。
ステップ1
空文字列が与えられたら、0を返す Red→Green ※ Refactorは必要であれば実施
Redフェーズ:最初の要件「空文字列が与えられたら、0を返す」ためのテストを書きます。add関数はまだこの要件を満たしていないため、テストは失敗します。
describe('add', () => {
// テストの追加
test('空文字を渡すと0を返す', () => {
expect(add('')).toBe(0)
})
})
Greenフェーズ:このテストを成功させるための最小限の実装を行います。常に0を返すだけの簡単な実装ですが、テストが通ることが重要です。コードの最適化は後のRefactorフェーズで行います。
export const add = (numbers: string): number => {
return 0;
};
ステップ2
数字が1つだけ与えられたら、その数値を返す Red→Green
※ Refactorは必要であれば実施
Redフェーズ:次に数字が一つだけ与えられた場合にその数値を返すという要件を追加したテストを書きます。
describe('add', () => {
test('空文字を渡すと0を返す', () => {
expect(add('')).toBe(0)
});
test('数字が1つだけ与えられたら、その数値を返す', () => {
expect(add('1')).toBe(1)
});
})
Greenフェーズ:このテストを通すために入力が空文字列でなければ数値に変換して返す処理を追加します。
export const add = (numbers: string): number => {
if (numbers) {
return Number(numbers);
};
return 0;
};
ステップ3
カンマ区切りで二つの数字が与えられたらその合計を返す Red→Green→Refactor
Redフェーズ:カンマ区切りで二つの数字が与えられた場合にその合計を返すテストを追加します。
describe('add', () => {
test('空文字を渡すと0を返す', () => {
expect(add('')).toBe(0)
});
test('数字が1つだけ与えられたら、その数値を返す', () => {
expect(add('1')).toBe(1)
});
test('カンマ区切りで二つの数字が与えられたらその合計を返す', () => {
expect(add('1,2')).toBe(3)
})
})
Greenフェーズ:入力にカンマが含まれているかを判定し、分割してそれぞれの数値を足し合わせる処理を追加します。これで三つのケースの全てのテストが通るようになります。
export const add = (numbers: string): number => {
if (numbers.includes(',')) {
const numbersArray = numbers.split(',');
return Number(numbersArray[0]) + Number(numbersArray[1]);
} else if (numbers) {
return Number(numbers);
};
return 0;
};
Refactorフェーズ
if文が少し複雑になっているのでよりシンプルで、将来的に「3つ以上の数字」にも対応できそうな、見通しの良いコードに修正します。if文の分岐も減らすことができました。
export const add = (numbers: string): number => {
if (!numbers) return 0;
const stringNumbers = numbers.split(',');
let sum = 0;
for (const numStr of stringNumbers) {
sum += Number(numStr);
}
return sum;
};
この画像のようにテストが成功していることが確認できます。

注意点
注意点としてはコードを変更するたびに全てのテストを回すことです。
理由としては新機能の追加やリファクタリングで意図せずデグレード(コードの変更で既存機能が壊れること)してしまうのを回避するためです。
これにて完了です! 意外にやってみるとそこまで難しくありませんでした。
よくある勘違い
ここでテスト駆動開発するにあたって、よくある勘違いをt-wadaさんの以下のサイトを参考に考えていきます。
【翻訳】テスト駆動開発の定義
テストコードを先にたくさん書くことではない
TDDの構成要素の中では、特にテストファーストの「先にテストコードから書く」という印象が強いようです。また、テスト駆動開発は品質保証の手法であるという誤解も特に日本では根強いように思います。これらのイメージが独り歩きすると、テストコードを先に書きすぎるといった「よくある過ち」につながります。
今回でいえば三つのテストケースを一気に全部書くのではなく一つずつテストを回しながら実装してきました。
これにより高速なフィードバックが維持されます。テストが壊れた時、原因は直前に書いたごく少量のコードにあることが明らかであり、デバッグが非常に容易になります。このセーフティネットがあるからこそリファクタリング等の変更もトライできます。
設計せずにいきなりテストコードを書き始めること
Kent Beckの今回のブログエントリや書籍『テスト駆動開発』を読むと分かるのは、TDDのステップは、より正確に表現するなら「リスト、レッド、グリーン、リファクタ」であるということです。これは今回の翻訳記事で初めて認識された方も多いのではないでしょうか。TDDには設計のステップが明示的に存在しています。
今回の場合テストリストは
- 空文字列が与えられたら、0を返す
- '1'のように数字が1つだけ与えられたら、その数値を返す
- '1, 2'のようにカンマ区切りで二つの数字が与えられたらその合計を返す
このように作成をしてからテストを作っていきます。
リストはテストの指針となりとても重要になってきます。
テスト駆動開発を構成する技術プラクティス
日本でのテスト駆動開発の誤認にはいくつかの種類があると考えています。1つめは、今回のようなワークフローではなく、テスト駆動開発を構成する技術プラクティスをテスト駆動開発と呼んでしまうことです。典型的には下記のものを「テスト駆動開発」と呼んでいる場面があるように思います。
| 概念 | 定義 |
|---|---|
| 自動テスト(Automated Test) | テスティングフレームワーク(Jestやpytestなど)を使ってテストコードを書くこと。誰がいつ書いても構わない。 |
| 開発者テスト(Developer Testing) | 開発者自身が自動テストを書きながら開発すること。テストコードを書くタイミングは後からでも効果がある。 |
| テストファースト(Test-First Programming) | 実装よりも前にテストコードを書くこと。TDDのようなサイクルを回す意味は含まれていない。 |
正しく理解し、運用することで単にバグを減らすだけでなく、コードの品質向上、設計の改善などの効果があります。
AI駆動開発との親和性(応用)
近年の開発トレンドでAI駆動開発が加速していますが、このアプローチにテスト駆動開発を組み合わせることで、その効果を最大化できます。
テスト駆動開発で作成するテストが、AIが高速でコードを生成・実装する際の「明確な指示書」となると同時に、AIが誤った方向へ進んだり、品質の低いコードを生成したりするのを防ぐ「品質と設計のガードレール」として機能します。
以下どのようなアプローチで進められるか記載しています。
-
アプローチ案:
-
仕様定義(人間主導 AI伴奏): ユーザーストーリーやドメインルールを明確化する。共通ルールファイルの作成も必須
- ユーザーストーリーや受け入れ条件を定義
- ここで、AIには理解が難しいドメイン固有のルールや暗黙の前提を明確に言語化することが非常に重要
- また、テストリストの作成で実装の設計をしていき、テスト駆動開発のサイクルを回すための準備をしていきます
-
代表テスト作成(人間主導 AI伴奏): 仕様に基づき、主要な振る舞いを検証するテストコードを先に書く。(TDDのRedフェーズ)
- 人間が最も重要ないくつかのテストケース(ハッピーパス、代表的な異常系など)をAIのサポートもと実装する。※ 実装されているコードはないのでテストは失敗する
- なぜこのステップが重要:
- AIの暴走を防ぐ:実装と同時にテストを生成させると、AIは実装の内部構造に依存した「通って当たり前」のテストを書きがちです。先にテストを書くことで、外部から観測可能な「振る舞い」を検証する、質の高いテストになります
-
実装(AI委託): 作成したテストコードをコンテキストとしてAIに渡し、テストをパスする実装を生成させる。(TDDのGreenフェーズ)
- 手順1の仕様と手順2のテストコードをAI(Claudeなど)に提示します。コードを生成させる
- AIが生成したコードでテストを実行し、すべてパスすることを確認します(Green)
- テスト駆動開発のサイクルを高速かつ確実に回すため、実装の差分(Diff)を最小化できるよう『テストケース一つにつき一つの実装』を徹底するルールを確立し、これによりコードレビューのコスト低減と品質向上を実現します。
-
テスト増幅(AI+人間): AIに境界値分析などの観点でテストケースを追加させ、人間がレビューする
- 考慮漏れているテスト観点(例: 境界値、例外処理、nullチェックなど)を洗い出し、テストケースを追加しテストコードを生成する
-
レビューと調整(人間): AIが生成したコード全体を人間がレビューし、リファクタリングやドメイン観点での修正を行う。(TDDのRefactorフェーズ)
- テストが通っている状態でコードのリファクタリングを行う
- 最終的にビジネス要件を満たしているか、ドメイン知識を持つ人間がレビューをしプルリクエストを承認する
-
仕様定義(人間主導 AI伴奏): ユーザーストーリーやドメインルールを明確化する。共通ルールファイルの作成も必須
まとめ
このように、AI駆動開発(特に仕様駆動開発)にTDDのフレームワークが組み込みやすいのは、テスト(=振る舞いの仕様)をAIが実装を始める前の明確なインプットとして定義できるからです。
またAIの高速なサイクル実行能力を最大限に引き出すためには、テストケース一つにつき一つの実装として差分(Diff)を小さく保つルールと、それを可能にするAIのプロンプトエンジニアリングの重要度が極めて高まると言えます。
最後に
品質を高める非常に有効な開発手法なので、ぜひ実際に試してみてください。
これからのAI活用にも応用が効くので身につけていきたいところです。
今回が初めての投稿でしたが、まとめるのに苦労しながらも楽しく執筆できました!
まだまだ明日以降もNTTテクノクロス Advent Calendar 2025 続いていきます。
明日はシリーズ1 @n-yuuto さんの 「日々の仕事に追われつつもDevOps活動に取り組む」とシリーズ2 @R-4oyagi さんの 「個人開発で0→リリースまで走り切った『いんすたんとアルバム』開発記録と、AIが加速させたプロダクトづくり」です。
お楽しみに!!