課題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