# Testing Strategy

### Testing Strategy

Tests should be close to the code they check. Zig makes this easy with `test` blocks.

```zig
const std = @import("std");

fn add(a: i32, b: i32) i32 {
    return a + b;
}

test "add positive numbers" {
    try std.testing.expectEqual(@as(i32, 30), add(10, 20));
}
```

Run it:

```sh
zig test main.zig
```

A test block has a name and a body.

```zig
test "add positive numbers" {
    ...
}
```

The body is ordinary Zig code. It may declare variables, call functions, allocate memory, and return errors.

The expression:

```zig
try std.testing.expectEqual(@as(i32, 30), add(10, 20));
```

checks that two values are equal. If they are not equal, the test fails.

The first value is the expected value. The second value is the actual value.

Tests should cover behavior, not implementation. A useful test says what the function must do.

```zig
fn clamp(n: i32, low: i32, high: i32) i32 {
    if (n < low) return low;
    if (n > high) return high;
    return n;
}

test "clamp returns low when value is too small" {
    try std.testing.expectEqual(@as(i32, 0), clamp(-5, 0, 10));
}

test "clamp returns high when value is too large" {
    try std.testing.expectEqual(@as(i32, 10), clamp(15, 0, 10));
}

test "clamp returns value inside range" {
    try std.testing.expectEqual(@as(i32, 7), clamp(7, 0, 10));
}
```

Small tests are better than large tests. Each test should have one reason to fail.

A parser needs tests for valid input and invalid input.

```zig
const ParseError = error{
    Empty,
    InvalidDigit,
};

fn parseDigit(text: []const u8) ParseError!u8 {
    if (text.len == 0) return error.Empty;
    if (text.len != 1) return error.InvalidDigit;

    const c = text[0];

    if (c < '0' or c > '9') return error.InvalidDigit;

    return c - '0';
}

test "parseDigit parses one digit" {
    try std.testing.expectEqual(@as(u8, 7), try parseDigit("7"));
}

test "parseDigit rejects empty input" {
    try std.testing.expectError(error.Empty, parseDigit(""));
}

test "parseDigit rejects non-digit input" {
    try std.testing.expectError(error.InvalidDigit, parseDigit("x"));
}
```

`expectError` checks that an expression returns a specific error.

```zig
try std.testing.expectError(error.Empty, parseDigit(""));
```

This is better than checking only that some error occurred. The test states the exact contract.

For code that allocates memory, use the testing allocator.

```zig
fn duplicate(allocator: std.mem.Allocator, text: []const u8) ![]u8 {
    const out = try allocator.alloc(u8, text.len);
    @memcpy(out, text);
    return out;
}

test "duplicate copies bytes" {
    const allocator = std.testing.allocator;

    const copy = try duplicate(allocator, "zig");
    defer allocator.free(copy);

    try std.testing.expectEqualStrings("zig", copy);
}
```

The testing allocator helps detect leaks in tests. If memory is allocated and not freed, the test runner can report it.

Tests should also check edge cases. Good edge cases usually come from boundaries:

| Function kind | Edge cases |
|---|---|
| parser | empty input, malformed input, largest valid input |
| numeric function | zero, one, negative values, overflow boundary |
| collection | empty collection, one item, many items |
| I/O code | missing file, empty file, permission failure |
| allocator code | allocation failure, cleanup after partial work |

For larger programs, keep pure logic separate from I/O. Pure functions are easier to test.

Instead of testing this directly:

```zig
fn run(path: []const u8) !void {
    const cwd = std.fs.cwd();

    var file = try cwd.openFile(path, .{});
    defer file.close();

    // parse and execute
}
```

split the work:

```zig
fn parseLine(line: []const u8) !i32 {
    return try std.fmt.parseInt(i32, line, 10);
}
```

Now `parseLine` can be tested without creating files.

```zig
test "parseLine parses decimal integer" {
    try std.testing.expectEqual(@as(i32, 123), try parseLine("123"));
}

test "parseLine rejects letters" {
    try std.testing.expectError(error.InvalidCharacter, parseLine("abc"));
}
```

Tests do not remove the need for simple code. They protect simple code from quiet changes.

A practical strategy is:

1. Test public behavior first.
2. Test error cases explicitly.
3. Test allocation and cleanup.
4. Keep I/O thin.
5. Move decisions into small functions.

For a build-based project, add a test step in `build.zig`.

```zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    const tests = b.addTest(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    const run_tests = b.addRunArtifact(tests);

    const test_step = b.step("test", "Run tests");
    test_step.dependOn(&run_tests.step);
}
```

Run it:

```sh
zig build test
```

This gives the project one command for tests, regardless of how many source files and artifacts it later contains.

Exercise 20-26. Write tests for the `sub` command from the calculator.

Exercise 20-27. Add tests for missing command-line arguments by moving parsing into a function.

Exercise 20-28. Write a test that uses `std.testing.allocator`.

Exercise 20-29. Add a test step to a build script.

Exercise 20-30. Write one test that proves cleanup happens after an error.

