# Table-Driven Tests

### Table-Driven Tests

A table-driven test checks many input cases with one test loop.

Instead of writing many separate tests like this:

```zig
test "double handles one" {
    try std.testing.expectEqual(@as(i32, 2), double(1));
}

test "double handles two" {
    try std.testing.expectEqual(@as(i32, 4), double(2));
}

test "double handles ten" {
    try std.testing.expectEqual(@as(i32, 20), double(10));
}
```

You put the cases in a small table:

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

fn double(n: i32) i32 {
    return n * 2;
}

test "double multiplies numbers by two" {
    const cases = [_]struct {
        input: i32,
        expected: i32,
    }{
        .{ .input = 1, .expected = 2 },
        .{ .input = 2, .expected = 4 },
        .{ .input = 10, .expected = 20 },
    };

    for (cases) |case| {
        try std.testing.expectEqual(case.expected, double(case.input));
    }
}
```

This is called table-driven testing because the test data is written as a table of cases.

#### Why Use Table-Driven Tests

Table-driven tests are useful when the same logic should be checked with many inputs.

They work well for:

parsers

math functions

string functions

validation functions

small algorithms

boundary checks

error cases

They reduce repeated test code. The function call and expectation stay in one place. The inputs and expected outputs are easy to scan.

#### A Test Case Is Just Data

In Zig, a table of test cases is usually an array of anonymous structs.

Look at this part:

```zig
const cases = [_]struct {
    input: i32,
    expected: i32,
}{
    .{ .input = 1, .expected = 2 },
    .{ .input = 2, .expected = 4 },
    .{ .input = 10, .expected = 20 },
};
```

Read it as:

“Create an array. Each item has an `input` field and an `expected` field.”

The `[_]` means Zig should infer the array length.

The type of each item is:

```zig
struct {
    input: i32,
    expected: i32,
}
```

Each row is one case:

```zig
.{ .input = 1, .expected = 2 }
```

This style is explicit. A reader can understand the whole test by reading the cases.

#### Looping Through Cases

After defining the cases, use a `for` loop:

```zig
for (cases) |case| {
    try std.testing.expectEqual(case.expected, double(case.input));
}
```

For each case:

`case.input` is passed to the function.

`case.expected` is compared with the result.

The test fails if any row fails.

#### Start with a Normal Case

Suppose we have this function:

```zig
fn clamp(value: i32, min: i32, max: i32) i32 {
    if (value < min) return min;
    if (value > max) return max;
    return value;
}
```

A table-driven test can cover several cases:

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

fn clamp(value: i32, min: i32, max: i32) i32 {
    if (value < min) return min;
    if (value > max) return max;
    return value;
}

test "clamp limits a value to a range" {
    const cases = [_]struct {
        value: i32,
        min: i32,
        max: i32,
        expected: i32,
    }{
        .{ .value = 5, .min = 0, .max = 10, .expected = 5 },
        .{ .value = -3, .min = 0, .max = 10, .expected = 0 },
        .{ .value = 99, .min = 0, .max = 10, .expected = 10 },
        .{ .value = 0, .min = 0, .max = 10, .expected = 0 },
        .{ .value = 10, .min = 0, .max = 10, .expected = 10 },
    };

    for (cases) |case| {
        try std.testing.expectEqual(
            case.expected,
            clamp(case.value, case.min, case.max),
        );
    }
}
```

This one test checks:

a value inside the range

a value below the range

a value above the range

the lower boundary

the upper boundary

That is a good fit for table-driven testing.

#### Add a Name Field for Better Debugging

A table-driven test has one weakness: when a row fails, it may not be obvious which case caused the failure.

You can add a `name` field:

```zig
test "clamp limits a value to a range" {
    const cases = [_]struct {
        name: []const u8,
        value: i32,
        min: i32,
        max: i32,
        expected: i32,
    }{
        .{
            .name = "inside range",
            .value = 5,
            .min = 0,
            .max = 10,
            .expected = 5,
        },
        .{
            .name = "below range",
            .value = -3,
            .min = 0,
            .max = 10,
            .expected = 0,
        },
        .{
            .name = "above range",
            .value = 99,
            .min = 0,
            .max = 10,
            .expected = 10,
        },
    };

    for (cases) |case| {
        std.debug.print("case: {s}\n", .{case.name});

        try std.testing.expectEqual(
            case.expected,
            clamp(case.value, case.min, case.max),
        );
    }
}
```

This prints the case name before each check.

For small tests, this is not always necessary. For larger tables, a name field can save time.

