評価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);
    }
    

    学習の手引き

    テスト作成の基礎

  • テストの構造を理解
- Arrange(準備): テストデータの準備 - Act(実行): テスト対象の実行 - Assert(検証): 結果の検証

  • エッジケースの考え方
- 境界値(0、最小値、最大値) - 空の状態 - 無効な入力

  • エラーケースのテスト
- expectError を使用 - すべてのエラータイプを網羅

効果的な学習方法

  • TDD(Test-Driven Development)
- 先にテストを書く - 実装は最小限から始める - リファクタリングで改善

  • カバレッジの意識
- すべてのパスをテスト - すべての分岐をカバー

  • テストの可読性
- 明確なテスト名 - 1テスト1検証を心がける

ピアレビューのポイント

評価者は以下の点を確認してください:

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. 理解度の確認

質問例:
  • 「このテストで何を確認していますか?」
  • 「エッジケースとして何を考慮しましたか?」
  • 「メモリリークをどう防いでいますか?」
  • フィードバックの例

    良いフィードバック

    > 「テストの実装が非常に包括的です。単体テストでは正常系だけでなく、空文字列や空配列などのエッジケースもしっかりカバーされています。テーブル駆動テストも効率的に実装されており、テストケースの追加が容易な設計です。メモリリーク検出テストも、testing.allocatorを正しく使用しており、すべてのテストでリークがありません。」

    改善が必要な場合のフィードバック

    > 「基本的なテストは良いですが、findMax関数で空配列のケースがテストされていません。また、Stack のテストでオーバーフロー時のエラーハンドリングが不足しています。StringListのテストでメモリリークが発生しているため、deinit関数の実装を確認し、すべての文字列を解放するように修正してください。」

    参考資料

  • 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

まとめ

効果的なテストは、ソフトウェアの品質を保証する重要な要素です。この評価を通じて、単体テスト、テーブル駆動テスト、メモリリーク検出など、実践的なテスト手法を習得しました。これらのスキルは、信頼性の高いソフトウェアを開発するために不可欠です。テストを通じて、コードの正確性とメモリ安全性を確保できるようになりましょう。