Testing
Voltaire uses a strict TDD approach. Tests are first-class citizens, never stubs or placeholders.
Core Principle
Every line correct. No stubs, no commented tests.If a test exists, it passes. If it doesn’t pass, fix it or delete it.
Test Organization
Zig Tests
Inline in source files:
src/primitives/Address/address.zig # Implementation + tests
src/crypto/Keccak256/keccak256.zig # Implementation + tests
TypeScript Tests
Separate test files:
src/primitives/Address/Address.test.ts
src/primitives/Address/Address.wasm.test.ts
src/crypto/Keccak256/Keccak256.test.ts
Benchmarks
src/primitives/Address/address.bench.zig # Zig benchmarks (zbench)
src/primitives/Address/Address.bench.ts # TS benchmarks (mitata)
Running Tests
Zig Tests
# All Zig tests
zig build test
# Filter by name
zig build -Dtest-filter=Address
zig build -Dtest-filter=keccak
zig build -Dtest-filter="fromHex"
# With debug output
zig build test 2>&1 | head -100
TypeScript Tests
# Watch mode (development)
bun run test
# Single run
bun run test:run
# Filter tests
bun run test -- address
bun run test -- "Address.fromHex"
# Coverage report
bun run test:coverage
# Specific test suites
bun run test:native # Native FFI
bun run test:wasm # WASM
TDD Workflow
The Loop
- Write failing test
- Implement minimal code to pass
- Refactor
- Run
zig build && zig build test
- Repeat
# Keep this running in a terminal
while true; do zig build && zig build test && bun run test:run; sleep 2; done
Bug Fixing
Always produce a failing test first:
test "regression: fromHex handles mixed case" {
// This was failing before the fix
const addr = try Address.fromHex("0x742D35Cc6634c0532925A3b844Bc9e7595f251E3");
try testing.expectEqual(@as(u8, 0x74), addr.bytes[0]);
}
Then fix the implementation. The test stays forever.
Zig Test Patterns
Basic Assertion
const testing = std.testing;
test "Address.fromHex valid input" {
const addr = try Address.fromHex("0x742d35cc6634c0532925a3b844bc9e7595f251e3");
try testing.expectEqual(@as(u8, 0x74), addr.bytes[0]);
try testing.expectEqual(@as(u8, 0x2d), addr.bytes[1]);
}
Error Testing
test "Address.fromHex rejects invalid length" {
const result = Address.fromHex("0x123");
try testing.expectError(error.InvalidLength, result);
}
test "Address.fromHex rejects invalid characters" {
const result = Address.fromHex("0xgggggggggggggggggggggggggggggggggggggggg");
try testing.expectError(error.InvalidHexDigit, result);
}
Slice Comparison
test "Address.toHex output" {
const addr = Address{ .bytes = [_]u8{0x74} ++ [_]u8{0} ** 19 };
const hex = addr.toHex();
try testing.expectEqualSlices(u8, "0x7400000000000000000000000000000000000000", &hex);
}
Debug Output
test "complex operation" {
testing.log_level = .debug;
const result = try complexOperation();
std.log.debug("result: {x}", .{result});
try testing.expect(result.len > 0);
}
Memory Testing
test "no memory leaks" {
var gpa = std.heap.GeneralPurposeAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();
const result = try encode(allocator, data);
defer allocator.free(result);
// If we get here without leak detection, test passes
}
TypeScript Test Patterns
Basic Test
import { describe, it, expect } from "vitest";
import * as Address from "./index.js";
describe("Address", () => {
describe("fromHex", () => {
it("converts valid lowercase hex", () => {
const addr = Address.fromHex("0x742d35cc6634c0532925a3b844bc9e7595f251e3");
expect(addr).toBeInstanceOf(Uint8Array);
expect(addr.length).toBe(20);
});
it("converts valid checksummed hex", () => {
const addr = Address.fromHex("0x742d35Cc6634C0532925a3b844Bc9e7595f251e3");
expect(addr[0]).toBe(0x74);
});
});
});
Error Testing
describe("fromHex", () => {
it("throws on invalid hex", () => {
expect(() => Address.fromHex("0xinvalid")).toThrow();
});
it("throws InvalidAddressError", () => {
expect(() => Address.fromHex("0x123")).toThrow(InvalidAddressError);
});
it("throws with descriptive message", () => {
expect(() => Address.fromHex("bad")).toThrow(/invalid.*address/i);
});
});
Async Testing
describe("async operations", () => {
it("resolves with valid data", async () => {
const result = await fetchAddress(id);
expect(result).toBeDefined();
});
it("rejects on network error", async () => {
await expect(fetchAddress("invalid")).rejects.toThrow();
});
});
Table-Driven Tests
describe("toChecksummed", () => {
const cases = [
{ input: "0x742d35cc6634c0532925a3b844bc9e7595f251e3", expected: "0x742d35Cc6634C0532925a3b844Bc9e7595f251e3" },
{ input: "0x0000000000000000000000000000000000000000", expected: "0x0000000000000000000000000000000000000000" },
{ input: "0xffffffffffffffffffffffffffffffffffffffff", expected: "0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF" },
];
it.each(cases)("checksums $input", ({ input, expected }) => {
const addr = Address.fromHex(input);
expect(Address.toChecksummed(addr)).toBe(expected);
});
});
Cross-Validation
Validate against known-good vectors (no external libs):
import * as Address from "./index.js";
describe("checksum test vectors", () => {
const cases = [
["0x742d35cc6634c0532925a3b844bc9e7595f251e3", "0x742d35Cc6634C0532925a3b844Bc9e7595f251e3"],
["0x0000000000000000000000000000000000000000", "0x0000000000000000000000000000000000000000"],
["0xffffffffffffffffffffffffffffffffffffffff", "0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF"],
] as const;
it.each(cases)("checksums %s", (input, expected) => {
const ours = Address.toChecksummed(Address.fromHex(input));
expect(ours).toBe(expected);
});
});
Test Coverage
Generating Coverage
# TypeScript coverage
bun run test:coverage
# View report
open coverage/index.html
Coverage Goals
- Core primitives: 100%
- Crypto functions: 100%
- Edge cases: exhaustive
- Error paths: covered
Fuzz Testing
Zig Fuzz Tests
// address.fuzz.zig
const std = @import("std");
const Address = @import("address.zig").Address;
test "fuzz: fromHex roundtrip" {
var input: [40]u8 = undefined;
// Generate random hex
var rng = std.Random.DefaultPrng.init(0);
for (&input) |*c| {
const idx = rng.random().int(u4);
c.* = "0123456789abcdef"[idx];
}
const hex = "0x" ++ input;
const addr = try Address.fromHex(hex);
const output = addr.toHex();
try std.testing.expectEqualSlices(u8, &hex, &output);
}
Running Fuzz Tests
# Enable fuzzing
zig build test -Dfuzz=true
Security Testing
# Run security-focused tests
zig build test-security
# Constant-time verification
zig build test -Dtest-filter=secure
What Security Tests Cover
- Constant-time comparisons
- No timing leaks
- Input validation
- Memory clearing after sensitive ops
- Edge case handling
Benchmarks
Zig Benchmarks
// address.bench.zig
const zbench = @import("zbench");
pub fn benchFromHex(b: *zbench.Benchmark) void {
const hex = "0x742d35cc6634c0532925a3b844bc9e7595f251e3";
b.run(struct {
fn f() void {
_ = Address.fromHex(hex) catch unreachable;
}
}.f);
}
TypeScript Benchmarks
// Address.bench.ts
import { bench, run } from "mitata";
import * as Address from "./index.js";
bench("Address.fromHex", () => {
Address.fromHex("0x742d35cc6634c0532925a3b844bc9e7595f251e3");
});
bench("Address.toChecksummed", () => {
Address.toChecksummed(addr);
});
await run();
Running Benchmarks
# Zig benchmarks
zig build bench -Dwith-benches=true
# TypeScript benchmarks
bun run bench
Common Mistakes
Avoid these testing anti-patterns:
// ❌ Commented out test (remove or implement)
// it("handles edge case", () => {
// // implement here when ready
// });
// ❌ Empty test
it("does something", () => {});
// ❌ Test with placeholder
it("validates input", () => {
// add real assertions here
expect(true).toBe(true);
});
// ❌ Skipped test
it.skip("broken test", () => { ... });
All of these should either be:
- Implemented properly
- Deleted entirely