Zig Error Test Cases
An error test case is a test that checks a function fails with the exact error you expect. It proves the failure path works, not just the success path.
Two named errors
A range check can fail in two ways. Each test names one error and the input that causes it.
const std = @import("std");
const CheckError = error{ TooSmall, TooBig };
fn check(n: i32) CheckError!i32 {
if (n < 0) return error.TooSmall;
if (n > 10) return error.TooBig;
return n * 2;
}
test "negative input fails" {
try std.testing.expectError(error.TooSmall, check(-1));
}
test "large input fails" {
try std.testing.expectError(error.TooBig, check(99));
}Success beside failure
Error tests are stronger beside a success test. The success test proves good inputs still work.
const std = @import("std");
const CheckError = error{ TooSmall, TooBig };
fn check(n: i32) CheckError!i32 {
if (n < 0) return error.TooSmall;
if (n > 10) return error.TooBig;
return n * 2;
}
test "good input succeeds" {
try std.testing.expectEqual(@as(i32, 6), try check(3));
}
test "negative input fails" {
try std.testing.expectError(error.TooSmall, check(-1));
}
test "large input fails" {
try std.testing.expectError(error.TooBig, check(99));
}A second failing function
The same pattern works for a different function. Here division fails when the divisor is zero.
const std = @import("std");
const DivError = error{DivideByZero};
fn divide(a: i32, b: i32) DivError!i32 {
if (b == 0) return error.DivideByZero;
return @divTrunc(a, b);
}
test "division succeeds" {
try std.testing.expectEqual(@as(i32, 5), try divide(10, 2));
}
test "division by zero fails" {
try std.testing.expectError(error.DivideByZero, divide(10, 0));
}
test "negative division succeeds" {
try std.testing.expectEqual(@as(i32, -3), try divide(-9, 3));
}Example
const std = @import("std");
const LengthError = error{ TooShort, TooLong };
fn checkLength(s: []const u8) LengthError!usize {
if (s.len < 4) return error.TooShort;
if (s.len > 12) return error.TooLong;
return s.len;
}
test "short string fails" {
try std.testing.expectError(error.TooShort, checkLength("abc"));
}
test "long string fails" {
try std.testing.expectError(error.TooLong, checkLength("abcdefghijklm"));
}
test "hello passes" {
try std.testing.expectEqual(@as(usize, 5), try checkLength("hello"));
}
test "longer good string passes" {
try std.testing.expectEqual(@as(usize, 10), try checkLength("hello, zig"));
}Key points
- Error case: an error case is a test for a failure path, where a function returns an error instead of a value.
- expectError compares the error a function returns with the error you name, and fails when they differ.
- Write the expected error first, then the call that should fail.
- Test the success path beside the error path, so both sides of the function are covered.
- Use a small error set for the function, so each failure has one clear name.
- A passing error test means the function rejected the bad input, not that it crashed.
This run tests a length check from both sides. Two tests name the two errors, and two tests check lengths that pass. Together they prove the check accepts good lengths and rejects bad ones.
Try it Yourself
Edit the code, then press Run.
Running is simulated
Exercise
Which assertion checks that a function returns a named error?