| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
A modern, feature-rich testing framework for Zig inspired by Jest, Vitest, and Bun's test runner.
Add to your build.zig.zon:
.{
.name = "my-project",
.version = "0.1.0",
.dependencies = .{
.@"zig-test-framework" = .{
.url = "https://github.com/zig-utils/zig-test-framework/archive/refs/tags/v0.1.0.tar.gz",
// Replace with actual hash after publishing
},
},
}There are two ways to use the Zig Test Framework:
The easiest way to use the framework is with automatic test discovery:
// tests/math.test.zig
const std = @import("std");
test "addition works" {
const result = 2 + 2;
try std.testing.expectEqual(@as(i32, 4), result);
}
test "subtraction works" {
const result = 10 - 5;
try std.testing.expectEqual(@as(i32, 5), result);
}zig-test --test-dir testsThis will automatically discover and run all *.test.zig files in the tests directory.
CLI Options for Test Discovery:
Examples:
# Run all tests in tests/ directory
zig-test --test-dir tests
# Run tests with custom pattern
zig-test --test-dir src --pattern "*.spec.zig"
# Run tests without recursion
zig-test --test-dir tests --no-recursive
# Stop on first failure
zig-test --test-dir tests --bailFor more control, you can manually register tests:
const std = @import("std");
const ztf = @import("zig-test-framework");
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer * = gpa.deinit();
const allocator = gpa.allocator();
// Define a test suite
try ztf.describe(allocator, "Math operations", struct {
fn testSuite(alloc: std.mem.Allocator) !void {
try ztf.it(alloc, "should add two numbers", testAddition);
try ztf.it(alloc, "should subtract two numbers", testSubtraction);
}
fn testAddition(alloc: std.mem.Allocator) !void {
const result = 2 + 2;
try ztf.expect(alloc, result).toBe(4);
}
fn testSubtraction(alloc: std.mem.Allocator) !void {
const result = 10 - 5;
try ztf.expect(alloc, result).toBe(5);
}
}.testSuite);
// Run all tests
const registry = ztf.getRegistry(allocator);
const success = try ztf.runTests(allocator, registry);
// Clean up the registry
ztf.cleanupRegistry();
if (!success) {
std.process.exit(1);
}
}// Basic equality
try expect(alloc, 5).toBe(5);
try expect(alloc, true).toBeTruthy();
try expect(alloc, false).toBeFalsy();
// Negation
try expect(alloc, 5).not().toBe(10);
// Comparisons
try expect(alloc, 10).toBeGreaterThan(5);
try expect(alloc, 10).toBeGreaterThanOrEqual(10);
try expect(alloc, 5).toBeLessThan(10);
try expect(alloc, 5).toBeLessThanOrEqual(5);
// Strings
try expect(alloc, "hello").toBe("hello");
try expect(alloc, "hello world").toContain("world");
try expect(alloc, "hello").toStartWith("hel");
try expect(alloc, "hello").toEndWith("lo");
try expect(alloc, "hello").toHaveLength(5);
// Optionals
const value: ?i32 = null;
try expect(alloc, value).toBeNull();
const defined: ?i32 = 42;
try expect(alloc, defined).toBeDefined();
// Arrays/Slices
const numbers = [*]i32{1, 2, 3, 4, 5};
const matcher = expectArray(alloc, &numbers);
try matcher.toHaveLength(5);
try matcher.toContain(3);
try matcher.toContainAll(&[*]i32{1, 3, 5});
// Error assertions
const FailingFn = struct {
fn call() !void {
return error.TestError;
}
};
try expect(alloc, FailingFn.call).toThrow();
try expect(alloc, FailingFn.call).toThrowError(error.TestError);// Floating-point comparison
try ztf.toBeCloseTo(0.1 + 0.2, 0.3, 10);
// NaN and Infinity
try ztf.toBeNaN(std.math.nan(f64));
try ztf.toBeInfinite(std.math.inf(f64));
// Struct matching
const User = struct {
name: []const u8,
age: u32,
};
const user = User{ .name = "Alice", .age = 30 };
const matcher = ztf.expectStruct(alloc, user);
try matcher.toHaveField("name", "Alice");
try matcher.toHaveField("age", @as(u32, 30));try ztf.describe(allocator, "Database tests", struct {
var db*connection: ?*Database = null;
fn testSuite(alloc: std.mem.Allocator) !void {
// Runs once before all tests
try ztf.beforeAll(alloc, setupDatabase);
// Runs before each test
try ztf.beforeEach(alloc, openConnection);
// Runs after each test
try ztf.afterEach(alloc, closeConnection);
// Runs once after all tests
try ztf.afterAll(alloc, teardownDatabase);
try ztf.it(alloc, "should query data", testQuery);
try ztf.it(alloc, "should insert data", testInsert);
}
fn setupDatabase(alloc: std.mem.Allocator) !void {
// Initialize database
}
fn teardownDatabase(alloc: std.mem.Allocator) !void {
// Cleanup database
}
fn openConnection(alloc: std.mem.Allocator) !void {
// Open DB connection
}
fn closeConnection(alloc: std.mem.Allocator) !void {
// Close DB connection
}
fn testQuery(alloc: std.mem.Allocator) !void {
// Test implementation
}
fn testInsert(alloc: std.mem.Allocator) !void {
// Test implementation
}
}.testSuite);Important: Always call cleanupRegistry() after running tests to prevent memory leaks:
pub fn main() !void {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer * = gpa.deinit();
const allocator = gpa.allocator();
// Define your test suites...
try ztf.describe(allocator, "My tests", ...);
// Run tests
const registry = ztf.getRegistry(allocator);
const success = try ztf.runTests(allocator, registry);
// Clean up - IMPORTANT!
ztf.cleanupRegistry();
if (!success) {
std.process.exit(1);
}
}The cleanupRegistry() function frees all test suites, test cases, and associated resources. Without it, you'll see memory leaks when using GeneralPurposeAllocator in debug mode.
try ztf.describe(allocator, "User Service", struct {
fn testSuite(alloc: std.mem.Allocator) !void {
try ztf.describe(alloc, "Authentication", struct {
fn nestedSuite(nested*alloc: std.mem.Allocator) !void {
try ztf.it(nested*alloc, "should login with valid credentials", testValidLogin);
try ztf.it(nested*alloc, "should reject invalid credentials", testInvalidLogin);
}
fn testValidLogin(nested*alloc: std.mem.Allocator) !void {
// Test implementation
}
fn testInvalidLogin(nested*alloc: std.mem.Allocator) !void {
// Test implementation
}
}.nestedSuite);
}
}.testSuite);Full Jest-compatible mocking API:
// Create a mock
var mock*fn = ztf.createMock(alloc, i32);
defer mock*fn.deinit();
// Record calls and assert
try mock*fn.recordCall("arg1");
try mock*fn.toHaveBeenCalled();
try mock*fn.toHaveBeenCalledTimes(1);
try mock*fn.toHaveBeenCalledWith("arg1");
// Mock return values
* = try mock*fn.mockReturnValue(42);
* = try mock*fn.mockReturnValueOnce(100);
const value = mock*fn.getReturnValue(); // Returns 100 first time, then 42
// Spy on existing functions
const original: i32 = 99;
var spy = ztf.createSpy(alloc, i32, original);
defer spy.deinit();
try spy.call("test");
try spy.toHaveBeenCalled();
const restored = spy.mockRestore();See docs/mocks.md for complete mocking documentation.
Capture and compare test outputs:
// Create a snapshot
var snap = ztf.createSnapshot(alloc, "user*test", .{
.update = true,
.format = .json,
});
// Match against snapshot
const user = User{ .name = "Alice", .age = 30 };
try snap.match(user);
// Named snapshots
try snap.matchNamed("initial*state", initial);
try snap.matchNamed("after*update", updated);
// String snapshots
try snap.matchString("Expected output");Snapshots are stored in .snapshots/ directory. Run tests with update: true to create/update snapshots.
See docs/snapshots.md for complete snapshot documentation.
Control time in your tests:
// Set specific time (Jan 1, 2020)
ztf.setSystemTime(alloc, 1577836800000);
const year = ztf.DateHelper.getYear(ztf.time.now(alloc));
try ztf.expect(alloc, year).toBe(@as(u16, 2020));
// Advance time
ztf.advanceTimersByTime(alloc, 60000); // Advance 1 minute
// Jest-compatible API
ztf.jest.useFakeTimers(alloc);
ztf.jest.setSystemTime(alloc, 1577836800000);
const current = ztf.jest.now(alloc);
ztf.jest.advanceTimersByTime(alloc, 5000);
ztf.jest.useRealTimers(alloc);
// Reset to real time
ztf.setSystemTime(alloc, null);See docs/dates-and-times.md for complete time mocking documentation.
// Skip a test
try ztf.itSkip(alloc, "should skip this test", testSkipped);
// Skip an entire suite
try ztf.describeSkip(allocator, "Skipped suite", struct {
// All tests in this suite will be skipped
}.testSuite);
// Run only specific tests
try ztf.itOnly(alloc, "should run only this test", testOnly);
// Run only specific suites
try ztf.describeOnly(allocator, "Only this suite", struct {
// Only tests in this suite will run
}.testSuite);Run tests asynchronously with full concurrency support:
// Basic async test
try ztf.itAsync(allocator, "async operation", struct {
fn run(alloc: std.mem.Allocator) !void {
* = alloc;
std.Thread.sleep(100 * std.time.ns*per*ms);
// Test async operations
}
}.run);
// Async test with custom timeout
try ztf.itAsyncTimeout(allocator, "slow async operation", asyncTestFn, 10000);
// Skip/only for async tests
try ztf.itAsyncSkip(allocator, "skipped async test", testFn);
try ztf.itAsyncOnly(allocator, "focused async test", testFn);
// Using AsyncTestExecutor for advanced control
var executor = ztf.AsyncTestExecutor.init(allocator, .{
.concurrent = true,
.max*concurrent = 5,
.default*timeout*ms = 5000,
});
defer executor.deinit();
try executor.registerTest("test1", testFn1);
try executor.registerTest("test2", testFn2);
const results = try executor.executeAll();
defer allocator.free(results);Configure timeouts at multiple levels:
// Per-test timeout (1 second)
try ztf.itTimeout(allocator, "timed test", testFn, 1000);
// Per-suite timeout (5 seconds for all tests in suite)
try ztf.describeTimeout(allocator, "Timed Suite", 5000, struct {
fn suite(alloc: std.mem.Allocator) !void {
try ztf.it(alloc, "test 1", test1);
try ztf.it(alloc, "test 2", test2);
}
}.suite);
// Global timeout configuration
const global*config = ztf.GlobalTimeoutConfig{
.default*timeout*ms = 5000,
.enabled = true,
.allow*extension = true,
.max*extension*ms = 30000,
};
var enforcer = ztf.TimeoutEnforcer.init(allocator, global*config);
defer enforcer.deinit();
// Timeout context for manual control
var context = ztf.TimeoutContext.init(allocator, 1000);
context.start();
// Extend timeout if needed
try context.extend(500);
// Check status
if (context.isTimedOut()) {
// Handle timeout
}
context.complete();
var result = try context.getResult();
defer result.deinit();# Run all tests
zig-test
# Show help
zig-test --help
# Use different reporter
zig-test --reporter dot
zig-test --reporter json
# Filter tests by name
zig-test --filter "user"
# Stop on first failure
zig-test --bail
# Disable colors
zig-test --no-color| Option | Alias | Description |
|---|---|---|
| --help | -h | Show help message |
| --version | -v | Show version information |
| --bail | -b | Stop on first failure |
| --filter <pattern> | Run only tests matching pattern | |
| --reporter <name> | Set reporter (spec, dot, json) | |
| --verbose | Enable verbose output | |
| --quiet | -q | Minimal output |
| --no-color | Disable colored output | |
| --test-dir <dir> | Directory to search for tests | |
| --pattern <pattern> | Test file pattern (default: *.test.zig) | |
| --no-recursive | Disable recursive directory search | |
| --coverage | Enable code coverage collection | |
| --coverage-dir <dir> | Coverage output directory (default: coverage) | |
| --coverage-tool <tool> | Coverage tool to use (kcov or grindcov) |
The framework integrates with external coverage tools to provide comprehensive code coverage reporting including line, branch, and function coverage.
Install one of the supported coverage tools:
kcov (recommended):
# macOS
brew install kcov
# Ubuntu/Debian
sudo apt-get install kcov
# From source
git clone https://github.com/SimonKagstrom/kcov.git
cd kcov
cmake . && make && sudo make installgrindcov:
# Install Valgrind first
brew install valgrind # macOS
sudo apt-get install valgrind # Ubuntu/Debian
# Then install grindcov
pip install grindcovEnable coverage collection with the --coverage flag when running tests:
# Enable coverage with default settings (kcov)
zig-test --test-dir tests --coverage
# Specify custom coverage directory
zig-test --test-dir tests --coverage --coverage-dir my-coverage
# Use grindcov instead of kcov
zig-test --test-dir tests --coverage --coverage-tool grindcovWhen coverage is enabled, you'll see a coverage summary after test execution:
Test Summary: Files run: 5 Passed: 5 Failed: 0 ==================== Coverage Summary ==================== Line Coverage: 245/300 (81.67%) Function Coverage: 45/50 (90.00%) Branch Coverage: 120/150 (80.00%) Coverage report: coverage/index.html ==========================================================
Coverage reports are generated in HTML format and can be viewed in your browser:
# Run tests with coverage
zig-test --test-dir tests --coverage
# Open the HTML report
open coverage/index.html # macOS
xdg-open coverage/index.html # LinuxThe HTML report provides:
Beautiful hierarchical output with colors:
Running 12 test(s)... Math operations ✓ should add two numbers (0.05ms) ✓ should subtract two numbers (0.03ms) ✓ should multiply two numbers (0.04ms) String operations ✓ should compare strings (0.02ms) ✓ should check substring (0.03ms) Test Summary: Total: 12 Passed: 12 Time: 1.23ms
Minimal output for CI environments:
Running 12 tests: ............ Passed: 12, Failed: 0, Total: 12 (1.23ms)
Machine-readable output for tooling:
{
"totalTests": 12,
"tests": [
{
"name": "should add two numbers",
"status": "passed",
"time": 0.05
}
],
"summary": {
"total": 12,
"passed": 12,
"failed": 0,
"skipped": 0,
"time": 1.23
}
}See docs/api.md for complete API documentation.
Check out the examples/ directory for comprehensive examples:
# Clone the repository
git clone https://github.com/zig-utils/zig-test-framework.git
cd zig-test-framework
# Build the framework
zig build
# Run self-tests
zig build test
# Run examples
zig build examplesComprehensive guides and API documentation:
Contributions are welcome! Please see CONTRIBUTING.md for guidelines.
For help, discussion about best practices, or any other conversation that would benefit from being searchable:
For casual chit-chat with others using this package:
Join the zig-utils Discord Server
MIT License - see LICENSE for details.
Inspired by:
Made with ❤️ for the Zig community
| Back | FazBrowse Home | New Git URL |