第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のテストについて学びました:
次の章では、ビルドシステムについて学びます。