課題13: テストの実践

マンダトリー要件 (80点)

Part 1: 基本的な単体テスト (20点)

様々な関数の単体テストを実装してください。

ファイル: part1/basic_tests.zig

const std = @import("std");

// TODO: 文字列を反転する関数
fn reverseString(allocator: std.mem.Allocator, str: []const u8) ![]u8 {
    // 実装
}

// TODO: 配列の最大値を返す関数
fn findMax(numbers: []const i32) ?i32 {
    // 実装
}

// TODO: 文字列が回文か判定する関数
fn isPalindrome(str: []const u8) bool {
    // 実装
}

// TODO: 階乗を計算する関数
fn factorial(n: u32) u64 {
    // 実装
}

// TODO: フィボナッチ数を計算する関数
fn fibonacci(n: u32) u64 {
    // 実装
}

// ===== テスト =====

test "reverseString" {
    const result = try reverseString(std.testing.allocator, "hello");
    defer std.testing.allocator.free(result);

    try std.testing.expectEqualStrings("olleh", result);
}

test "reverseString - empty string" {
    const result = try reverseString(std.testing.allocator, "");
    defer std.testing.allocator.free(result);

    try std.testing.expectEqualStrings("", result);
}

test "findMax - normal case" {
    const numbers = [_]i32{ 3, 7, 2, 9, 1 };
    const max = findMax(&numbers);

    try std.testing.expectEqual(@as(?i32, 9), max);
}

test "findMax - empty array" {
    const numbers = [_]i32{};
    const max = findMax(&numbers);

    try std.testing.expectEqual(@as(?i32, null), max);
}

test "isPalindrome - true cases" {
    try std.testing.expect(isPalindrome("racecar"));
    try std.testing.expect(isPalindrome("a"));
    try std.testing.expect(isPalindrome(""));
}

test "isPalindrome - false cases" {
    try std.testing.expect(!isPalindrome("hello"));
    try std.testing.expect(!isPalindrome("ab"));
}

test "factorial" {
    try std.testing.expectEqual(@as(u64, 1), factorial(0));
    try std.testing.expectEqual(@as(u64, 1), factorial(1));
    try std.testing.expectEqual(@as(u64, 2), factorial(2));
    try std.testing.expectEqual(@as(u64, 6), factorial(3));
    try std.testing.expectEqual(@as(u64, 24), factorial(4));
    try std.testing.expectEqual(@as(u64, 120), factorial(5));
}

test "fibonacci" {
    try std.testing.expectEqual(@as(u64, 0), fibonacci(0));
    try std.testing.expectEqual(@as(u64, 1), fibonacci(1));
    try std.testing.expectEqual(@as(u64, 1), fibonacci(2));
    try std.testing.expectEqual(@as(u64, 2), fibonacci(3));
    try std.testing.expectEqual(@as(u64, 3), fibonacci(4));
    try std.testing.expectEqual(@as(u64, 5), fibonacci(5));
    try std.testing.expectEqual(@as(u64, 8), fibonacci(6));
}

Part 2: データ構造のテスト (20点)

スタックデータ構造とそのテストを実装してください。

ファイル: part2/stack_tests.zig

const std = @import("std");

fn Stack(comptime T: type) type {
    return struct {
        items: []T,
        len: usize,
        allocator: std.mem.Allocator,

        const Self = @This();

        // TODO: 初期化
        pub fn init(allocator: std.mem.Allocator, capacity: usize) !Self {
            // 実装
        }

        // TODO: 解放
        pub fn deinit(self: *Self) void {
            // 実装
        }

        // TODO: プッシュ
        pub fn push(self: *Self, item: T) !void {
            // 実装
        }

        // TODO: ポップ
        pub fn pop(self: *Self) ?T {
            // 実装
        }

        // TODO: ピーク
        pub fn peek(self: *Self) ?T {
            // 実装
        }

        // TODO: サイズ取得
        pub fn size(self: Self) usize {
            return self.len;
        }

        // TODO: 空か判定
        pub fn isEmpty(self: Self) bool {
            return self.len == 0;
        }
    };
}

