解答13: テストの実践
概要
この解答では、Zigの組み込みテストフレームワークを活用して、単体テスト、データ構造のテスト、テーブル駆動テスト、メモリリーク検出テストについて学びます。
解答例
Part 1: 基本的な単体テスト
const std = @import("std");
// 文字列を反転する関数
fn reverseString(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;
}
// 配列の最大値を返す関数
fn findMax(numbers: []const i32) ?i32 {
if (numbers.len == 0) return null;
var max = numbers[0];
for (numbers[1..]) |num| {
if (num > max) max = num;
}
return max;
}
// 文字列が回文か判定する関数
fn isPalindrome(str: []const u8) bool {
if (str.len == 0) return true;
var i: usize = 0;
var j: usize = str.len - 1;
while (i < j) {
if (str[i] != str[j]) return false;
i += 1;
j -= 1;
}
return true;
}
// 階乗を計算する関数
fn factorial(n: u32) u64 {
if (n == 0) return 1;
return n * factorial(n - 1);
}
// フィボナッチ数を計算する関数
fn fibonacci(n: u32) u64 {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// ===== テスト =====
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 "reverseString - single char" {
const result = try reverseString(std.testing.allocator, "a");
defer std.testing.allocator.free(result);
try std.testing.expectEqualStrings("a", 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 "findMax - negative numbers" {
const numbers = [_]i32{ -5, -2, -10, -1 };
const max = findMax(&numbers);
try std.testing.expectEqual(@as(?i32, -1), max);
}
test "isPalindrome - true cases" {
try std.testing.expect(isPalindrome("racecar"));
try std.testing.expect(isPalindrome("a"));
try std.testing.expect(isPalindrome(""));
try std.testing.expect(isPalindrome("noon"));
}
test "isPalindrome - false cases" {
try std.testing.expect(!isPalindrome("hello"));
try std.testing.expect(!isPalindrome("ab"));
try std.testing.expect(!isPalindrome("world"));
}
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: データ構造のテスト
const std = @import("std");
fn Stack(comptime T: type) type {
return struct {
items: []T,
len: usize,
allocator: std.mem.Allocator,
const Self = @This();
const StackError = error{StackOverflow};
pub fn init(allocator: std.mem.Allocator, capacity: usize) !Self {
const items = try allocator.alloc(T, capacity);
return Self{
.items = items,
.len = 0,
.allocator = allocator,
};
}
pub fn deinit(self: *Self) void {
self.allocator.free(self.items);
}
pub fn push(self: *Self, item: T) !void {
if (self.len >= self.items.len) {
return StackError.StackOverflow;
}
self.items[self.len] = item;
self.len += 1;
}
pub fn pop(self: *Self) ?T {
if (self.len == 0) return null;
self.len -= 1;
return self.items[self.len];
}
pub fn peek(self: *Self) ?T {
if (self.len == 0) return null;
return self.items[self.len - 1];
}
pub fn size(self: Self) usize {
return self.len;
}
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: テーブル駆動テスト
const std = @import("std");
fn intToString(allocator: std.mem.Allocator, value: i32) ![]u8 {
return try std.fmt.allocPrint(allocator, "{}", .{value});
}
fn stringToInt(str: []const u8) !i32 {
return try std.fmt.parseInt(i32, str, 10);
}
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: メモリリーク検出
const std = @import("std");
const StringList = struct {
items: std.ArrayList([]u8),
allocator: std.mem.Allocator,
pub fn init(allocator: std.mem.Allocator) StringList {
return StringList{
.items = std.ArrayList([]u8).init(allocator),
.allocator = allocator,
};
}
pub fn deinit(self: *StringList) void {
for (self.items.items) |str| {
self.allocator.free(str);
}
self.items.deinit();
}
pub fn append(self: *StringList, str: []const u8) !void {
const owned = try self.allocator.alloc(u8, str.len);
@memcpy(owned, str);
try self.items.append(owned);
}
pub fn get(self: StringList, index: usize) ?[]const u8 {
if (index >= self.items.items.len) return null;
return self.items.items[index];
}
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).?);
}
}
ポイント解説
1. テストアロケータの重要性
std.testing.allocatorは、メモリリークを自動的に検出します。
2. テーブル駆動テストの利点
- 多数のテストケースを簡潔に記述
- テストケースの追加が容易
- パターンの可視化
expectEqual: 値の等価性をチェックexpectEqualStrings: 文字列の等価性をチェック(メッセージが分かりやすい)
3. expectEqual vs expectEqualStrings
4. deferの活用
テストでは必ずdeferでリソースを解放します。
よくある間違い
1. アロケータの使い忘れ
// 間違い
test "bad test" {
const data = try std.heap.page_allocator.alloc(u8, 100);
defer std.heap.page_allocator.free(data);
}
// 正しい
test "good test" {
const data = try std.testing.allocator.alloc(u8, 100);
defer std.testing.allocator.free(data);
}
2. expectEqualの型不一致
// 間違い
try std.testing.expectEqual(42, some_u32); // 型が一致しない
// 正しい
try std.testing.expectEqual(@as(u32, 42), some_u32);
発展課題
Challenge 1: モッククライアント
HTTP クライアントのモックを作成し、ネットワークなしでテストできるようにしてください。
Challenge 2: プロパティベーステスト
ランダムな入力でソート関数の性質(順序性、要素の保存)をテストしてください。
Challenge 3: ファズテスト
パーサーにランダムな入力を与えて、クラッシュしないことを確認してください。
Challenge 4: カバレッジ測定
テストのコードカバレッジを計測するツールを作成してください。
Challenge 5: テストフィクスチャ
セットアップとティアダウンを持つテストフレームワークを実装してください。
まとめ
この課題を通じて、以下を学びました:
Zigのテストシステムは、シンプルかつ強力です。適切なテストを書くことで、バグを早期に発見できます。