# Writing Unit Tests

### Writing Unit Tests

A unit test checks one small piece of code in isolation.

Usually, that “unit” is a single function.

For example:

```zig
fn multiply(a: i32, b: i32) i32 {
    return a * b;
}
```

A unit test for this function checks whether it behaves correctly:

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

test "multiply returns the product of two numbers" {
    try std.testing.expectEqual(@as(i32, 12), multiply(3, 4));
}
```

The goal is simple:

give the function input

check the output

If the output is wrong, the test fails.

#### Why Unit Tests Matter

When programs grow larger, changing code becomes risky.

You might improve one function and accidentally break another part of the program.

Unit tests reduce that risk.

A good unit test gives you confidence that:

the function still works

edge cases still behave correctly

refactoring did not break behavior

future bugs are easier to detect

Tests also improve design. A function that is difficult to test is often difficult to understand.

#### A Unit Test Should Be Small

A unit test should focus on one behavior.

Bad example:

```zig
test "everything works" {
    // too many unrelated checks
}
```

Good example:

```zig
test "parseNumber handles valid input" {
    // one clear behavior
}

test "parseNumber rejects invalid input" {
    // another clear behavior
}
```

Small tests are easier to read and easier to debug.

When a test fails, you want to know exactly what broke.

#### The Basic Structure of a Unit Test

Most unit tests follow the same pattern:

1. create input
2. call the function
3. check the result

Example:

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

fn subtract(a: i32, b: i32) i32 {
    return a - b;
}

test "subtract removes the second number from the first" {
    const result = subtract(10, 3);

    try std.testing.expectEqual(@as(i32, 7), result);
}
```

This style is straightforward:

setup

action

verification

Keep tests mechanically simple.

#### Testing Multiple Cases

Most functions should be tested with multiple inputs.

Example:

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

fn isPositive(n: i32) bool {
    return n > 0;
}

test "isPositive handles positive numbers" {
    try std.testing.expect(isPositive(10));
}

test "isPositive handles zero" {
    try std.testing.expect(!isPositive(0));
}

test "isPositive handles negative numbers" {
    try std.testing.expect(!isPositive(-5));
}
```

Each test checks one category of input.

This matters because bugs often appear only in special cases.

#### Test Normal Cases First

Beginners sometimes start with strange edge cases immediately.

Start with normal input first.

Suppose you write this function:

```zig
fn average(a: i32, b: i32) i32 {
    return (a + b) / 2;
}
```

First test:

```zig
test "average calculates the midpoint between two numbers" {
    try std.testing.expectEqual(@as(i32, 15), average(10, 20));
}
```

After normal behavior works, test boundaries and unusual input.

#### Then Test Edge Cases

Edge cases are values near limits or boundaries.

Example:

```zig
test "average handles equal numbers" {
    try std.testing.expectEqual(@as(i32, 5), average(5, 5));
}

test "average handles zero" {
    try std.testing.expectEqual(@as(i32, 0), average(0, 0));
}

test "average handles negative numbers" {
    try std.testing.expectEqual(@as(i32, -15), average(-10, -20));
}
```

Good unit tests combine:

normal cases

boundary cases

invalid cases

special cases

#### Unit Tests Should Be Independent

One test should not depend on another test.

Bad idea:

```zig
var global_counter: i32 = 0;

test "increments counter" {
    global_counter += 1;
}

test "expects counter to already be incremented" {
    try std.testing.expectEqual(@as(i32, 1), global_counter);
}
```

This is fragile because test order should not matter.

A good test creates its own state:

```zig
test "counter starts at zero" {
    var counter: i32 = 0;
    counter += 1;

    try std.testing.expectEqual(@as(i32, 1), counter);
}
```

Independent tests are predictable.

#### Avoid Large Test Functions

A unit test should stay small.

Bad:

```zig
test "big complicated test" {
    // 200 lines
}
```

Large tests become difficult to understand.

Instead, split behavior into separate tests:

```zig
test "parser handles empty input" {}