// ===== テスト =====

test "Stack - basic operations" {
    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());

    // プッシュ
    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());

    // ピーク
    try std.testing.expectEqual(@as(?i32, 3), stack.peek());
    try std.testing.expectEqual(@as(usize, 3), stack.size());

    // ポップ
    try std.testing.expectEqual(@as(?i32, 3), stack.pop());
    try std.testing.expectEqual(@as(?i32, 2), stack.pop());
    try std.testing.expectEqual(@as(usize, 1), stack.size());
}

test "Stack - empty pop" {
    var stack = try Stack(i32).init(std.testing.allocator, 10);
    defer stack.deinit();

    try std.testing.expectEqual(@as(?i32, null), stack.pop());
}

test "Stack - capacity 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));
}

test "Stack - different types" {
    var int_stack = try Stack(i32).init(std.testing.allocator, 5);
    defer int_stack.deinit();

    try int_stack.push(42);
    try std.testing.expectEqual(@as(?i32, 42), int_stack.pop());

    var bool_stack = try Stack(bool).init(std.testing.allocator, 5);
    defer bool_stack.deinit();

    try bool_stack.push(true);
    try std.testing.expectEqual(@as(?bool, true), bool_stack.pop());
}

Part 3: テーブル駆動テスト (20点)

テーブル駆動テストを使って複雑なロジックをテストしてください。

ファイル: part3/table_driven_tests.zig

const std = @import("std");

// TODO: 整数を文字列に変換
fn intToString(allocator: std.mem.Allocator, value: i32) ![]u8 {
    // 実装
    // ヒント: std.fmt.allocPrint を使用
}

// TODO: 文字列を整数に変換
fn stringToInt(str: []const u8) !i32 {
    // 実装
    // ヒント: std.fmt.parseInt を使用
}

// TODO: 簡単な計算機
const Operation = enum {
    Add,
    Subtract,
    Multiply,
    Divide,
};

fn calculate(op: Operation, a: i32, b: i32) !i32 {
    return switch (op) {
        .Add => a + b,
        .Subtract => a - b,
        .Multiply => a * b,
        .Divide => {
            if (b == 0) return error.DivisionByZero;
            return @divTrunc(a, b);
        },
    };
}

// ===== テスト =====

test "intToString - table driven" {
    const test_cases = [_]struct {
        input: i32,
        expected: []const u8,
    }{
        .{ .input = 0, .expected = "0" },
        .{ .input = 1, .expected = "1" },
        .{ .input = 42, .expected = "42" },
        .{ .input = -1, .expected = "-1" },
        .{ .input = -42, .expected = "-42" },
        .{ .input = 123456, .expected = "123456" },
    };

    for (test_cases) |tc| {
        const result = try intToString(std.testing.allocator, tc.input);
        defer std.testing.allocator.free(result);

        try std.testing.expectEqualStrings(tc.expected, result);
    }
}

test "stringToInt - table driven" {
    const test_cases = [_]struct {
        input: []const u8,
        expected: i32,
    }{
        .{ .input = "0", .expected = 0 },
        .{ .input = "1", .expected = 1 },
        .{ .input = "42", .expected = 42 },
        .{ .input = "-1", .expected = -1 },
        .{ .input = "-42", .expected = -42 },
        .{ .input = "123456", .expected = 123456 },
    };

    for (test_cases) |tc| {
        const result = try stringToInt(tc.input);
        try std.testing.expectEqual(tc.expected, result);
    }
}

test "calculate - table driven" {
    const test_cases = [_]struct {
        op: Operation,
        a: i32,
        b: i32,
        expected: i32,
    }{
        .{ .op = .Add, .a = 2, .b = 3, .expected = 5 },
        .{ .op = .Add, .a = -2, .b = 3, .expected = 1 },
        .{ .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 = .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));
}

Part 4: メモリリーク検出 (20点)

テストアロケータを使ってメモリリークを検出してください。

ファイル: part4/leak_detection_tests.zig

const std = @import("std");

