Zig Test

Code written within one or more test declarations can be used to ensure behavior meets expectations:

introducing_zig_test.zig

  1. const std = @import("std");
  2. test "expect addOne adds one to 41" {
  3. // The Standard Library contains useful functions to help create tests.
  4. // `expect` is a function that verifies its argument is true.
  5. // It will return an error if its argument is false to indicate a failure.
  6. // `try` is used to return an error to the test runner to notify it that the test failed.
  7. try std.testing.expect(addOne(41) == 42);
  8. }
  9. /// The function `addOne` adds one to the number given as its argument.
  10. fn addOne(number: i32) i32 {
  11. return number + 1;
  12. }

Shell

  1. $ zig test introducing_zig_test.zig
  2. 1/1 test "expect addOne adds one to 41"... OK
  3. All 1 tests passed.

The introducing_zig_test.zig code sample tests the function addOne to ensure that it returns 42 given the input 41. From this test’s perspective, the addOne function is said to be code under test.

zig test is a tool that creates and runs a test build. By default, it builds and runs an executable program using the default test runner provided by the Zig Standard Library as its main entry point. During the build, test declarations found while resolving the given Zig source file are included for the default test runner to run and report on.

This documentation discusses the features of the default test runner as provided by the Zig Standard Library. Its source code is located in lib/std/special/test_runner.zig.

The shell output shown above displays two lines after the zig test command. These lines are printed to standard error by the default test runner:

Test [1/1] test “expect addOne adds one to 41”…

Lines like this indicate which test, out of the total number of tests, is being run. In this case, [1/1] indicates that the first test, out of a total of one test, is being run. Note that, when the test runner program’s standard error is output to the terminal, these lines are cleared when a test succeeds.

All 1 tests passed.

This line indicates the total number of tests that have passed.

Test Declarations

Test declarations contain the keyword test, followed by an optional name written as a string literal, followed by a block containing any valid Zig code that is allowed in a function.

By convention, non-named tests should only be used to make other tests run. Non-named tests cannot be filtered.

Test declarations are similar to Functions: they have a return type and a block of code. The implicit return type of test is the Error Union Type anyerror!void, and it cannot be changed. When a Zig source file is not built using the zig test tool, the test declarations are omitted from the build.

Test declarations can be written in the same file, where code under test is written, or in a separate Zig source file. Since test declarations are top-level declarations, they are order-independent and can be written before or after the code under test.

See also:

Nested Container Tests

When the zig test tool is building a test runner, only resolved test declarations are included in the build. Initially, only the given Zig source file’s top-level declarations are resolved. Unless nested containers are referenced from a top-level test declaration, nested container tests will not be resolved.

The code sample below uses the std.testing.refAllDecls(@This()) function call to reference all of the containers that are in the file including the imported Zig source file. The code sample also shows an alternative way to reference containers using the _ = C; syntax. This syntax tells the compiler to ignore the result of the expression on the right side of the assignment operator.

testdecl_container_top_level.zig

  1. const std = @import("std");
  2. const expect = std.testing.expect;
  3. // Imported source file tests will run when referenced from a top-level test declaration.
  4. // The next line alone does not cause "introducing_zig_test.zig" tests to run.
  5. const imported_file = @import("introducing_zig_test.zig");
  6. test {
  7. // To run nested container tests, either, call `refAllDecls` which will
  8. // reference all declarations located in the given argument.
  9. // `@This()` is a builtin function that returns the innermost container it is called from.
  10. // In this example, the innermost container is this file (implicitly a struct).
  11. std.testing.refAllDecls(@This());
  12. // or, reference each container individually from a top-level test declaration.
  13. // The `_ = C;` syntax is a no-op reference to the identifier `C`.
  14. _ = S;
  15. _ = U;
  16. _ = @import("introducing_zig_test.zig");
  17. }
  18. const S = struct {
  19. test "S demo test" {
  20. try expect(true);
  21. }
  22. const SE = enum {
  23. V,
  24. // This test won't run because its container (SE) is not referenced.
  25. test "This Test Won't Run" {
  26. try expect(false);
  27. }
  28. };
  29. };
  30. const U = union { // U is referenced by the file's top-level test declaration
  31. s: US, // and US is referenced here; therefore, "U.Us demo test" will run
  32. const US = struct {
  33. test "U.US demo test" {
  34. // This test is a top-level test declaration for the struct.
  35. // The struct is nested (declared) inside of a union.
  36. try expect(true);
  37. }
  38. };
  39. test "U demo test" {
  40. try expect(true);
  41. }
  42. };

