評価13: テストの実践 - 評価基準
評価の目的
この評価では、Zigの組み込みテストフレームワークを使った効果的なテスト作成能力を確認します。単体テスト、データ構造のテスト、テーブル駆動テスト、メモリリーク検出など、実践的なテスト手法を評価します。
学習目標
この評価を通じて、以下の能力を確認します:
- test ブロックを使った単体テストの作成
- データ構造の包括的なテストケース設計
- テーブル駆動テストによる効率的なテスト
- std.testing アロケータによるメモリリーク検出
- エッジケースとエラーケースのテスト
評価項目
1. 基本的な単体テスト(20点)
| 項目 | 配点 | 評価基準 |
|---|---|---|
| 文字列操作のテスト | 5点 | reverseString が正しくテストされている |
| 配列操作のテスト | 5点 | findMax が境界条件を含めてテスト |
| 文字列判定のテスト | 5点 | isPalindrome が正負両方のケースをテスト |
| 数値計算のテスト | 5点 | factorial、fibonacci が複数のケースでテスト |
確認ポイント:
- [ ] すべてのテストが通る
- [ ] 正常系と異常系の両方をテスト
- [ ] エッジケース(空文字列、0、負数など)を考慮
- [ ] testing.expectEqual などを正しく使用
良いテスト例:
test "reverseString - various cases" {
// 通常のケース
{
const result = try reverseString(std.testing.allocator, "hello");
defer std.testing.allocator.free(result);
try std.testing.expectEqualStrings("olleh", result);
}
// エッジケース:空文字列
{
const result = try reverseString(std.testing.allocator, "");
defer std.testing.allocator.free(result);
try std.testing.expectEqualStrings("", result);
}
// エッジケース:1文字
{
const result = try reverseString(std.testing.allocator, "a");
defer std.testing.allocator.free(result);
try std.testing.expectEqualStrings("a", result);
}
}
// 不十分なテスト:エッジケースの考慮不足
test "reverseString - basic only" {
const result = try reverseString(std.testing.allocator, "hello");
defer std.testing.allocator.free(result);
try std.testing.expectEqualStrings("olleh", result);
// 空文字列や1文字のケースがない
}
2. データ構造のテスト(20点)
| 項目 | 配点 | 評価基準 |
|---|---|---|
| 基本操作のテスト | 6点 | push、pop、peek が正しく動作することを確認 |
| 空状態のテスト | 6点 | 空スタックのpopが適切に処理される |
| 容量制限のテスト | 4点 | オーバーフロー時にエラーが返される |
| 型の汎用性テスト | 4点 | 異なる型でスタックが動作する |
確認ポイント:
- [ ] 初期状態のテスト(isEmpty、size)
- [ ] 操作の順序性テスト(LIFO の確認)
- [ ] 境界条件のテスト(満杯、空)
- [ ] メモリリークがない
包括的なテスト例:
test "Stack - comprehensive test" {
var stack = try Stack(i32).init(std.testing.allocator, 10);
defer stack.deinit();
// 初期状態の確認
try std.testing.expect(stack.isEmpty());
try std.testing.expectEqual(@as(usize, 0), stack.size());
// push操作
try stack.push(1);
try stack.push(2);
try stack.push(3);
// サイズと空状態の確認
try std.testing.expectEqual(@as(usize, 3), stack.size());
try std.testing.expect(!stack.isEmpty());
// peek操作(値を削除しない)
try std.testing.expectEqual(@as(?i32, 3), stack.peek());
try std.testing.expectEqual(@as(usize, 3), stack.size());
// pop操作(LIFO の確認)
try std.testing.expectEqual(@as(?i32, 3), stack.pop());
try std.testing.expectEqual(@as(?i32, 2), stack.pop());
try std.testing.expectEqual(@as(?i32, 1), stack.pop());
// 空になったことを確認
try std.testing.expect(stack.isEmpty());
try std.testing.expectEqual(@as(?i32, null), stack.pop());
}
test "Stack - overflow" {
var stack = try Stack(i32).init(std.testing.allocator, 2);
defer stack.deinit();
try stack.push(1);
try stack.push(2);
// 容量超過時のエラー
try std.testing.expectError(error.StackOverflow, stack.push(3));
}
3. テーブル駆動テスト(20点)
| 項目 | 配点 | 評価基準 |
|---|---|---|
| intToString のテスト | 6点 | 複数のケースをテーブルで効率的にテスト |
| stringToInt のテスト | 6点 | 正負、ゼロなど網羅的にテスト |
| calculate のテスト | 6点 | 四則演算すべてをテーブルでテスト |
| エラーケース | 2点 | ゼロ除算などのエラーを適切にテスト |
確認ポイント:
- [ ] テストケースが構造体の配列として定義されている
- [ ] for ループで効率的にテスト
- [ ] テストケースが十分に網羅的
- [ ] 失敗時のメッセージが分かりやすい
効果的なテーブル駆動テスト:
test "calculate - table driven" {
const TestCase = struct {
op: Operation,
a: i32,
b: i32,
expected: i32,
};
const test_cases = [_]TestCase{
// 加算
.{ .op = .Add, .a = 2, .b = 3, .expected = 5 },
.{ .op = .Add, .a = -2, .b = 3, .expected = 1 },
.{ .op = .Add, .a = 0, .b = 0, .expected = 0 },
// 減算
.{ .op = .Subtract, .a = 5, .b = 3, .expected = 2 },
.{ .op = .Subtract, .a = 3, .b = 5, .expected = -2 },
// 乗算
.{ .op = .Multiply, .a = 4, .b = 5, .expected = 20 },
.{ .op = .Multiply, .a = -4, .b = 5, .expected = -20 },
.{ .op = .Multiply, .a = 0, .b = 100, .expected = 0 },
// 除算
.{ .op = .Divide, .a = 10, .b = 2, .expected = 5 },
.{ .op = .Divide, .a = -10, .b = 2, .expected = -5 },
};
for (test_cases) |tc| {
const result = try calculate(tc.op, tc.a, tc.b);
try std.testing.expectEqual(tc.expected, result);
}
}
test "calculate - division by zero" {
try std.testing.expectError(
error.DivisionByZero,
calculate(.Divide, 10, 0)
);
}
4. メモリリーク検出(20点)
| 項目 | 配点 | 評価基準 |
|---|---|---|
| StringList の実装 | 8点 | init、deinit、append が正しく実装 |
| リークのないテスト | 8点 | testing.allocator で リークがない |
| 複数操作のテスト | 4点 | 多数の文字列でもリークがない |
確認ポイント:
- [ ] std.testing.allocator を使用
- [ ] すべてのアロケーションに対応する解放がある
- [ ] defer を適切に配置
- [ ] テストが成功する
リーク検出テストの例:
test "StringList - no memory leaks" {
var list = StringList.init(std.testing.allocator);
defer list.deinit(); // これが正しく動作することを確認
try list.append("hello");
try list.append("world");
try list.append("!");
try std.testing.expectEqual(@as(usize, 3), list.len());
// testing.allocator が自動的にリークを検出
// deinit が正しく実装されていればテストが通る
}
test "StringList - stress test" {
var list = StringList.init(std.testing.allocator);
defer list.deinit();
// 多数の文字列を追加
var i: usize = 0;
while (i < 100) : (i += 1) {
const str = try std.fmt.allocPrint(
std.testing.allocator,
"item{}",
.{i}
);
defer std.testing.allocator.free(str);
try list.append(str);
}
try std.testing.expectEqual(@as(usize, 100), list.len());
// すべて解放されることを確認
}
5. ボーナス課題(20点)
| 項目 | 配点 | 評価基準 |
|---|---|---|
| プロパティベーステスト | 10点 | ランダム入力でソートの性質を検証 |
| モックとテストダブル | 10点 | インターフェースを使ったモックの実装 |
ボーナス評価ポイント:
- [ ] 複数の性質(ソート済み、要素同一、長さ保持)を検証
- [ ] モックで呼び出し回数などを追跡
- [ ] テストが分離され、依存関係がない
チェックリスト
テスト作成前の確認
- [ ] テスト対象の仕様を理解している
- [ ] エッジケースを洗い出している
- [ ] エラーケースを特定している
- [ ] 必要なテストケース数を見積もっている
テスト実行の確認
- [ ] すべてのテストが通る
- [ ]
zig testでメモリリークが検出されない - [ ] テストの実行時間が適切
- [ ] テストカバレッジが十分
テストコードの品質確認
- [ ] テスト名が内容を表している
- [ ] テストが独立している(順序に依存しない)
- [ ] テストが読みやすい
- [ ] テストのメンテナンスが容易
合格基準
| レベル | 点数 | 説明 |
|---|---|---|
| 優秀 | 90-100 | すべてのテストを完璧に実装、ボーナス課題も完了 |
| 良好 | 80-89 | マンダトリー課題を完全実装、ボーナス課題の一部完了 |
| 合格 | 64-79 | マンダトリー課題の80%以上を正しく実装 |
| 要再提出 | 0-63 | テストが不十分またはメモリリークあり |
最低合格ライン: 64点以上(マンダトリー80点満点の80%)
よくある減点ポイント
1. エッジケースの考慮不足(-5〜10点)
// NG: 正常系のみ
test "findMax" {
const numbers = [_]i32{ 3, 7, 2, 9, 1 };
try std.testing.expectEqual(@as(?i32, 9), findMax(&numbers));
// 空配列のケースがない
}
// OK: エッジケースも含む
test "findMax - with edge cases" {
// 正常系
const numbers = [_]i32{ 3, 7, 2, 9, 1 };
try std.testing.expectEqual(@as(?i32, 9), findMax(&numbers));
// エッジケース:空配列
const empty = [_]i32{};
try std.testing.expectEqual(@as(?i32, null), findMax(&empty));
// エッジケース:1要素
const single = [_]i32{42};
try std.testing.expectEqual(@as(?i32, 42), findMax(&single));
}
2. メモリリーク(-10〜20点)
// NG: defer が抜けている
test "reverseString" {
const result = try reverseString(std.testing.allocator, "hello");
// defer allocator.free(result); がない -> リーク
try std.testing.expectEqualStrings("olleh", result);
}
// OK: 正しく解放
test "reverseString" {
const result = try reverseString(std.testing.allocator, "hello");
defer std.testing.allocator.free(result); // 必須
try std.testing.expectEqualStrings("olleh", result);
}
3. テストの独立性の欠如(-5点)
// NG: グローバル状態に依存
var global_counter: i32 = 0;
test "test1" {
global_counter += 1;
try std.testing.expectEqual(@as(i32, 1), global_counter);
}
test "test2" {
// test1の実行順序に依存してしまう
try std.testing.expectEqual(@as(i32, 1), global_counter);
}
// OK: テストが独立
test "test1" {
var counter: i32 = 0;
counter += 1;
try std.testing.expectEqual(@as(i32, 1), counter);
}
test "test2" {
var counter: i32 = 0;
try std.testing.expectEqual(@as(i32, 0), counter);
}
学習の手引き
テスト作成の基礎
- エッジケースの考え方
- エラーケースのテスト
効果的な学習方法
- TDD(Test-Driven Development)
- カバレッジの意識
- テストの可読性
ピアレビューのポイント
評価者は以下の点を確認してください:
1. テスト実行
# すべてのテストを実行
zig test part1/basic_tests.zig
zig test part2/stack_tests.zig
zig test part3/table_driven_tests.zig
zig test part4/leak_detection_tests.zig
# メモリリークの確認
# testing.allocatorが自動検出
2. コード確認
- テストケースの網羅性
- メモリリークの有無
- テストの独立性
- エラーハンドリング
3. 理解度の確認
質問例:- 「このテストで何を確認していますか?」
- 「エッジケースとして何を考慮しましたか?」
- 「メモリリークをどう防いでいますか?」
- Zig Language Reference - Testing: https://ziglang.org/documentation/master/#Testing
- Zig Standard Library - Testing: https://ziglang.org/documentation/master/std/#std.testing
- Ziglearn - Testing: https://ziglearn.org/chapter-2/#testing
- Test-Driven Development: https://en.wikipedia.org/wiki/Test-driven_development
フィードバックの例
良いフィードバック
> 「テストの実装が非常に包括的です。単体テストでは正常系だけでなく、空文字列や空配列などのエッジケースもしっかりカバーされています。テーブル駆動テストも効率的に実装されており、テストケースの追加が容易な設計です。メモリリーク検出テストも、testing.allocatorを正しく使用しており、すべてのテストでリークがありません。」
改善が必要な場合のフィードバック
> 「基本的なテストは良いですが、findMax関数で空配列のケースがテストされていません。また、Stack のテストでオーバーフロー時のエラーハンドリングが不足しています。StringListのテストでメモリリークが発生しているため、deinit関数の実装を確認し、すべての文字列を解放するように修正してください。」
参考資料
まとめ
効果的なテストは、ソフトウェアの品質を保証する重要な要素です。この評価を通じて、単体テスト、テーブル駆動テスト、メモリリーク検出など、実践的なテスト手法を習得しました。これらのスキルは、信頼性の高いソフトウェアを開発するために不可欠です。テストを通じて、コードの正確性とメモリ安全性を確保できるようになりましょう。