第6章: ソフトウェアテストの理論 - 品質保証の科学的基盤
6.1 ソフトウェアテストの起源と歴史
6.1.1 バグの誕生 - 1945年の蛾
ソフトウェアにおける「バグ」という用語の起源は、1945年9月9日にまで遡る。Harvard Mark II計算機で作業していたGrace Hopperのチームが、リレーに挟まった実際の蛾(moth)を発見し、それがプログラムの誤動作の原因であることを突き止めた。
1945年9月9日、ハーバード大学
Grace Hopper: "計算機が停止した原因を調査中..."
↓
リレー #70 に蛾が挟まっているのを発見
↓
ログブックに蛾を貼り付け
"First actual case of bug being found."
(バグが発見された最初の実例)
この逸話以前から「バグ」という用語は工学的な問題を指す俗語として使われていたが(Thomas Edisonも1878年に使用していた)、Hopperの記録により、コンピュータ分野でこの用語が定着した。
歴史的意義:
この出来事が示すのは、計算機科学の黎明期から、予期しない問題の検出と解決がエンジニアの中核的な作業であったということである。現代のソフトウェアテストは、この問題解決の営みを体系化したものと言える。
6.1.2 初期のソフトウェアテスト - 1950年代
1950年代、コンピュータプログラミングが専門職として確立し始めると、テストの必要性も認識されるようになった。この時代のテストは主にデバッギングと同義であり、プログラムを実行して問題が発生したら修正する、というアドホックなアプローチが取られていた。
初期のテストアプローチ(1950年代):
1. プログラムを実行
2. 出力を手動で確認
3. 問題があれば修正
4. 再度実行
5. 繰り返し
特徴:
- テストとデバッグの区別なし
- 計画的なテスト設計なし
- 主に「動作確認」のみ
6.1.3 構造化テストの登場 - 1970年代
1970年代に入ると、ソフトウェアの複雑さが増大し、体系的なテスト手法の必要性が明らかになった。
Glenford Myersの貢献(1979):
Glenford Myersの著書「The Art of Software Testing」は、ソフトウェアテストを科学として確立した画期的な作品である。Myersは重要な洞察を提示した:
> 「テストとは、エラーを見つける目的でプログラムを実行するプロセスである。」 > - Glenford Myers, 1979
この定義は革命的だった。それまでテストは「プログラムが正しく動くことを確認する」ものと考えられていたが、Myersは「エラーを見つけることこそがテストの本質」であると主張したのである。
/* Myersのパラダイムシフト */
/* 旧来の考え方 */
// テスト = 「動くことを確認する」
// 成功 = 「テストが通った」
// 目標 = 「すべてのテストが通ること」
/* Myersの考え方 */
// テスト = 「バグを見つける」
// 成功 = 「バグを発見した」
// 目標 = 「できるだけ多くのバグを見つけること」
/* 心理学的含意 */
// テストは「破壊的」な活動である
// 良いテスターは自分のコードの欠陥を見つけることに喜びを感じる
// テストと開発は異なるマインドセットを要求する
6.1.4 テスト駆動開発の源流 - 1960年代IBM
テスト駆動開発(TDD)は近年のアジャイル開発で広まったが、その概念は1960年代のIBMにまで遡る。NASAのProject Mercuryのソフトウェア開発において、プログラマたちはテストを先に書くプラクティスを採用していた。
Project Mercury(1961-1963)のプラクティス:
1. 要件を受け取る
2. テストケースを設計(実装前)
3. テストをパンチカードに記録
4. プログラムを実装
5. テストを実行
6. 失敗したら修正、繰り返し
この方法論は「incremental approach」と呼ばれた
Kent Beckが2003年に「Test-Driven Development: By Example」を出版し、TDDを現代的な形で体系化したが、その根幹にある考え方は40年以上前から存在していたのである。
6.2 ソフトウェアテストの理論的基盤
6.2.1 テストの限界 - Dijkstraの定理
Edsger Dijkstraは1969年に、ソフトウェアテストの根本的な限界を指摘した:
> 「テストはバグの存在を示すことはできるが、バグの不在を証明することはできない。」 > - Edsger W. Dijkstra
この言葉は、ソフトウェアテストの本質的な性質を表している。
Dijkstraの定理の数学的理解:
プログラム P の入力空間を I とする。
|I| = 入力空間の大きさ
例: 32ビット整数1つを受け取る関数
|I| = 2^32 = 4,294,967,296 通り
例: 32ビット整数2つを受け取る関数
|I| = 2^64 ≈ 1.8 × 10^19 通り
例: 長さn の文字列を受け取る関数(ASCII)
|I| = 128^n = 指数関数的に増大
すべての入力を網羅的にテストすることは、
非常に単純なプログラム以外では実質的に不可能である。
帰結:
- テストは確率的である: 有限個のテストケースで無限(または極めて大きな)入力空間をサンプリングする
- テスト設計が重要: どの入力をテストするかの選択が決定的に重要
- 完全性は幻想: 100%の信頼性をテストだけで達成することは不可能
6.2.2 テストの数学的モデル
ソフトウェアテストを数学的に定式化すると、その本質が明らかになる。
定義(プログラムの正しさ):
プログラム P: I → O (入力空間Iから出力空間Oへの関数)
仕様 S: I → O (期待される動作を規定する関数)
P が正しい ⟺ ∀i ∈ I: P(i) = S(i)
つまり、すべての入力に対して、プログラムの出力が仕様と一致する。
定義(テストケース):
テストケース t = (i, o) where i ∈ I, o ∈ O
t が成功 ⟺ P(i) = o
t が失敗 ⟺ P(i) ≠ o
定義(テストスイート):
テストスイート T = {t₁, t₂, ..., tₙ}
T が成功 ⟺ ∀t ∈ T: t が成功
T が失敗 ⟺ ∃t ∈ T: t が失敗
テストの網羅性(理想的な場合):
完全テストスイート T_complete = {(i, S(i)) | i ∈ I}
T_complete が成功 ⟺ P は正しい
しかし、|T_complete| = |I| であり、
多くの場合 |I| が巨大すぎて実行不可能
6.2.3 等価クラス分割 - 入力空間の構造化
等価クラス分割(Equivalence Class Partitioning)は、無限の入力空間を有限個の「代表的なケース」に分割するための理論的枠組みである。
定義(等価クラス):
入力空間 I を分割する等価関係 ~ を定義:
i₁ ~ i₂ ⟺ P(i₁) の振る舞いが P(i₂) と「同等」
等価クラス [i] = {j ∈ I | j ~ i}
重要な性質:
1. 網羅性: ∪[i] = I (すべての入力がいずれかのクラスに属する)
2. 排他性: [i] ∩ [j] = ∅ (異なるクラスは重複しない)
等価クラス分割の原理:
各等価クラスから1つの代表値をテストすれば、そのクラス全体の振る舞いを推測できる、という仮定に基づく。
/* ft_printf の %d に対する等価クラス分割 */
/*
入力空間 I = 全ての int 値
= {-2147483648, ..., -1, 0, 1, ..., 2147483647}
等価クラスの分割:
EC1: 正の数 {1, 2, 3, ..., 2147483647}
- 代表値: 42, 1, 12345
EC2: ゼロ {0}
- 代表値: 0 (唯一の要素)
EC3: 負の数 {-2147483648, ..., -1}
- 代表値: -42, -1, -12345
EC4: 境界値 {INT_MIN, INT_MAX}
- 代表値: -2147483648, 2147483647
EC5: 特殊な内部境界 {-1, 1, 9, 10, 99, 100, ...}
- 桁数が変わる境界
*/
void test_equivalent_classes(void)
{
/* EC1: 正の数 */
assert(ft_printf("%d", 42) == printf("%d", 42));
assert(ft_printf("%d", 1) == printf("%d", 1));
/* EC2: ゼロ */
assert(ft_printf("%d", 0) == printf("%d", 0));
/* EC3: 負の数 */
assert(ft_printf("%d", -42) == printf("%d", -42));
/* EC4: 境界値 */
assert(ft_printf("%d", INT_MIN) == printf("%d", INT_MIN));
assert(ft_printf("%d", INT_MAX) == printf("%d", INT_MAX));
}
6.2.4 境界値分析 - エラーが潜む場所
境界値分析(Boundary Value Analysis)は、エラーは境界付近で発生しやすいという経験則に基づくテスト技法である。
理論的根拠:
多くのプログラムエラーは以下のパターンで発生:
1. Off-by-one エラー
- < を <= と書くべきだった
- > を >= と書くべきだった
- i < n を i <= n とすべきだった
2. 境界条件の見落とし
- 空配列のケースを考慮していない
- ゼロ除算のチェック漏れ
- NULL ポインタのチェック漏れ
3. オーバーフロー
- INT_MAX + 1 の振る舞い
- 配列の境界を超えるアクセス
境界値テストの公式:
変数 x が範囲 [min, max] を取るとき:
テストすべき値:
- min - 1 (範囲外、左側)
- min (境界、左端)
- min + 1 (境界の直後)
- nom (典型的な中間値)
- max - 1 (境界の直前)
- max (境界、右端)
- max + 1 (範囲外、右側)
/* 境界値分析の実装例 */
void test_boundary_values(void)
{
/* 32ビット整数の境界 */
/* INT_MIN 周辺 */
printf("Testing INT_MIN boundary:\n");
test_value(INT_MIN); /* 境界値 */
test_value(INT_MIN + 1); /* 境界の直後 */
/* ゼロ周辺 */
printf("Testing zero boundary:\n");
test_value(-1); /* ゼロの直前 */
test_value(0); /* 境界値 */
test_value(1); /* ゼロの直後 */
/* INT_MAX 周辺 */
printf("Testing INT_MAX boundary:\n");
test_value(INT_MAX - 1); /* 境界の直前 */
test_value(INT_MAX); /* 境界値 */
/* 桁数の境界(10進数表現) */
printf("Testing digit boundaries:\n");
test_value(9); /* 1桁の最大値 */
test_value(10); /* 2桁の最小値 */
test_value(99); /* 2桁の最大値 */
test_value(100); /* 3桁の最小値 */
test_value(-9);
test_value(-10);
}
void test_value(int n)
{
int ret_ft = ft_printf("%d", n);
int ret_std = printf("%d", n);
printf(" | n=%d: ft=%d, std=%d %s\n",
n, ret_ft, ret_std,
(ret_ft == ret_std) ? "[OK]" : "[FAIL]");
}
6.3 ホワイトボックステストとブラックボックステスト
6.3.1 二つのテスト哲学
ソフトウェアテストには、根本的に異なる二つのアプローチが存在する。
ブラックボックステスト(Black-box Testing):
┌─────────────────┐
入力 ────────────────▶│ プログラム │────────────────▶ 出力
│ (内部は見えない) │
└─────────────────┘
特徴:
- プログラムの内部構造を考慮しない
- 入力と出力の関係のみに基づいてテスト
- 仕様に基づくテスト(Specification-based testing)
利点:
- 実装に依存しない
- 仕様と実装の乖離を発見できる
- テスターが開発者である必要がない
欠点:
- 内部のエラーパスをテストできない可能性
- コードカバレッジを直接測定できない
ホワイトボックステスト(White-box Testing):
┌─────────────────┐
入力 ────────────────▶│ プログラム │────────────────▶ 出力
│ (内部が見える) │
│ if-else, loop │
│ function calls │
└─────────────────┘
特徴:
- プログラムの内部構造に基づいてテスト
- すべてのパス、分岐、文をカバーすることを目指す
- 構造に基づくテスト(Structure-based testing)
利点:
- 内部の論理エラーを発見しやすい
- コードカバレッジを測定できる
- デッドコードを発見できる
欠点:
- 仕様の誤りを見つけられない可能性
- 実装に依存するため、リファクタリングで無効になる
- テスターがコードを理解する必要がある
6.3.2 コードカバレッジの理論
ホワイトボックステストの品質を測定するために、様々なカバレッジ基準が定義されている。
文カバレッジ(Statement Coverage):
定義: プログラム中のすべての文が少なくとも1回実行される
カバレッジ = (実行された文の数) / (全文の数) × 100%
例:
1: int abs_value(int x)
2: {
3: if (x < 0) ← 条件
4: return -x; ← 文A
5: return x; ← 文B
6: }
テスト: abs_value(-5)
実行: 行3(条件), 行4(文A)
カバレッジ: 2/3 = 66.7%
テスト: abs_value(-5), abs_value(5)
実行: 行3, 行4, 行5
カバレッジ: 3/3 = 100%
分岐カバレッジ(Branch Coverage):
定義: すべての分岐(if-elseの両方向)が少なくとも1回実行される
カバレッジ = (実行された分岐の数) / (全分岐の数) × 100%
例:
if (x < 0) ← 分岐点
return -x; ← true 分岐
return x; ← false 分岐
100% 分岐カバレッジには、true と false の両方のテストが必要
条件カバレッジ(Condition Coverage):
定義: 各条件の各ブール部分式が true と false の両方を取る
例:
if (a > 0 && b > 0) ← 2つの条件
100% 条件カバレッジには:
- a > 0 が true のケース
- a > 0 が false のケース
- b > 0 が true のケース
- b > 0 が false のケース
注意: 条件カバレッジ 100% でも分岐カバレッジ 100% とは限らない
MC/DC(Modified Condition/Decision Coverage):
定義: 各条件が結果に独立して影響することを示す
航空宇宙ソフトウェア(DO-178B Level A)で要求される厳格な基準
例: if (a && b)
必要なテストケース:
1. a=true, b=true → 結果true
2. a=true, b=false → 結果false (bの影響を示す)
3. a=false, b=true → 結果false (aの影響を示す)
これにより、各条件が独立して判定結果に影響することを確認
6.3.3 パスカバレッジとその限界
パスカバレッジ(Path Coverage):
定義: プログラム中のすべての実行パスが少なくとも1回実行される
問題: パスの数は指数関数的に増加する
例:
if (cond1) { A } else { B }
if (cond2) { C } else { D }
if (cond3) { E } else { F }
パスの数: 2 × 2 × 2 = 8 通り
A-C-E, A-C-F, A-D-E, A-D-F,
B-C-E, B-C-F, B-D-E, B-D-F
n個の分岐 → 2^n のパス
ループを含む場合:
for (int i = 0; i < n; i++) { ... }
n=0 のパス
n=1 のパス
n=2 のパス
...
n=∞ のパス → 無限個のパス
結論:
完全なパスカバレッジは、ほとんどのプログラムで不可能である。実用的には、文カバレッジや分岐カバレッジを基準として使用し、重要なパスに対しては明示的にテストを追加する。
6.4 テスト駆動開発(TDD)の理論
6.4.1 TDDの形式的定義
テスト駆動開発は、以下のサイクルを繰り返す開発手法である。
Red-Green-Refactorサイクル:
┌────────────────────────────────────────────────────────┐
│ │
│ ┌─────────┐ ┌─────────┐ ┌─────────────┐ │
│ │ RED │───▶│ GREEN │───▶│ REFACTOR │───┐ │
│ │(失敗する │ │(テストを │ │(コードを │ │ │
│ │テストを │ │通すコード│ │改善) │ │ │
│ │書く) │ │を書く) │ │ │ │ │
│ └─────────┘ └─────────┘ └─────────────┘ │ │
│ ▲ │ │
│ └──────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────┘
各フェーズの詳細:
/* RED フェーズ: 失敗するテストを書く */
/*
1. 次に実装すべき機能を1つ選ぶ
2. その機能に対するテストを書く
3. テストを実行し、失敗することを確認
重要: テストは「正しい失敗」をするべき
- コンパイルエラーではなく
- アサーションの失敗であるべき
*/
/* 例: %d 変換のテスト */
void test_print_decimal(void)
{
/* ft_printf がまだ存在しないので、これは失敗する */
int ret = ft_printf("%d", 42);
assert(ret == 2); /* "42" は2文字 */
}
/* GREEN フェーズ: テストを通す最小限のコードを書く */
/*
1. テストを通すための最小限のコードを書く
2. 「きれいなコード」は気にしない
3. とにかくテストを通すことだけを目指す
重要: 過度な一般化を避ける
今のテストを通すことだけに集中
*/
/* 例: 最小限の実装 */
int ft_printf(const char *format, ...)
{
/* とりあえずハードコードで通す */
write(1, "42", 2);
return 2;
}
/* これはテストを通す!(ただし汎用性はない) */
/* REFACTOR フェーズ: コードを改善する */
/*
1. テストが通る状態を維持しながらコードを改善
2. 重複を除去
3. 可読性を向上
4. 設計を改善
重要: リファクタリング中はテストが通り続けること
*/
/* 例: 一般化された実装 */
int ft_printf(const char *format, ...)
{
va_list args;
int len = 0;
va_start(args, format);
while (*format)
{
if (*format == '%' && *(format + 1) == 'd')
{
len += print_decimal(va_arg(args, int));
format += 2;
}
else
{
write(1, format, 1);
len++;
format++;
}
}
va_end(args);
return len;
}
6.4.2 TDDの理論的利点
1. 設計の改善(Design for Testability):
TDDでは、テストしやすいコードを書くことが強制される。
テストしやすいコード = 疎結合なコード
= 変更しやすいコード
= 良い設計
例: グローバル変数を多用したコードはテストしにくい
→ TDDでは自然とグローバル変数を避けることになる
2. ドキュメントとしてのテスト:
/* テストはコードの使い方を示す生きたドキュメント */
/* この関数はどう使うのか? */
/* → テストを見ればわかる */
void test_ft_printf_examples(void)
{
/* 基本的な使い方 */
ft_printf("Hello, World!\n");
/* 数値の出力 */
ft_printf("Number: %d\n", 42);
/* 複数の引数 */
ft_printf("Name: %s, Age: %d\n", "Alice", 30);
/* NULLの扱い */
ft_printf("%s\n", NULL); /* "(null)" を出力 */
}
3. リグレッションの防止:
リグレッション = 以前動いていた機能が壊れること
TDDのテストスイートは「セーフティネット」として機能:
1. コードを変更
2. テストを実行
3. 失敗があれば、どの機能が壊れたかすぐわかる
4. 変更が安全かどうかを即座に確認できる
6.4.3 TDDの批判と限界
批判1: 過度なテストによるオーバーヘッド:
すべてのコードにTDDを適用すると:
- 開発時間が増加
- メンテナンスコストが増加
- テストコード自体にもバグが混入する可能性
対応: 重要な機能に集中してTDDを適用
GUIやプロトタイプにはTDDは不向きな場合も
批判2: テストしにくい領域:
TDDが困難な領域:
- 並行処理
- GUI
- ネットワーク通信
- データベース
対応: モックやスタブを使用
統合テストとは別の戦略を使用
批判3: 誤った安心感:
テストが通る = コードが正しい ではない
- テスト自体が間違っている可能性
- テストが不十分な可能性
- 仕様の理解が間違っている可能性
対応: コードレビュー、ペアプログラミングの併用
テストの品質にも注意を払う
6.5 ミューテーションテスト - テストの品質を測る
6.5.1 ミューテーションテストの理論
ミューテーションテスト(Mutation Testing)は、テストスイートの品質を測定する技法である。
基本原理:
もしテストスイートが良いものであれば、
コードに小さな変更(ミューテーション)を加えると、
テストが失敗するはずである。
テストが失敗しない = テストスイートに欠陥がある
定義(ミュータント):
元のプログラム P からミューテーション演算子 M を適用して
生成されたプログラム P' をミュータントと呼ぶ。
P' = M(P)
例:
P: if (x > 0)
M: > を >= に変更
P': if (x >= 0) ← ミュータント
ミューテーション演算子の例:
/* AOR: 算術演算子置換 */
/* Arithmetic Operator Replacement */
x = a + b; → x = a - b; /* + を - に */
x = a + b; → x = a * b; /* + を * に */
x = a + b; → x = a / b; /* + を / に */
/* ROR: 関係演算子置換 */
/* Relational Operator Replacement */
if (x < y) → if (x <= y) /* < を <= に */
if (x < y) → if (x > y) /* < を > に */
if (x < y) → if (x == y) /* < を == に */
/* LOR: 論理演算子置換 */
/* Logical Operator Replacement */
if (a && b) → if (a || b) /* && を || に */
if (a && b) → if (a) /* 条件の削除 */
/* AORB: 定数置換 */
/* Absolute and Relative Boundary Operator */
if (x == 0) → if (x == 1) /* 0 を 1 に */
return n; → return n + 1; /* +1 */
return n; → return 0; /* 定数に置換 */
6.5.2 ミューテーションスコア
定義(ミューテーションスコア):
ミューテーションスコア = (殺されたミュータントの数) / (生成されたミュータントの数) × 100%
殺されたミュータント = テストスイートによって検出されたミュータント
= 少なくとも1つのテストが失敗したミュータント
生き残ったミュータント = すべてのテストが通ったミュータント
= テストスイートの弱点を示す
例:
/* 元のコード */
int max(int a, int b)
{
if (a > b) /* 行1 */
return a; /* 行2 */
return b; /* 行3 */
}
/* テストスイート */
void test_max(void)
{
assert(max(3, 5) == 5); /* テスト1 */
assert(max(7, 2) == 7); /* テスト2 */
}
/* ミュータント1: > を >= に変更 */
int max_mutant1(int a, int b)
{
if (a >= b) /* 変更 */
return a;
return b;
}
/* max(3, 3) のテストがないので生き残る! */
/* ミュータント2: > を < に変更 */
int max_mutant2(int a, int b)
{
if (a < b) /* 変更 */
return a;
return b;
}
/* テスト1: max(3, 5) = 5 → mutant: 3 → 失敗!殺される */
/* ミュータント3: return a を return b に変更 */
int max_mutant3(int a, int b)
{
if (a > b)
return b; /* 変更 */
return b;
}
/* テスト2: max(7, 2) = 7 → mutant: 2 → 失敗!殺される */
/*
分析:
- 生成されたミュータント: 3
- 殺されたミュータント: 2
- 生き残ったミュータント: 1
- ミューテーションスコア: 2/3 = 66.7%
改善: max(3, 3) のテストを追加
→ ミュータント1も殺される
→ スコア: 100%
*/
6.5.3 等価ミュータント問題
等価ミュータント:
元のプログラムと完全に同じ振る舞いをするミュータント。これは殺すことが不可能である。
/* 元のコード */
int index = 0;
while (index < n) {
/* ... */
index = index + 1;
}
/* 等価ミュータント */
int index = 0;
while (index != n) { /* < を != に変更 */
/* ... */
index = index + 1;
}
/* indexは0からnまで1ずつ増加するので、
< と != は同じ結果を生む(index > nにはならない) */
等価ミュータントの問題:
6.6 ft_printfに対するテスト戦略
6.6.1 テスト可能な設計
ft_printfをテストしやすい形で設計することが、品質保証の第一歩である。
モジュール分割によるテスト可能性:
/*
ft_printf を以下のモジュールに分割:
1. パーサー: フォーマット文字列を解析
2. 変換器: 各型を文字列に変換
3. 出力器: 文字列を出力
各モジュールは独立してテスト可能
*/
/* パーサーのテスト */
void test_parser(void)
{
t_format fmt = parse_format("%10.5d");
assert(fmt.width == 10);
assert(fmt.precision == 5);
assert(fmt.specifier == 'd');
}
/* 変換器のテスト */
void test_converter(void)
{
char buffer[50];
int len = convert_decimal(42, buffer);
assert(strcmp(buffer, "42") == 0);
assert(len == 2);
}
/* 出力器のテスト */
void test_output(void)
{
/* 標準出力をリダイレクトしてキャプチャ */
/* ... */
}
6.6.2 等価クラスとテストケース設計
ft_printfの各変換指定子に対して、等価クラスを定義する。
/*
=== %c 変換のテストケース設計 ===
入力ドメイン: int 型(char として解釈)
等価クラス:
EC1: 印刷可能なASCII文字 (32-126)
代表値: 'A', 'a', '0', ' ', '~'
EC2: 制御文字 (0-31, 127)
代表値: '\0', '\n', '\t', 127
EC3: 拡張ASCII (128-255)
代表値: 128, 255
EC4: 負の値(int として)
代表値: -1 → 255 として解釈される可能性
境界値:
- 0 (NULL文字)
- 31 (制御文字の最大)
- 32 (印刷可能の最小)
- 126 (印刷可能の最大)
- 127 (DEL)
- 255 (unsigned charの最大)
*/
void test_char_comprehensive(void)
{
int ret_ft, ret_std;
/* EC1: 印刷可能文字 */
ret_ft = ft_printf("%c", 'A');
ret_std = printf("%c", 'A');
assert(ret_ft == ret_std);
ret_ft = ft_printf("%c", ' ');
ret_std = printf("%c", ' ');
assert(ret_ft == ret_std);
/* EC2: 制御文字 */
ret_ft = ft_printf("%c", '\0');
ret_std = printf("%c", '\0');
assert(ret_ft == ret_std);
ret_ft = ft_printf("%c", '\n');
ret_std = printf("%c", '\n');
assert(ret_ft == ret_std);
/* 境界値テスト */
ret_ft = ft_printf("%c", 0);
ret_std = printf("%c", 0);
assert(ret_ft == ret_std);
ret_ft = ft_printf("%c", 127);
ret_std = printf("%c", 127);
assert(ret_ft == ret_std);
}
/*
=== %s 変換のテストケース設計 ===
入力ドメイン: char* (文字列ポインタ)
等価クラス:
EC1: 通常の文字列
代表値: "Hello", "World"
EC2: 空文字列
代表値: ""
EC3: NULLポインタ
代表値: NULL → "(null)" を出力
EC4: 特殊文字を含む文字列
代表値: "Tab\there", "New\nline"
EC5: 非常に長い文字列
代表値: 10000文字の文字列
境界値:
- 空文字列 ""
- 1文字の文字列 "A"
- NULL
- 非常に長い文字列
*/
void test_string_comprehensive(void)
{
int ret_ft, ret_std;
/* EC1: 通常の文字列 */
ret_ft = ft_printf("%s", "Hello");
ret_std = printf("%s", "Hello");
assert(ret_ft == ret_std);
/* EC2: 空文字列 */
ret_ft = ft_printf("%s", "");
ret_std = printf("%s", "");
assert(ret_ft == ret_std);
/* EC3: NULLポインタ */
ret_ft = ft_printf("%s", NULL);
ret_std = printf("%s", NULL);
/* 注意: NULLの扱いは実装依存 */
/* EC5: 長い文字列 */
char *long_str = malloc(10001);
if (long_str)
{
memset(long_str, 'A', 10000);
long_str[10000] = '\0';
ret_ft = ft_printf("%s", long_str);
ret_std = printf("%s", long_str);
assert(ret_ft == ret_std);
free(long_str);
}
}
6.6.3 INT_MIN問題とテスト
第4章で学んだINT_MIN問題は、テストで特に注意が必要である。
/*
INT_MIN の特殊性:
INT_MIN = -2147483648
-INT_MIN = 2147483648 > INT_MAX (オーバーフロー!)
これは2の補数表現の非対称性から生じる。
テストでこの問題を確実に検出する必要がある。
*/
void test_int_min_problem(void)
{
int ret_ft, ret_std;
/* INT_MIN の基本テスト */
ret_ft = ft_printf("%d", INT_MIN);
ret_std = printf("%d", INT_MIN);
assert(ret_ft == ret_std);
/* 期待される出力: "-2147483648" (11文字) */
/* INT_MIN - 1 はオーバーフロー */
/* このテストは未定義動作を引き起こす可能性がある */
/* ret_ft = ft_printf("%d", INT_MIN - 1); */
/* 推奨: このようなテストは避ける */
/* 代わりに、数値リテラルで直接テスト */
ret_ft = ft_printf("%d", -2147483648);
ret_std = printf("%d", -2147483648);
assert(ret_ft == ret_std);
}
6.6.4 出力比較テストの実装
ft_printfの出力を標準printfと比較するテストシステム。
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#define BUFFER_SIZE 4096
/*
出力キャプチャの原理:
1. 標準出力(fd 1)を一時的にパイプにリダイレクト
2. 関数を実行
3. パイプから出力を読み取り
4. 標準出力を復元
*/
typedef struct s_capture {
char output[BUFFER_SIZE];
int length;
int return_value;
} t_capture;
t_capture capture_output(int (*func)(const char *, ...),
const char *format, ...)
{
t_capture result = {0};
int pipefd[2];
int saved_stdout;
va_list args;
/* パイプを作成 */
if (pipe(pipefd) == -1)
return result;
/* 標準出力を保存 */
saved_stdout = dup(STDOUT_FILENO);
/* 標準出力をパイプの書き込み端にリダイレクト */
dup2(pipefd[1], STDOUT_FILENO);
close(pipefd[1]);
/* 関数を実行 */
va_start(args, format);
result.return_value = func(format, args);
va_end(args);
/* 標準出力を復元 */
fflush(stdout);
dup2(saved_stdout, STDOUT_FILENO);
close(saved_stdout);
/* パイプから出力を読み取り */
result.length = read(pipefd[0], result.output, BUFFER_SIZE - 1);
if (result.length >= 0)
result.output[result.length] = '\0';
close(pipefd[0]);
return result;
}
/*
テスト比較フレームワーク
*/
typedef struct s_test_result {
int passed;
int failed;
int total;
} t_test_result;
void compare_test(t_test_result *result,
const char *test_name,
const char *format, ...)
{
t_capture ft_result, std_result;
va_list args1, args2;
result->total++;
/* 両方の関数をキャプチャ */
/* 注意: 実際の実装ではva_listの扱いがより複雑 */
/* 比較 */
if (ft_result.return_value != std_result.return_value ||
strcmp(ft_result.output, std_result.output) != 0)
{
result->failed++;
printf("FAIL: %s\n", test_name);
printf(" ft_printf: ret=%d, output=\"%s\"\n",
ft_result.return_value, ft_result.output);
printf(" printf: ret=%d, output=\"%s\"\n",
std_result.return_value, std_result.output);
}
else
{
result->passed++;
printf("PASS: %s\n", test_name);
}
}
6.7 デバッグの科学
6.7.1 デバッグの歴史と理論
デバッグ(debugging)という言葉の起源は前述のGrace Hopperに遡るが、体系的なデバッグ手法の研究は1970年代から本格化した。
科学的デバッグ法:
Andreas Zellerは2009年の著書「Why Programs Fail」で、デバッグを科学的方法論として定式化した:
科学的方法とデバッグの対応:
科学的方法:
1. 現象を観察する
2. 仮説を立てる
3. 実験で仮説を検証
4. 結果を分析
5. 仮説を修正または確認
デバッグ:
1. バグの症状を観察する
2. 原因の仮説を立てる
3. テストで仮説を検証
4. 結果を分析
5. 仮説を修正し、修正を適用
デルタデバッギング(Delta Debugging):
問題: バグを引き起こす最小の入力は何か?
アルゴリズム:
1. バグを引き起こす入力 I を二分する
2. 前半 I₁ でバグが発生するか確認
3. 発生すれば I₁ をさらに二分
4. 発生しなければ後半 I₂ で確認
5. 再帰的に繰り返し
6. 最小の失敗入力を特定
例:
入力 "ABCDEFGHIJ" でクラッシュ
"ABCDE" でクラッシュ
"ABC" でクラッシュ
"AB" でクラッシュ
"A" で正常
"B" でクラッシュ → 最小失敗入力
6.7.2 GDBの理論と実践
GDB(GNU Debugger)は、プログラムの実行を制御し、内部状態を観察するためのツールである。
GDBの基本概念:
ブレークポイント(Breakpoint):
プログラムの実行を一時停止する地点
ウォッチポイント(Watchpoint):
特定の変数が変更されたときに停止
ステッピング:
- step: 関数呼び出しの中に入る
- next: 関数呼び出しを飛ばす
- continue: 次のブレークポイントまで実行
スタックフレーム:
関数呼び出しの履歴(コールスタック)
ft_printfのデバッグ例:
# デバッグ情報付きでコンパイル
gcc -g -o test_printf test.c ft_printf.c
# GDBを起動
gdb ./test_printf
# 一般的なコマンド
(gdb) break ft_printf # ブレークポイント設定
(gdb) run # 実行開始
(gdb) print format # 変数の値を表示
(gdb) print *format@10 # 文字列の最初の10文字
(gdb) backtrace # コールスタックを表示
(gdb) step # 次の行(関数内に入る)
(gdb) next # 次の行(関数を飛ばす)
(gdb) continue # 続行
(gdb) quit # 終了
6.7.3 メモリデバッグ - Valgrind
Valgrindは、メモリエラーを検出するための動的解析ツールである。
Valgrindの動作原理:
Valgrindは、プログラムのすべてのメモリアクセスを
シミュレートされたCPU上で追跡する。
追跡される情報:
1. 各メモリ位置が割り当て済みかどうか
2. 各メモリ位置が初期化されているかどうか
3. 各ポインタが有効な範囲を指しているかどうか
4. 解放されたメモリへのアクセス
オーバーヘッド:
- 実行速度: 10-50倍遅くなる
- メモリ使用量: 2-3倍増加
検出可能なエラー:
/* 1. メモリリーク */
void memory_leak(void)
{
char *p = malloc(100);
/* freeを忘れている */
}
/* Valgrind: "100 bytes in 1 blocks are definitely lost" */
/* 2. 解放後の使用(Use After Free) */
void use_after_free(void)
{
char *p = malloc(100);
free(p);
p[0] = 'A'; /* 解放済みメモリへのアクセス */
}
/* Valgrind: "Invalid write of size 1" */
/* 3. 二重解放(Double Free) */
void double_free(void)
{
char *p = malloc(100);
free(p);
free(p); /* 二度目の解放 */
}
/* Valgrind: "Invalid free()" */
/* 4. 初期化されていないメモリの使用 */
void uninitialized_read(void)
{
int x;
if (x > 0) /* 初期化されていない */
printf("positive\n");
}
/* Valgrind: "Conditional jump depends on uninitialised value" */
/* 5. バッファオーバーフロー */
void buffer_overflow(void)
{
char *p = malloc(10);
p[10] = 'A'; /* 境界外アクセス */
free(p);
}
/* Valgrind: "Invalid write of size 1" */
Valgrindの使い方:
# 基本的な使用法
valgrind ./test_printf
# 詳細なメモリリークレポート
valgrind --leak-check=full ./test_printf
# すべてのリークタイプを表示
valgrind --leak-check=full --show-leak-kinds=all ./test_printf
# エラーの原因を追跡
valgrind --leak-check=full --track-origins=yes ./test_printf
6.7.4 アサーションとプリコンディション
アサーション(Assertion):
プログラムの特定の地点で、ある条件が真であることを表明する。
#include <assert.h>
/*
assert(condition):
- condition が真なら何もしない
- condition が偽ならプログラムを中断し、エラーメッセージを表示
コンパイル時に NDEBUG を定義すると、assertは無効化される
*/
int ft_strlen(const char *s)
{
int len = 0;
/* プリコンディション: s は NULL であってはならない */
assert(s != NULL);
while (s[len])
len++;
/* ポストコンディション: len は非負である */
assert(len >= 0);
return len;
}
契約による設計(Design by Contract):
Bertrand Meyerが提唱した設計手法で、関数の「契約」を明示的に定義する。
/*
契約の三要素:
1. 事前条件(Precondition):
関数が呼ばれる前に満たされるべき条件
2. 事後条件(Postcondition):
関数が返る時に満たされるべき条件
3. 不変条件(Invariant):
常に満たされるべき条件
*/
int ft_atoi(const char *str)
{
/* 事前条件 */
assert(str != NULL);
int result = 0;
int sign = 1;
int i = 0;
/* 空白をスキップ */
while (str[i] == ' ' || (str[i] >= 9 && str[i] <= 13))
i++;
/* 符号を処理 */
if (str[i] == '-' || str[i] == '+')
{
if (str[i] == '-')
sign = -1;
i++;
}
/* 数字を変換 */
while (str[i] >= '0' && str[i] <= '9')
{
/* 不変条件: result は現在までの変換結果 */
result = result * 10 + (str[i] - '0');
i++;
}
/* 事後条件: なし(atoiは特に保証がない) */
return result * sign;
}
6.8 テスト自動化とCI/CD
6.8.1 継続的インテグレーションの理論
継続的インテグレーション(CI: Continuous Integration):
開発者が頻繁にコードを共有リポジトリに統合し、各統合を自動ビルドとテストで検証する実践。
CIの基本サイクル:
1. 開発者がコードをコミット
↓
2. CIサーバーが変更を検出
↓
3. 自動ビルド開始
↓
4. 自動テスト実行
↓
5. 結果を通知
├─ 成功 → 次の開発者がコミット可能
└─ 失敗 → 開発者に即座に通知
Martin Fowlerの原則(2006):
1. 単一のソースリポジトリを維持する
2. ビルドを自動化する
3. テストを自動化する
4. 全員が毎日メインラインにコミットする
5. すべてのコミットがメインラインでビルドされる
6. 壊れたビルドを即座に修正する
7. テストを高速に保つ
8. 本番環境のクローンでテストする
9. 最新の実行可能ファイルを誰でも取得できるようにする
10. 何が起きているか全員が見えるようにする
6.8.2 自動テストスクリプトの構築
#!/bin/bash
# test_ft_printf.sh
# ft_printfの自動テストスクリプト
# 色の定義
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# 統計
TOTAL=0
PASSED=0
FAILED=0
# ログ関数
log_pass() {
((PASSED++))
((TOTAL++))
echo -e "${GREEN}[PASS]${NC} $1"
}
log_fail() {
((FAILED++))
((TOTAL++))
echo -e "${RED}[FAIL]${NC} $1"
echo " Expected: $2"
echo " Got: $3"
}
# テスト実行関数
run_test() {
local name="$1"
local expected="$2"
local actual="$3"
if [ "$expected" = "$actual" ]; then
log_pass "$name"
else
log_fail "$name" "$expected" "$actual"
fi
}
# コンパイル
echo -e "${YELLOW}=== Compiling ===${NC}"
make re
if [ $? -ne 0 ]; then
echo -e "${RED}Compilation failed${NC}"
exit 1
fi
# テストプログラムをコンパイル
gcc -o test_runner test_runner.c -L. -lftprintf
# テスト実行
echo -e "${YELLOW}=== Running Tests ===${NC}"
# 基本テスト
run_test "Simple string" "Hello" "$(./test_runner 1)"
run_test "Integer positive" "42" "$(./test_runner 2)"
run_test "Integer negative" "-42" "$(./test_runner 3)"
run_test "INT_MIN" "-2147483648" "$(./test_runner 4)"
# 結果サマリー
echo ""
echo -e "${YELLOW}=== Summary ===${NC}"
echo "Total: $TOTAL"
echo -e "${GREEN}Passed: $PASSED${NC}"
if [ $FAILED -gt 0 ]; then
echo -e "${RED}Failed: $FAILED${NC}"
else
echo "Failed: $FAILED"
fi
# 成功率
RATE=$(echo "scale=2; $PASSED * 100 / $TOTAL" | bc)
echo "Success rate: ${RATE}%"
# 終了コード
if [ $FAILED -eq 0 ]; then
exit 0
else
exit 1
fi
6.8.3 Makefileへのテスト統合
# Makefile
NAME = libftprintf.a
CC = gcc
CFLAGS = -Wall -Wextra -Werror
# ソースファイル
SRCS = ft_printf.c ft_print_char.c ft_print_str.c \
ft_print_int.c ft_print_unsigned.c ft_print_hex.c \
ft_print_ptr.c
OBJS = $(SRCS:.c=.o)
# テスト設定
TEST_DIR = tests
TEST_SRCS = $(wildcard $(TEST_DIR)/*.c)
TEST_OBJS = $(TEST_SRCS:.c=.o)
# メインターゲット
all: $(NAME)
$(NAME): $(OBJS)
ar rcs $(NAME) $(OBJS)
# テストターゲット
test: $(NAME)
@echo "Running tests..."
@./test_ft_printf.sh
# 個別テスト
test_char: $(NAME)
$(CC) $(CFLAGS) -o test_char $(TEST_DIR)/test_char.c -L. -lftprintf
./test_char
test_string: $(NAME)
$(CC) $(CFLAGS) -o test_string $(TEST_DIR)/test_string.c -L. -lftprintf
./test_string
test_int: $(NAME)
$(CC) $(CFLAGS) -o test_int $(TEST_DIR)/test_int.c -L. -lftprintf
./test_int
# Valgrind テスト
leak_test: $(NAME)
@echo "Running memory leak tests..."
$(CC) $(CFLAGS) -g -o leak_test $(TEST_DIR)/leak_test.c -L. -lftprintf
valgrind --leak-check=full --show-leak-kinds=all ./leak_test
# カバレッジテスト
coverage: CFLAGS += -fprofile-arcs -ftest-coverage
coverage: clean $(NAME) test
@echo "Generating coverage report..."
gcov $(SRCS)
@echo "Coverage report generated"
# クリーンアップ
clean:
rm -f $(OBJS) $(TEST_OBJS)
rm -f *.gcda *.gcno *.gcov
fclean: clean
rm -f $(NAME)
rm -f test_char test_string test_int leak_test
re: fclean all
.PHONY: all clean fclean re test test_char test_string test_int leak_test coverage
6.9 テストカバレッジの実践
6.9.1 gcovによるカバレッジ測定
# カバレッジ用にコンパイル
gcc -fprofile-arcs -ftest-coverage -o test test.c ft_printf.c
# テストを実行
./test
# カバレッジレポートを生成
gcov ft_printf.c
# 出力例:
# File 'ft_printf.c'
# Lines executed:85.71% of 42
# Creating 'ft_printf.c.gcov'
gcovの出力解釈:
/* ft_printf.c.gcov */
-: 0:Source:ft_printf.c
-: 1:#include "ft_printf.h"
-: 2:
10: 3:int ft_printf(const char *format, ...)
-: 4:{
10: 5: va_list args;
10: 6: int len = 0;
-: 7:
10: 8: if (!format)
#####: 9: return -1; /* 実行されていない */
-: 10:
10: 11: va_start(args, format);
120: 12: while (*format)
-: 13: {
110: 14: if (*format == '%')
-: 15: {
50: 16: len += handle_conversion(&format, args);
-: 17: }
-: 18: else
-: 19: {
60: 20: write(1, format, 1);
60: 21: len++;
60: 22: format++;
-: 23: }
-: 24: }
10: 25: va_end(args);
10: 26: return len;
-: 27:}
/*
凡例:
数字: その行が実行された回数
-: 実行可能なコードがない行(宣言、コメントなど)
#####: 実行されなかった行
*/
6.9.2 カバレッジ改善戦略
/* カバレッジ分析から改善点を特定 */
/*
分析:
- 行9 (format == NULL) が実行されていない
- すべての変換指定子がテストされているか確認
必要なテストの追加:
*/
void test_improve_coverage(void)
{
/* NULL format のテスト */
int ret = ft_printf(NULL);
assert(ret == -1);
/* すべての変換指定子をテスト */
ft_printf("%c", 'A'); /* char */
ft_printf("%s", "test"); /* string */
ft_printf("%d", 42); /* decimal */
ft_printf("%i", -42); /* integer */
ft_printf("%u", 42u); /* unsigned */
ft_printf("%x", 255); /* hex lowercase */
ft_printf("%X", 255); /* hex uppercase */
ft_printf("%p", &ret); /* pointer */
ft_printf("%%"); /* percent */
}
6.10 実践:包括的テストスイートの構築
6.10.1 テストフレームワークの実装
/* test_framework.h */
#ifndef TEST_FRAMEWORK_H
# define TEST_FRAMEWORK_H
# include <stdio.h>
# include <string.h>
# include <stdlib.h>
/* 色定義 */
# define COLOR_GREEN "\033[0;32m"
# define COLOR_RED "\033[0;31m"
# define COLOR_YELLOW "\033[1;33m"
# define COLOR_RESET "\033[0m"
/* テスト統計 */
typedef struct s_test_stats {
int total;
int passed;
int failed;
const char *current_suite;
} t_test_stats;
/* グローバル統計 */
extern t_test_stats g_stats;
/* マクロ定義 */
# define TEST_SUITE(name) \
do { \
g_stats.current_suite = name; \
printf(COLOR_YELLOW "\n=== %s ===\n" COLOR_RESET, name); \
} while(0)
# define ASSERT(condition, message) \
do { \
g_stats.total++; \
if (condition) { \
g_stats.passed++; \
printf(COLOR_GREEN " [PASS] " COLOR_RESET "%s\n", message); \
} else { \
g_stats.failed++; \
printf(COLOR_RED " [FAIL] " COLOR_RESET "%s\n", message); \
printf(" at %s:%d\n", __FILE__, __LINE__); \
} \
} while(0)
# define ASSERT_EQ(actual, expected, message) \
do { \
g_stats.total++; \
if ((actual) == (expected)) { \
g_stats.passed++; \
printf(COLOR_GREEN " [PASS] " COLOR_RESET "%s\n", message); \
} else { \
g_stats.failed++; \
printf(COLOR_RED " [FAIL] " COLOR_RESET "%s\n", message); \
printf(" Expected: %d, Got: %d\n", (expected), (actual)); \
} \
} while(0)
# define ASSERT_STR_EQ(actual, expected, message) \
do { \
g_stats.total++; \
if (strcmp((actual), (expected)) == 0) { \
g_stats.passed++; \
printf(COLOR_GREEN " [PASS] " COLOR_RESET "%s\n", message); \
} else { \
g_stats.failed++; \
printf(COLOR_RED " [FAIL] " COLOR_RESET "%s\n", message); \
printf(" Expected: \"%s\"\n", (expected)); \
printf(" Got: \"%s\"\n", (actual)); \
} \
} while(0)
# define TEST_SUMMARY() \
do { \
printf(COLOR_YELLOW "\n=== Summary ===\n" COLOR_RESET); \
printf("Total: %d\n", g_stats.total); \
printf(COLOR_GREEN "Passed: %d\n" COLOR_RESET, g_stats.passed); \
if (g_stats.failed > 0) \
printf(COLOR_RED "Failed: %d\n" COLOR_RESET, g_stats.failed); \
else \
printf("Failed: 0\n"); \
printf("Rate: %.1f%%\n", \
g_stats.total > 0 ? (float)g_stats.passed / g_stats.total * 100 : 0); \
} while(0)
#endif /* TEST_FRAMEWORK_H */
/* test_framework.c */
#include "test_framework.h"
t_test_stats g_stats = {0, 0, 0, NULL};
6.10.2 包括的テストスイート
/* test_ft_printf.c */
#include "test_framework.h"
#include "ft_printf.h"
#include <limits.h>
/*
=== テスト戦略 ===
1. 等価クラス分割に基づくテスト
2. 境界値分析に基づくテスト
3. 標準printfとの出力比較
4. エッジケースの網羅
*/
/* 文字 (%c) のテスト */
void test_char(void)
{
TEST_SUITE("Character (%c)");
/* 等価クラス: 印刷可能文字 */
ASSERT_EQ(ft_printf("%c", 'A'), 1, "Printable char 'A'");
ASSERT_EQ(ft_printf("%c", 'z'), 1, "Printable char 'z'");
ASSERT_EQ(ft_printf("%c", '0'), 1, "Printable char '0'");
/* 等価クラス: 制御文字 */
ASSERT_EQ(ft_printf("%c", '\n'), 1, "Control char newline");
ASSERT_EQ(ft_printf("%c", '\t'), 1, "Control char tab");
/* 境界値 */
ASSERT_EQ(ft_printf("%c", 0), 1, "NULL character (0)");
ASSERT_EQ(ft_printf("%c", 127), 1, "DEL character (127)");
/* 複数文字 */
ASSERT_EQ(ft_printf("%c%c%c", 'A', 'B', 'C'), 3, "Multiple chars");
}
/* 文字列 (%s) のテスト */
void test_string(void)
{
TEST_SUITE("String (%s)");
/* 等価クラス: 通常の文字列 */
ASSERT_EQ(ft_printf("%s", "Hello"), 5, "Normal string");
ASSERT_EQ(ft_printf("%s", "World"), 5, "Normal string 2");
/* 等価クラス: 空文字列 */
ASSERT_EQ(ft_printf("%s", ""), 0, "Empty string");
/* 等価クラス: NULL */
/* 注意: NULLの扱いは実装依存 */
int ret = ft_printf("%s", NULL);
ASSERT(ret == 6 || ret == -1, "NULL string handling");
/* 境界値: 1文字 */
ASSERT_EQ(ft_printf("%s", "A"), 1, "Single char string");
}
/* 整数 (%d, %i) のテスト */
void test_integer(void)
{
TEST_SUITE("Integer (%d, %i)");
/* 等価クラス: 正の数 */
ASSERT_EQ(ft_printf("%d", 42), 2, "Positive: 42");
ASSERT_EQ(ft_printf("%d", 1), 1, "Positive: 1");
ASSERT_EQ(ft_printf("%d", 12345), 5, "Positive: 12345");
/* 等価クラス: ゼロ */
ASSERT_EQ(ft_printf("%d", 0), 1, "Zero");
/* 等価クラス: 負の数 */
ASSERT_EQ(ft_printf("%d", -42), 3, "Negative: -42");
ASSERT_EQ(ft_printf("%d", -1), 2, "Negative: -1");
/* 境界値 */
ASSERT_EQ(ft_printf("%d", INT_MAX), 10, "INT_MAX");
ASSERT_EQ(ft_printf("%d", INT_MIN), 11, "INT_MIN");
/* 桁数の境界 */
ASSERT_EQ(ft_printf("%d", 9), 1, "Boundary: 9");
ASSERT_EQ(ft_printf("%d", 10), 2, "Boundary: 10");
ASSERT_EQ(ft_printf("%d", 99), 2, "Boundary: 99");
ASSERT_EQ(ft_printf("%d", 100), 3, "Boundary: 100");
/* %i も同様 */
ASSERT_EQ(ft_printf("%i", 42), 2, "%i: 42");
ASSERT_EQ(ft_printf("%i", -42), 3, "%i: -42");
}
/* 符号なし整数 (%u) のテスト */
void test_unsigned(void)
{
TEST_SUITE("Unsigned Integer (%u)");
/* 基本ケース */
ASSERT_EQ(ft_printf("%u", 42), 2, "Unsigned: 42");
ASSERT_EQ(ft_printf("%u", 0), 1, "Unsigned: 0");
/* 境界値 */
ASSERT_EQ(ft_printf("%u", UINT_MAX), 10, "UINT_MAX");
/* 負の値を渡した場合(unsigned として解釈) */
ASSERT_EQ(ft_printf("%u", -1), 10, "-1 as unsigned");
}
/* 16進数 (%x, %X) のテスト */
void test_hex(void)
{
TEST_SUITE("Hexadecimal (%x, %X)");
/* 基本ケース */
ASSERT_EQ(ft_printf("%x", 255), 2, "hex: 255 -> ff");
ASSERT_EQ(ft_printf("%X", 255), 2, "HEX: 255 -> FF");
/* ゼロ */
ASSERT_EQ(ft_printf("%x", 0), 1, "hex: 0");
/* 境界値 */
ASSERT_EQ(ft_printf("%x", UINT_MAX), 8, "hex: UINT_MAX");
/* 特定のパターン */
ASSERT_EQ(ft_printf("%x", 0xdeadbeef), 8, "hex: deadbeef");
ASSERT_EQ(ft_printf("%X", 0xdeadbeef), 8, "HEX: DEADBEEF");
}
/* ポインタ (%p) のテスト */
void test_pointer(void)
{
TEST_SUITE("Pointer (%p)");
int x = 42;
/* NULLポインタ */
int ret = ft_printf("%p", NULL);
ASSERT(ret > 0, "NULL pointer output length");
/* 有効なポインタ */
ret = ft_printf("%p", &x);
ASSERT(ret > 0, "Valid pointer output length");
}
/* パーセント (%%) のテスト */
void test_percent(void)
{
TEST_SUITE("Percent (%%)");
ASSERT_EQ(ft_printf("%%"), 1, "Single percent");
ASSERT_EQ(ft_printf("100%%"), 4, "100%");
ASSERT_EQ(ft_printf("%%%%"), 2, "Double percent");
}
/* 混合テスト */
void test_mixed(void)
{
TEST_SUITE("Mixed Formats");
int ret;
ret = ft_printf("Hello %s!", "World");
ASSERT_EQ(ret, 12, "Hello World!");
ret = ft_printf("%d + %d = %d", 2, 3, 5);
ASSERT_EQ(ret, 9, "2 + 3 = 5");
ret = ft_printf("%c%c%c", 'A', 'B', 'C');
ASSERT_EQ(ret, 3, "ABC");
}
/* メイン関数 */
int main(void)
{
printf(COLOR_YELLOW "=== ft_printf Test Suite ===\n" COLOR_RESET);
test_char();
test_string();
test_integer();
test_unsigned();
test_hex();
test_pointer();
test_percent();
test_mixed();
TEST_SUMMARY();
return (g_stats.failed == 0) ? 0 : 1;
}
まとめ
本章では、ソフトウェアテストの理論的基盤から実践的な技法まで、包括的に学習した。
理論的基盤:
テスト手法:
デバッグ技術:
ft_printfへの適用:
ソフトウェアテストは、単なるバグ発見の手段ではなく、ソフトウェアの品質を体系的に保証するための科学である。本章で学んだ理論と技法は、ft_printfに限らず、あらゆるソフトウェア開発において適用可能な普遍的な知識である。