Shell

  1. $ zig test testdecl_container_top_level.zig
  2. 1/5 test ""... OK
  3. 2/5 S.test "S demo test"... OK
  4. 3/5 U.test "U demo test"... OK
  5. 4/5 introducing_zig_test.test "expect addOne adds one to 41"... OK
  6. 5/5 US.test "U.US demo test"... OK
  7. All 5 tests passed.

Test Failure

The default test runner checks for an error returned from a test. When a test returns an error, the test is considered a failure and its error return trace is output to standard error. The total number of failures will be reported after all tests have run.

test.zig

  1. const std = @import("std");
  2. test "expect this to fail" {
  3. try std.testing.expect(false);
  4. }
  5. test "expect this to succeed" {
  6. try std.testing.expect(true);
  7. }

Shell

  1. $ zig test test.zig
  2. 1/2 test "expect this to fail"... test "expect this to fail"... FAIL (TestUnexpectedResult)
  3. FAIL (TestUnexpectedResult)
  4. /home/andy/Downloads/zig/lib/std/testing.zig:303:14: 0x20801b in std.testing.expect (test)
  5. if (!ok) return error.TestUnexpectedResult;
  6. ^
  7. /home/andy/Downloads/zig/docgen_tmp/test.zig:4:5: 0x207a51 in test "expect this to fail" (test)
  8. try std.testing.expect(false);
  9. ^
  10. 2/2 test "expect this to succeed"... OK
  11. 1 passed; 0 skipped; 1 failed.
  12. error: the following test command failed with exit code 1:
  13. docgen_tmp/zig-cache/o/8e4964e05ddaf866e77360a551b3c05b/test /home/andy/Downloads/zig/build-release/zig

Skip Tests

One way to skip tests is to filter them out by using the zig test command line parameter --test-filter [text]. This makes the test build only include tests whose name contains the supplied filter text. Note that non-named tests are run even when using the --test-filter [text] command line parameter.

To programmatically skip a test, make a test return the error error.SkipZigTest and the default test runner will consider the test as being skipped. The total number of skipped tests will be reported after all tests have run.

test.zig

  1. test "this will be skipped" {
  2. return error.SkipZigTest;
  3. }

Shell

  1. $ zig test test.zig
  2. 1/1 test "this will be skipped"... test "this will be skipped"... SKIP
  3. SKIP
  4. 0 passed; 1 skipped; 0 failed.

The default test runner skips tests containing a suspend point while the test is running using the default, blocking IO mode. (The evented IO mode is enabled using the --test-evented-io command line parameter.)

async_skip.zig

  1. const std = @import("std");
  2. test "async skip test" {
  3. var frame = async func();
  4. const result = await frame;
  5. try std.testing.expect(result == 1);
  6. }
  7. fn func() i32 {
  8. suspend {
  9. resume @frame();
  10. }
  11. return 1;
  12. }

Shell

  1. $ zig test async_skip.zig
  2. 1/1 test "async skip test"... test "async skip test"... SKIP (async test)
  3. SKIP (async test)
  4. 0 passed; 1 skipped; 0 failed.

In the code sample above, the test would not be skipped in blocking IO mode if the nosuspend keyword was used (see Async and Await).

Report Memory Leaks

When code allocates Memory using the Zig Standard Library‘s testing allocator, std.testing.allocator, the default test runner will report any leaks that are found from using the testing allocator:

test.zig

  1. const std = @import("std");
  2. test "detect leak" {
  3. var list = std.ArrayList(u21).init(std.testing.allocator);
  4. // missing `defer list.deinit();`
  5. try list.append('☔');
  6. try std.testing.expect(list.items.len == 1);
  7. }