#### Table-Driven Tests for Strings

Table-driven tests are also useful for string functions.

Example:

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

fn isEmpty(text: []const u8) bool {
    return text.len == 0;
}

test "isEmpty checks whether a string has no bytes" {
    const cases = [_]struct {
        input: []const u8,
        expected: bool,
    }{
        .{ .input = "", .expected = true },
        .{ .input = "a", .expected = false },
        .{ .input = "hello", .expected = false },
    };

    for (cases) |case| {
        try std.testing.expectEqual(case.expected, isEmpty(case.input));
    }
}
```

Here, the input is a string slice:

```zig
[]const u8
```

That means “a read-only slice of bytes.”

#### Table-Driven Tests for Errors

You can also test error behavior.

Suppose we have a function that parses a single digit:

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

fn parseDigit(c: u8) !u8 {
    if (c < '0' or c > '9') {
        return ParseError.InvalidDigit;
    }

    return c - '0';
}
```

We can test valid input with a table:

```zig
test "parseDigit parses valid digits" {
    const cases = [_]struct {
        input: u8,
        expected: u8,
    }{
        .{ .input = '0', .expected = 0 },
        .{ .input = '1', .expected = 1 },
        .{ .input = '9', .expected = 9 },
    };

    for (cases) |case| {
        try std.testing.expectEqual(case.expected, try parseDigit(case.input));
    }
}
```

Then test invalid input separately:

```zig
test "parseDigit rejects invalid characters" {
    const cases = [_]u8{
        'x',
        '/',
        ':',
        ' ',
    };

    for (cases) |input| {
        try std.testing.expectError(
            ParseError.InvalidDigit,
            parseDigit(input),
        );
    }
}
```

This is clearer than mixing successful results and expected errors in one table.

#### Avoid Overcomplicated Tables

A table-driven test should make the test easier to read.

This is good:

```zig
const cases = [_]struct {
    input: i32,
    expected: i32,
}{
    .{ .input = 1, .expected = 2 },
    .{ .input = 2, .expected = 4 },
};
```

This is usually too much for a beginner test:

```zig
const cases = [_]struct {
    name: []const u8,
    input: []const u8,
    allocator: std.mem.Allocator,
    mode: Mode,
    flags: Flags,
    expected_error: ?ParseError,
    expected_output: ?Result,
    cleanup: ?fn () void,
}{
    // hard to read
};
```

When a table has too many fields, split the test into smaller tests.

The goal is clarity, not cleverness.

#### Use Tables When the Shape Repeats

A table-driven test is a good choice when each case has the same shape.

For example:

```text
input -> expected output
```

or:

```text
input -> expected error
```

or:

```text
value, min, max -> expected value
```

It is less useful when every case needs different setup, different cleanup, or different assertions.

In that situation, separate test blocks are easier to read.

#### Keep Test Logic Simple

The loop should usually contain only the function call and the expectation.

Good:

```zig
for (cases) |case| {
    try std.testing.expectEqual(case.expected, double(case.input));
}
```

Less good:

```zig
for (cases) |case| {
    // many branches
    // several modes
    // special cleanup
    // multiple unrelated checks
}
```

When the loop becomes complicated, the table is no longer helping.

#### A Complete Example

Here is a full example you can save as `main.zig`:

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

fn startsWith(text: []const u8, prefix: []const u8) bool {
    if (prefix.len > text.len) return false;

    for (prefix, 0..) |ch, i| {
        if (text[i] != ch) return false;
    }

    return true;
}

test "startsWith checks whether text begins with a prefix" {
    const cases = [_]struct {
        text: []const u8,
        prefix: []const u8,
        expected: bool,
    }{
        .{ .text = "zig", .prefix = "z", .expected = true },
        .{ .text = "zig", .prefix = "zi", .expected = true },
        .{ .text = "zig", .prefix = "zig", .expected = true },
        .{ .text = "zig", .prefix = "zag", .expected = false },
        .{ .text = "zig", .prefix = "ziglang", .expected = false },
        .{ .text = "zig", .prefix = "", .expected = true },
        .{ .text = "", .prefix = "", .expected = true },
        .{ .text = "", .prefix = "z", .expected = false },
    };

    for (cases) |case| {
        try std.testing.expectEqual(
            case.expected,
            startsWith(case.text, case.prefix),
        );
    }
}
```

Run it:

```bash
zig test main.zig
```

This one test checks normal cases, boundary cases, and empty strings.

Table-driven tests are not special magic. They are just data plus a loop. The value comes from making repeated checks compact, readable, and easy to extend.

