第13章: テスト

学習目標

この章を終えると、以下ができるようになります:

  • test ブロックを使って単体テストを書ける
  • テストを実行し、デバッグできる
  • テストアロケータを使ってメモリリークを検出できる
  • プロパティベーステストの基本を理解できる
  • テストの基本

    test ブロック

    Zigでは、testキーワードを使ってテストを定義します。

    const std = @import("std");
    
    fn add(a: i32, b: i32) i32 {
        return a + b;
    }
    
    test "add function" {
        const result = add(2, 3);
        try std.testing.expect(result == 5);
    }
    
    test "add with negative numbers" {
        const result = add(-2, 3);
        try std.testing.expect(result == 1);
    }
    
    test "add with zero" {
        const result = add(0, 0);
        try std.testing.expect(result == 0);
    }
    

    テストの実行

    # すべてのテストを実行
    zig test file.zig
    
    # 特定のテストを実行
    zig test file.zig --test-filter "add function"
    

    std.testing の基本的なアサーション

    const std = @import("std");
    
    test "basic assertions" {
        // 真偽値のテスト
        try std.testing.expect(true);
        try std.testing.expect(1 + 1 == 2);
    
        // 等価性のテスト
        try std.testing.expectEqual(@as(i32, 42), 42);
        try std.testing.expectEqual(true, true);
    
        // エラーのテスト
        const error_value: anyerror!i32 = error.SomeError;
        try std.testing.expectError(error.SomeError, error_value);
    }
    
    test "string comparison" {
        const str1 = "hello";
        const str2 = "hello";
    
        // 文字列の比較
        try std.testing.expectEqualStrings(str1, str2);
    }
    
    test "slice comparison" {
        const slice1 = [_]i32{ 1, 2, 3 };
        const slice2 = [_]i32{ 1, 2, 3 };
    
        // スライスの比較
        try std.testing.expectEqualSlices(i32, &slice1, &slice2);
    }
    

    テストアロケータ

    メモリリークの検出

    std.testing.allocatorを使うと、テスト中のメモリリークを自動的に検出できます。

    const std = @import("std");
    
    fn createBuffer(allocator: std.mem.Allocator, size: usize) ![]u8 {
        return try allocator.alloc(u8, size);
    }
    
    test "no memory leaks" {
        const buffer = try createBuffer(std.testing.allocator, 100);
        defer std.testing.allocator.free(buffer);
    
        // リークしない✓
    }
    
    test "memory leak detection" {
        const buffer = try createBuffer(std.testing.allocator, 100);
        _ = buffer;
    
        // リークする - テストが失敗する❌
    }
    

    失敗するアロケータ

    const std = @import("std");
    
    fn processWithFallback(allocator: std.mem.Allocator) ![]u8 {
        const buffer = allocator.alloc(u8, 1024) catch {
            // 確保失敗時のフォールバック
            var fallback: [256]u8 = undefined;
            return fallback[0..];
        };
        return buffer;
    }
    
    test "allocation failure handling" {
        var failing_allocator = std.testing.FailingAllocator.init(std.testing.allocator, 0);
        const allocator = failing_allocator.allocator();
    
        const result = try processWithFallback(allocator);
        try std.testing.expect(result.len == 256);
    }
    

    構造化されたテスト

    テストヘルパー関数

    const std = @import("std");
    
    const Calculator = struct {
        const Self = @This();
    
        pub fn add(a: i32, b: i32) i32 {
            return a + b;
        }
    
        pub fn subtract(a: i32, b: i32) i32 {
            return a - b;
        }
    
        pub fn multiply(a: i32, b: i32) i32 {
            return a * b;
        }
    
        pub fn divide(a: i32, b: i32) !i32 {
            if (b == 0) return error.DivisionByZero;
            return @divTrunc(a, b);
        }
    };
    
    // テストヘルパー
    fn expectCalculation(
        comptime operation: fn(i32, i32) i32,
        a: i32,
        b: i32,
        expected: i32,
    ) !void {
        const result = operation(a, b);
        try std.testing.expectEqual(expected, result);
    }
    
    test "calculator operations" {
        try expectCalculation(Calculator.add, 2, 3, 5);
        try expectCalculation(Calculator.subtract, 5, 3, 2);
        try expectCalculation(Calculator.multiply, 4, 5, 20);
    }
    
    test "calculator division" {
        const result = try Calculator.divide(10, 2);
        try std.testing.expectEqual(@as(i32, 5), result);
    
        try std.testing.expectError(error.DivisionByZero, Calculator.divide(10, 0));
    }
    

    テストフィクスチャ

    const std = @import("std");
    
    const TestContext = struct {
        allocator: std.mem.Allocator,
        arena: std.heap.ArenaAllocator,
    
        fn setup() TestContext {
            return TestContext{
                .allocator = std.testing.allocator,
                .arena = std.heap.ArenaAllocator.init(std.testing.allocator),
            };
        }
    
        fn teardown(self: *TestContext) void {
            self.arena.deinit();
        }
    
        fn createTestData(self: *TestContext, size: usize) ![]u8 {
            return try self.arena.allocator().alloc(u8, size);
        }
    };
    
    test "using test context" {
        var ctx = TestContext.setup();
        defer ctx.teardown();
    
        const data1 = try ctx.createTestData(100);
        const data2 = try ctx.createTestData(200);
    
        try std.testing.expect(data1.len == 100);
        try std.testing.expect(data2.len == 200);
    }
    

    高度なテストパターン

    パラメトライズドテスト

    const std = @import("std");
    
    fn isPrime(n: u32) bool {
        if (n < 2) return false;
        if (n == 2) return true;
        if (n % 2 == 0) return false;
    
        var i: u32 = 3;
        while (i * i <= n) : (i += 2) {
            if (n % i == 0) return false;
        }
        return true;
    }
    
    test "prime numbers" {
        const prime_numbers = [_]u32{ 2, 3, 5, 7, 11, 13, 17, 19, 23, 29 };
    
        for (prime_numbers) |num| {
            try std.testing.expect(isPrime(num));
        }
    }
    
    test "non-prime numbers" {
        const non_primes = [_]u32{ 0, 1, 4, 6, 8, 9, 10, 12, 14, 15 };
    
        for (non_primes) |num| {
            try std.testing.expect(!isPrime(num));
        }
    }
    

    テーブル駆動テスト

    const std = @import("std");
    
    fn romanToInt(s: []const u8) i32 {
        var result: i32 = 0;
        var i: usize = 0;
    
        while (i < s.len) : (i += 1) {
            const current = switch (s[i]) {
                'I' => 1,
                'V' => 5,
                'X' => 10,
                'L' => 50,
                'C' => 100,
                'D' => 500,
                'M' => 1000,
                else => 0,
            };
    
            if (i + 1 < s.len) {
                const next = switch (s[i + 1]) {
                    'I' => 1,
                    'V' => 5,
                    'X' => 10,
                    'L' => 50,
                    'C' => 100,
                    'D' => 500,
                    'M' => 1000,
                    else => 0,
                };
    
                if (current < next) {
                    result -= current;
                } else {
                    result += current;
                }
            } else {
                result += current;
            }
        }
    
        return result;
    }
    
    test "roman numerals - table driven" {
        const test_cases = [_]struct {
            input: []const u8,
            expected: i32,
        }{
            .{ .input = "I", .expected = 1 },
            .{ .input = "II", .expected = 2 },
            .{ .input = "III", .expected = 3 },
            .{ .input = "IV", .expected = 4 },
            .{ .input = "V", .expected = 5 },
            .{ .input = "IX", .expected = 9 },
            .{ .input = "X", .expected = 10 },
            .{ .input = "XL", .expected = 40 },
            .{ .input = "L", .expected = 50 },
            .{ .input = "XC", .expected = 90 },
            .{ .input = "C", .expected = 100 },
            .{ .input = "CD", .expected = 400 },
            .{ .input = "D", .expected = 500 },
            .{ .input = "CM", .expected = 900 },
            .{ .input = "M", .expected = 1000 },
            .{ .input = "MCMXCIV", .expected = 1994 },
        };
    
        for (test_cases) |tc| {
            const result = romanToInt(tc.input);
            try std.testing.expectEqual(tc.expected, result);
        }
    }
    

    モックとスタブ

    const std = @import("std");
    
    const Database = struct {
        const Self = @This();
    
        getUserFn: *const fn(*Self, u32) anyerror![]const u8,
    
        pub fn getUser(self: *Self, id: u32) ![]const u8 {
            return self.getUserFn(self, id);
        }
    };
    
    const MockDatabase = struct {
        db: Database,
        call_count: usize,
    
        fn init() MockDatabase {
            return MockDatabase{
                .db = Database{
                    .getUserFn = mockGetUser,
                },
                .call_count = 0,
            };
        }
    
        fn mockGetUser(db: *Database, id: u32) ![]const u8 {
            const self = @fieldParentPtr(MockDatabase, "db", db);
            self.call_count += 1;
    
            return if (id == 1) "Alice" else error.UserNotFound;
        }
    };
    
    test "database mock" {
        var mock = MockDatabase.init();
    
        const user = try mock.db.getUser(1);
        try std.testing.expectEqualStrings("Alice", user);
        try std.testing.expectEqual(@as(usize, 1), mock.call_count);
    
        try std.testing.expectError(error.UserNotFound, mock.db.getUser(999));
        try std.testing.expectEqual(@as(usize, 2), mock.call_count);
    }
    

    プロパティベーステスト

    基本的なプロパティテスト

    const std = @import("std");
    
    fn reverse(allocator: std.mem.Allocator, slice: []const i32) ![]i32 {
        const result = try allocator.alloc(i32, slice.len);
        for (slice, 0..) |item, i| {
            result[slice.len - 1 - i] = item;
        }
        return result;
    }
    
    test "reverse property: double reverse equals original" {
        const allocator = std.testing.allocator;
    
        const original = [_]i32{ 1, 2, 3, 4, 5 };
    
        const reversed_once = try reverse(allocator, &original);
        defer allocator.free(reversed_once);
    
        const reversed_twice = try reverse(allocator, reversed_once);
        defer allocator.free(reversed_twice);
    
        try std.testing.expectEqualSlices(i32, &original, reversed_twice);
    }
    
    test "reverse property: length is preserved" {
        const allocator = std.testing.allocator;
    
        const test_cases = [_][]const i32{
            &[_]i32{},
            &[_]i32{1},
            &[_]i32{ 1, 2 },
            &[_]i32{ 1, 2, 3, 4, 5 },
        };
    
        for (test_cases) |tc| {
            const reversed = try reverse(allocator, tc);
            defer allocator.free(reversed);
    
            try std.testing.expectEqual(tc.len, reversed.len);
        }
    }
    

    ランダムテスト

    const std = @import("std");
    
    fn sort(slice: []i32) void {
        if (slice.len <= 1) return;
    
        var i: usize = 0;
        while (i < slice.len - 1) : (i += 1) {
            var j: usize = 0;
            while (j < slice.len - 1 - i) : (j += 1) {
                if (slice[j] > slice[j + 1]) {
                    const temp = slice[j];
                    slice[j] = slice[j + 1];
                    slice[j + 1] = temp;
                }
            }
        }
    }
    
    fn isSorted(slice: []const i32) bool {
        if (slice.len <= 1) return true;
    
        var i: usize = 0;
        while (i < slice.len - 1) : (i += 1) {
            if (slice[i] > slice[i + 1]) return false;
        }
        return true;
    }
    
    test "sort property: result is sorted" {
        var prng = std.rand.DefaultPrng.init(42);
        const random = prng.random();
    
        var i: usize = 0;
        while (i < 100) : (i += 1) {
            var numbers: [10]i32 = undefined;
            for (&numbers) |*num| {
                num.* = random.intRangeAtMost(i32, -100, 100);
            }
    
            sort(&numbers);
            try std.testing.expect(isSorted(&numbers));
        }
    }
    

    テストの整理

    テストの名前空間

    const std = @import("std");
    
    const math = struct {
        pub fn add(a: i32, b: i32) i32 {
            return a + b;
        }
    
        pub fn multiply(a: i32, b: i32) i32 {
            return a * b;
        }
    };
    
    const string_utils = struct {
        pub fn reverse(allocator: std.mem.Allocator, str: []const u8) ![]u8 {
            const result = try allocator.alloc(u8, str.len);
            for (str, 0..) |c, i| {
                result[str.len - 1 - i] = c;
            }
            return result;
        }
    };
    
    test "math.add" {
        try std.testing.expectEqual(@as(i32, 5), math.add(2, 3));
    }
    
    test "math.multiply" {
        try std.testing.expectEqual(@as(i32, 20), math.multiply(4, 5));
    }
    
    test "string_utils.reverse" {
        const result = try string_utils.reverse(std.testing.allocator, "hello");
        defer std.testing.allocator.free(result);
    
        try std.testing.expectEqualStrings("olleh", result);
    }
    

    まとめ

    この章では、Zigのテストについて学びました:

  • test ブロック: 単体テストの基本
  • std.testing: アサーション関数
  • テストアロケータ: メモリリーク検出
  • 高度なパターン: テーブル駆動、モック、プロパティベース
  • テストの整理: 名前空間、フィクスチャ
  • 次の章では、ビルドシステムについて学びます。

    参考資料

  • 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