Shell

  1. $ zig test test.zig
  2. 1/1 test "detect leak"... OK
  3. [gpa] (err): memory address 0x7fc140eb0000 leaked:
  4. /home/andy/Downloads/zig/lib/std/array_list.zig:325:69: 0x20d9d9 in std.array_list.ArrayListAligned(u21,null).ensureTotalCapacityPrecise (test)
  5. const new_memory = try self.allocator.reallocAtLeast(self.allocatedSlice(), new_capacity);
  6. ^
  7. /home/andy/Downloads/zig/lib/std/array_list.zig:310:55: 0x20d7de in std.array_list.ArrayListAligned(u21,null).ensureTotalCapacity (test)
  8. return self.ensureTotalCapacityPrecise(better_capacity);
  9. ^
  10. /home/andy/Downloads/zig/lib/std/array_list.zig:349:41: 0x20d758 in std.array_list.ArrayListAligned(u21,null).addOne (test)
  11. try self.ensureTotalCapacity(newlen);
  12. ^
  13. /home/andy/Downloads/zig/lib/std/array_list.zig:161:49: 0x209d54 in std.array_list.ArrayListAligned(u21,null).append (test)
  14. const new_item_ptr = try self.addOne();
  15. ^
  16. /home/andy/Downloads/zig/docgen_tmp/test.zig:6:20: 0x209730 in test "detect leak" (test)
  17. try list.append('☔');
  18. ^
  19. /home/andy/Downloads/zig/lib/std/special/test_runner.zig:80:28: 0x23bd83 in std.special.main (test)
  20. } else test_fn.func();
  21. ^
  22. /home/andy/Downloads/zig/lib/std/start.zig:543:22: 0x233cac in std.start.callMain (test)
  23. root.main();
  24. ^
  25. /home/andy/Downloads/zig/lib/std/start.zig:495:12: 0x20e7ce in std.start.callMainWithArgs (test)
  26. return @call(.{ .modifier = .always_inline }, callMain, .{});
  27. ^
  28. All 1 tests passed.
  29. 1 errors were logged.
  30. 1 tests leaked memory.
  31. error: the following test command failed with exit code 1:
  32. docgen_tmp/zig-cache/o/8e4964e05ddaf866e77360a551b3c05b/test /home/andy/Downloads/zig/build-release/zig

See also:

Detecting Test Build

Use the compile variable @import("builtin").is_test to detect a test build:

detect_test.zig

  1. const std = @import("std");
  2. const builtin = @import("builtin");
  3. const expect = std.testing.expect;
  4. test "builtin.is_test" {
  5. try expect(isATest());
  6. }
  7. fn isATest() bool {
  8. return builtin.is_test;
  9. }

Shell

  1. $ zig test detect_test.zig
  2. 1/1 test "builtin.is_test"... OK
  3. All 1 tests passed.

Test Output and Logging

The default test runner and the Zig Standard Library’s testing namespace output messages to standard error.

The Testing Namespace

The Zig Standard Library’s testing namespace contains useful functions to help you create tests. In addition to the expect function, this document uses a couple of more functions as exemplified here:

testing_functions.zig

  1. const std = @import("std");
  2. test "expectEqual demo" {
  3. const expected: i32 = 42;
  4. const actual = 42;
  5. // The first argument to `expectEqual` is the known, expected, result.
  6. // The second argument is the result of some expression.
  7. // The actual's type is casted to the type of expected.
  8. try std.testing.expectEqual(expected, actual);
  9. }
  10. test "expectError demo" {
  11. const expected_error = error.DemoError;
  12. const actual_error_union: anyerror!void = error.DemoError;
  13. // `expectError` will fail when the actual error is different than
  14. // the expected error.
  15. try std.testing.expectError(expected_error, actual_error_union);
  16. }

Shell

  1. $ zig test testing_functions.zig
  2. 1/2 test "expectEqual demo"... OK
  3. 2/2 test "expectError demo"... OK
  4. All 2 tests passed.

The Zig Standard Library also contains functions to compare Slices, strings, and more. See the rest of the std.testing namespace in the Zig Standard Library for more available functions.

Test Tool Documentation

zig test has a few command line parameters which affect the compilation. See zig test —help for a full list.