test "parser handles whitespace" {}

test "parser handles valid numbers" {}

test "parser rejects invalid characters" {}
```

This produces better failure messages and cleaner structure.

#### Use Clear Test Names

Test names should explain behavior.

Weak:

```zig
test "math test" {}
```

Better:

```zig
test "divide returns the quotient for nonzero divisors" {}
```

A test name should answer:

“What behavior is this checking?”

You should understand the purpose without reading the implementation.

#### Testing Strings

Strings in Zig are slices of bytes:

```zig
[]const u8
```

Use `expectEqualStrings` for string comparisons.

Example:

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

fn language() []const u8 {
    return "Zig";
}

test "language returns Zig" {
    try std.testing.expectEqualStrings("Zig", language());
}
```

This produces better diagnostics if the strings differ.

#### Testing Floating Point Numbers

Floating point numbers are not always exact.

This can surprise beginners.

Example:

```zig
const x = 0.1 + 0.2;
```

You might expect exactly `0.3`, but binary floating point representation introduces tiny rounding differences.

So avoid direct equality checks like this:

```zig
try std.testing.expect(x == 0.3);
```

Instead, compare using tolerance:

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

test "floating point addition" {
    const result = 0.1 + 0.2;

    try std.testing.expectApproxEqAbs(
        @as(f64, 0.3),
        result,
        0.000001,
    );
}
```

This means:

“The result should be very close to `0.3`.”

#### Testing Errors Carefully

Suppose a function can fail:

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

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

    return c - '0';
}
```

Test both success and failure.

Success:

```zig
test "parseDigit converts numeric characters" {
    try std.testing.expectEqual(
        @as(u8, 5),
        try parseDigit('5'),
    );
}
```

Failure:

```zig
test "parseDigit rejects non-numeric characters" {
    try std.testing.expectError(
        ParseError.InvalidInput,
        parseDigit('x'),
    );
}
```

Good tests check both sides of the API contract.

#### Unit Tests Should Avoid Real External Systems

A unit test should avoid:

real network requests

real databases

real web APIs

real filesystem dependencies when possible

These make tests slower and less predictable.

Bad:

```zig
test "downloads data from the internet" {
    // unreliable external dependency
}
```

Good unit tests are:

fast

isolated

repeatable

deterministic

You want the same result every time.

#### Organizing Tests

Small projects often place tests in the same file:

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

test "add sums two numbers" {
    // test
}
```

Larger projects may use separate test files:

```text
src/
    math.zig
    parser.zig

tests/
    math_test.zig
    parser_test.zig
```

Both approaches are valid.

Keeping tests near the code is often easier for beginners.

#### Testing Internal Functions

A function does not need to be `pub` to be tested.

Example:

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

fn hiddenHelper(x: i32) i32 {
    return x * 10;
}

test "hiddenHelper multiplies by ten" {
    try std.testing.expectEqual(
        @as(i32, 50),
        hiddenHelper(5),
    );
}
```

This is useful because internal helpers can still contain bugs.

#### A Good Unit Test Feels Mechanical

Good unit tests are usually boring.

That is a good sign.

A test should read like a checklist:

given this input

call this function

expect this output

Avoid clever tricks or complicated logic inside tests.

If the test itself becomes difficult to understand, it becomes another source of bugs.

#### A Small Real Example

Here is a complete example:

```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 returns the value when inside the range" {
    try std.testing.expectEqual(
        @as(i32, 5),
        clamp(5, 0, 10),
    );
}

test "clamp returns the minimum when below the range" {
    try std.testing.expectEqual(
        @as(i32, 0),
        clamp(-3, 0, 10),
    );
}

test "clamp returns the maximum when above the range" {
    try std.testing.expectEqual(
        @as(i32, 10),
        clamp(99, 0, 10),
    );
}
```

Run it:

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

If all tests pass, the function behaves correctly for the cases you checked.

That does not mathematically prove the function is perfect, but it greatly increases confidence that the behavior matches your expectations.