const StringList = struct {
    items: std.ArrayList([]u8),
    allocator: std.mem.Allocator,

    // TODO: 初期化
    pub fn init(allocator: std.mem.Allocator) StringList {
        // 実装
    }

    // TODO: 解放
    pub fn deinit(self: *StringList) void {
        // 実装(すべての文字列を解放)
    }

    // TODO: 文字列を追加
    pub fn append(self: *StringList, str: []const u8) !void {
        // 実装(文字列をコピーして保存)
    }

    // TODO: インデックスで取得
    pub fn get(self: StringList, index: usize) ?[]const u8 {
        // 実装
    }

    // TODO: サイズ取得
    pub fn len(self: StringList) usize {
        return self.items.items.len;
    }
};

// ===== テスト =====

test "StringList - no 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());
    try std.testing.expectEqualStrings("hello", list.get(0).?);
    try std.testing.expectEqualStrings("world", list.get(1).?);
}

test "StringList - empty list" {
    var list = StringList.init(std.testing.allocator);
    defer list.deinit();

    try std.testing.expectEqual(@as(usize, 0), list.len());
    try std.testing.expectEqual(@as(?[]const u8, null), list.get(0));
}

test "StringList - multiple operations" {
    var list = StringList.init(std.testing.allocator);
    defer list.deinit();

    // 10個の文字列を追加
    var i: usize = 0;
    while (i < 10) : (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, 10), list.len());

    // すべての文字列を確認
    i = 0;
    while (i < 10) : (i += 1) {
        const expected = try std.fmt.allocPrint(
            std.testing.allocator,
            "item{}",
            .{i}
        );
        defer std.testing.allocator.free(expected);

        try std.testing.expectEqualStrings(expected, list.get(i).?);
    }
}

ボーナス課題 (20点)

Bonus 1: プロパティベーステスト (10点)

プロパティベーステストを実装してください。

ファイル: bonus1/property_tests.zig

const std = @import("std");

// TODO: クイックソート実装
fn quickSort(items: []i32) void {
    if (items.len <= 1) return;

    const pivot = items[items.len / 2];
    var i: usize = 0;
    var j: usize = items.len - 1;

    while (i <= j) {
        while (items[i] < pivot) : (i += 1) {}
        while (items[j] > pivot) : (j -= 1) {}

        if (i <= j) {
            const temp = items[i];
            items[i] = items[j];
            items[j] = temp;
            i += 1;
            if (j == 0) break;
            j -= 1;
        }
    }

    if (j > 0) quickSort(items[0..j + 1]);
    if (i < items.len) quickSort(items[i..]);
}

// TODO: ソート済みか確認
fn isSorted(items: []const i32) bool {
    // 実装
}

// TODO: 要素が同じか確認
fn sameElements(a: []const i32, b: []const i32) bool {
    if (a.len != b.len) return false;

    var a_copy = std.ArrayList(i32).init(std.testing.allocator);
    defer a_copy.deinit();
    var b_copy = std.ArrayList(i32).init(std.testing.allocator);
    defer b_copy.deinit();

    a_copy.appendSlice(a) catch return false;
    b_copy.appendSlice(b) catch return false;

    // 両方ソートして比較
    // 実装

    return true;
}

// ===== テスト =====

test "quickSort - property: result is sorted" {
    var prng = std.rand.DefaultPrng.init(42);
    const random = prng.random();

    var test_count: usize = 0;
    while (test_count < 100) : (test_count += 1) {
        var numbers: [20]i32 = undefined;
        for (&numbers) |*num| {
            num.* = random.intRangeAtMost(i32, -1000, 1000);
        }

        const original = numbers;
        quickSort(&numbers);

        // プロパティ1: ソート済み
        try std.testing.expect(isSorted(&numbers));

        // プロパティ2: 要素が同じ
        try std.testing.expect(sameElements(&original, &numbers));

        // プロパティ3: 長さが同じ
        try std.testing.expectEqual(original.len, numbers.len);
    }
}

test "quickSort - property: already sorted" {
    var numbers = [_]i32{ 1, 2, 3, 4, 5 };
    const original = numbers;

    quickSort(&numbers);

    try std.testing.expectEqualSlices(i32, &original, &numbers);
}

