Кроме классических тестов на правильность функционирования, в Zig доступно несколько полезных приёмов, позволяющих повысить уверенность в работоспособности вашего кода:

1. Параметризированные тесты

Параметризированные тесты позволяют проводить серию проверок одной и той же функции с различными наборами значений. Они полезны, когда необходимо убедиться, что функция стабильно работает с множеством комбинаций аргументов.

Например, предположим, что у вас есть функция, преобразующая температуру из Цельсия в Фаренгейты:

src/converter.zig
const std = @import("std");

pub fn celsiusToFahrenheit(celsius: f32) f32 {
    return celsius * 9 / 5 + 32;
}

Тогда в тестах можно применить параметризацию:

tests/test_converter.zig
const std = @import("std");
const converter = @import("converter");

test "Преобразование температуры" {
    inline for ([_][2]f32{
        .{0, 32}, // Входящее значение и ожидаемое выходящее
        .{100, 212},
        .{-40, -40},
    }) |pair| {
        const celsius = pair[0];
        const expected = pair[1];
        const result = converter.celsiusToFahrenheit(celsius);
        try std.testing.expectApproxEqAbs(expected, result, 0.01);
    }
}

Использование цикла inline for позволяет перебирать пары значений и сравнивать результат с ожиданием.

2. Тестирование ошибок

Многие функции в Zig возвращают ошибки (error), и важно уметь их проверять. Для этого служит оператор try, позволяющий перехватывать ошибки.

Допустим, у вас есть функция чтения файла, которая возвращает ошибку, если файл отсутствует:

src/file_io.zig
const std = @import("std");

pub fn readFile(file_path: []const u8) ![]const u8 {
    const file = try std.fs.openFileAbsolute(file_path, .{});
    defer file.close();
    return file.readToEndAlloc(std.testing.allocator, 1024);
}

В тестах можно проверять возникновение ожидаемой ошибки:

tests/test_file_io.zig
const std = @import("std");
const file_io = @import("file_io");

test "Ошибка при чтении отсутствующего файла" {
    const err = file_io.readFile("/path/to/nonexistent.file") catch |e| e;
    try std.testing.expect(err == error.FileNotFound);
}

3. Ассертивные ожидания

Иногда приходится проверять сложное состояние, например, свойства массива или сложной структуры. Для этого предусмотрены специальные функции сравнения:

  • std.testing.expectEqual — проверка точного совпадения значений.
  • std.testing.expectEqualStrings — сравнение строк.
  • std.testing.expectCloseAbs — проверка близости численных значений с заданной точностью.

Пример проверки численного результата с заданной погрешностью:

test "Корень квадратный из 2 примерно равен 1.414" {
    const sqrt_two: f32 = std.math.sqrt(2.0);
    try std.testing.expectApproxEqAbs(sqrt_two, 1.414, 0.001);
}

4. Тестирование на нескольких архитектурах

Одним из преимуществ Zig является возможность кросс-платформенного тестирования. Вы можете протестировать код на разных архитектурах и ОС, используя возможности целевых платформ.

Например, следующий скрипт запускает тесты на трёх разных платформах:

build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const targets = [_]std.zig.CrossTarget{
        .{},
        .{ .cpu_arch = .x86_64, .os_tag = .linux },
        .{ .cpu_arch = .arm, .os_tag = .ios },
    };

    const optimize = b.standardOptimizeOption(.{});

    const test_step = b.step("test-all-targets", "Запустить тесты на всех платформах");

    for (targets) |target| {
        const lib = b.addTest(.{
            .root_module = b.createModule(.{
                .root_source_file = b.path("tests/test_main.zig"),
                .target = target,
                .optimize = optimize,
            }),
        });
        const run_tests = b.addRunArtifact(lib);
        test_step.dependOn(&run_tests.step);
    }
}

Теперь команда zig build test-all-targets запускает тесты на каждой указанной платформе.

Заключение

Правильно составленные тесты являются основой качественной разработки на любом языке программирования, включая Zig. Использование рассмотренных приёмов поможет увеличить покрытие тестов, сократить число возможных дефектов и ускорить выявление проблем. Главное правило — каждый компонент должен быть покрыт соответствующими тестами, а сами тесты должны оставаться лёгкими для понимания и модификации.