Testing
Testing follows Tiger Style: each phase is fully tested before proceeding. Tests are written first, implementations make them pass.
TDD Workflow
1. Write failing test for new behavior
2. Implement minimal code to pass
3. Refactor with confidence
4. Repeat
Every phase in the roadmap has explicit test requirements. No phase is complete until all its tests pass.
Test Categories
Unit Tests
Fast, isolated tests for individual functions and types.
test "buffer handle allocation" {
var handles = BufferHandles.init(testing.allocator);
defer handles.deinit();
const h1 = handles.alloc();
const h2 = handles.alloc();
try testing.expect(h1 != h2);
handles.free(h1);
const h3 = handles.alloc();
try testing.expectEqual(h1, h3); // reuse
}
Coverage targets:
- Handle/resource allocation
- State machine transitions
- Error paths
- Boundary conditions
Integration Tests
Test JS↔Zig round-trips and WebGL shim workflows.
test "JS sets clear color" {
var rt = try Runtime.init(testing.allocator, 256 * 1024);
defer rt.deinit();
try rt.eval("setClearColor(1.0, 0.0, 0.0)");
const color = rt.getClearColor();
try testing.expectApproxEqAbs(@as(f32, 1.0), color.r, 0.001);
try testing.expectApproxEqAbs(@as(f32, 0.0), color.g, 0.001);
}
Coverage targets:
- JS function bindings
- WebGL call sequences
- Asset loading paths
- Error propagation
Screenshot Tests
Render and compare against golden images.
test "triangle renders correctly" {
var renderer = try TestRenderer.init();
defer renderer.deinit();
renderer.drawTriangle();
const pixels = renderer.readPixels();
try testing.expectScreenshotMatch("golden/triangle.png", pixels, .{
.tolerance = 0.01, // 1% pixel difference allowed
});
}
Why tolerances:
- Driver differences
- Floating point variance
- Platform antialiasing
Golden image updates:
zig build test -- --update-golden
Fuzz Tests
Test robustness against malformed inputs.
test "fuzz: GLTF parser doesn't crash" {
try testing.fuzz(struct {
fn run(input: []const u8) !void {
_ = gltf.parse(input) catch return;
}
}, .{});
}
Fuzz targets:
- GLTF parser
- Image decoders
- JS eval (if applicable)
- Any binary format handling
Run extended fuzzing:
zig build test --fuzz -- --duration 3600
Benchmark Tests
Validate performance meets napkin math estimates.
test "benchmark: context switch under 200ns" {
const result = testing.benchmark(struct {
fn run() void {
runtime.contextSwitch();
}
}, .{});
try testing.expect(result.mean_ns < 200);
}
Benchmarks track:
- WebGL call overhead
- JS eval latency
- Frame time budget
- Memory allocation rate
Test Infrastructure
Directory Structure
tests/
├── unit/
│ ├── handles_test.zig
│ ├── state_machine_test.zig
│ └── ...
├── integration/
│ ├── webgl_test.zig
│ ├── runtime_test.zig
│ └── ...
├── screenshot/
│ ├── triangle_test.zig
│ ├── cube_test.zig
│ └── golden/
│ ├── triangle.png
│ └── cube.png
├── fuzz/
│ ├── gltf_fuzz.zig
│ └── ...
└── fixtures/
├── simple.js
├── webgl_cube.js
└── ...
Running Tests
# All tests
zig build test
# Unit tests only
zig build test -- --filter unit
# Screenshot tests (requires display)
zig build test -- --filter screenshot
# With verbose output
zig build test -- -v
# With memory leak detection
zig build test -- --detect-leaks
CI Configuration
| Platform | Test Scope |
|---|---|
| Linux (every push) | Unit, integration, memory leaks |
| Linux + GPU (PR) | All including screenshot |
| macOS (PR) | Unit, integration |
| Windows (PR) | Unit, integration |
Memory Leak Detection
All tests run with GeneralPurposeAllocator in debug mode:
test "no memory leaks" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer {
const leaked = gpa.deinit();
if (leaked) @panic("memory leak detected");
}
// test code using gpa.allocator()
}
Failure Diagnostics
When a test fails:
- Context dump — Last N WebGL calls logged
- JS exception — Stack trace if JS threw
- Screenshot — Actual render saved (screenshot tests)
- Diff image — Visual diff against golden (screenshot tests)
- State snapshot — GL state machine dump
Example failure output:
test "cube renders correctly" FAILED
Expected: golden/cube.png
Actual: /tmp/test_output/cube_actual.png
Diff: /tmp/test_output/cube_diff.png
Pixel difference: 5.2% (threshold: 1.0%)
Last 5 WebGL calls:
gl.bindBuffer(ARRAY_BUFFER, 3)
gl.bufferData(ARRAY_BUFFER, 288, STATIC_DRAW)
gl.vertexAttribPointer(0, 3, FLOAT, false, 12, 0)
gl.enableVertexAttribArray(0)
gl.drawArrays(TRIANGLES, 0, 36)
Test-First Development
For each new feature:
-
Write the test first
test "gl.createBuffer returns valid handle" { var gl = try WebGLContext.init(); const buf = gl.createBuffer(); try testing.expect(buf != 0); } -
Watch it fail — Confirms test is testing the right thing
-
Implement — Minimum code to pass
-
Refactor — Clean up with passing tests as safety net
-
Commit — Test and implementation together
This ensures:
- No untested code
- Tests document expected behavior
- Regressions caught immediately