test "quickSort - property: reverse sorted" {
    var numbers = [_]i32{ 5, 4, 3, 2, 1 };

    quickSort(&numbers);

    try std.testing.expectEqualSlices(i32, &[_]i32{ 1, 2, 3, 4, 5 }, &numbers);
}

Bonus 2: モックとテストダブル (10点)

モックオブジェクトを使ったテストを実装してください。

ファイル: bonus2/mock_tests.zig

const std = @import("std");

// インターフェース
const Storage = struct {
    ptr: *anyopaque,
    vtable: *const VTable,

    const VTable = struct {
        save: *const fn(*anyopaque, []const u8, []const u8) anyerror!void,
        load: *const fn(*anyopaque, []const u8) anyerror![]const u8,
    };

    pub fn save(self: Storage, key: []const u8, value: []const u8) !void {
        return self.vtable.save(self.ptr, key, value);
    }

    pub fn load(self: Storage, key: []const u8) ![]const u8 {
        return self.vtable.load(self.ptr, key);
    }
};

// TODO: モック実装
const MockStorage = struct {
    data: std.StringHashMap([]const u8),
    save_count: usize,
    load_count: usize,

    fn init(allocator: std.mem.Allocator) MockStorage {
        return MockStorage{
            .data = std.StringHashMap([]const u8).init(allocator),
            .save_count = 0,
            .load_count = 0,
        };
    }

    fn deinit(self: *MockStorage) void {
        self.data.deinit();
    }

    fn storage(self: *MockStorage) Storage {
        return Storage{
            .ptr = self,
            .vtable = &.{
                .save = save,
                .load = load,
            },
        };
    }

    fn save(ptr: *anyopaque, key: []const u8, value: []const u8) !void {
        const self: *MockStorage = @ptrCast(@alignCast(ptr));
        self.save_count += 1;
        try self.data.put(key, value);
    }

    fn load(ptr: *anyopaque, key: []const u8) ![]const u8 {
        const self: *MockStorage = @ptrCast(@alignCast(ptr));
        self.load_count += 1;
        return self.data.get(key) orelse error.KeyNotFound;
    }
};

// TODO: ストレージを使うサービス
const UserService = struct {
    storage: Storage,

    pub fn saveUser(self: UserService, id: []const u8, name: []const u8) !void {
        try self.storage.save(id, name);
    }

    pub fn getUser(self: UserService, id: []const u8) ![]const u8 {
        return try self.storage.load(id);
    }
};

// ===== テスト =====

test "UserService with mock storage" {
    var mock = MockStorage.init(std.testing.allocator);
    defer mock.deinit();

    const service = UserService{ .storage = mock.storage() };

    // ユーザーを保存
    try service.saveUser("1", "Alice");
    try service.saveUser("2", "Bob");

    // 保存された回数を確認
    try std.testing.expectEqual(@as(usize, 2), mock.save_count);

    // ユーザーを取得
    const user1 = try service.getUser("1");
    const user2 = try service.getUser("2");

    try std.testing.expectEqualStrings("Alice", user1);
    try std.testing.expectEqualStrings("Bob", user2);

    // ロードされた回数を確認
    try std.testing.expectEqual(@as(usize, 2), mock.load_count);

    // 存在しないユーザー
    try std.testing.expectError(error.KeyNotFound, service.getUser("999"));
}

評価基準

項目 配点
Part 1: 基本的な単体テスト 20点
Part 2: データ構造のテスト 20点
Part 3: テーブル駆動テスト 20点
Part 4: メモリリーク検出 20点
**マンダトリー合計** **80点**
Bonus 1: プロパティベーステスト 10点
Bonus 2: モックとテストダブル 10点
**ボーナス合計** **20点**

合格基準

  • マンダトリー: 64点以上で合格(80点満点の80%)
  • ボーナス: 追加評価(最終成績の加算)
  • テスト実行

    # マンダトリー
    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
    
    # ボーナス
    zig test bonus1/property_tests.zig
    zig test bonus2/mock_tests.zig
    

    参考資料

  • 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