Back in 2022, I created a project called AutoFaker, a Python library designed to minimize the setup and arrange phase of unit tests. Coming from a C# background, I really missed tools like AutoFixture and decided to build something similar for my Python projects.

Fast forward to today, and I find myself diving deep into a new language: Zig. As part of my learning journey, I wanted a substantial project to sink my teeth into. If I’m to be completely honest, this project came to life simply because I wanted to write something non-trivial in Zig, and not necessarily because there is a burning need for such a tool in the Zig ecosystem right now. However, it served as a fantastic learning exercise to explore Zig’s powerful memory management and its incredible comptime type reflection capabilities.

The result of this exercise is ZigFaker, the Zig version of my AutoFaker library, designed to make it significantly easier to write unit tests by reducing boilerplate setup.

When writing unit tests, you normally start by creating objects that represent the initial state of the test. This phase is called the arrange or setup phase. In most cases, the system you want to test will force you to specify much more information than you really care about, so you frequently end up creating objects with no influence on the test itself, just to satisfy the compiler.

ZigFaker helps by generating such anonymous or fake data for you automatically.

Installation

ZigFaker is built for Zig 0.15.0 or later. To use it in your project, add it to your build.zig.zon dependencies by fetching the archive:

zig fetch --save https://github.com/christianhelle/zigfaker/archive/refs/heads/main.tar.gz

Then, in your build.zig, add the module to your test step:

const zigfaker_dep = b.dependency("zigfaker", .{
    .target = target,
    .optimize = optimize,
});
const zigfaker_mod = zigfaker_dep.module("zigfaker");

// Add to your test executable:
exe_tests.root_module.addImport("zigfaker", zigfaker_mod);

Supported Data Types

Leveraging Zig’s comptime, ZigFaker recursively inspects your types and populates them. Currently, it supports creating anonymous variables for:

  • Primitives: i8 through i64, u8 through u64, f32, f64, and bool
  • Strings: []const u8 (Generates UUID-like strings or contextual fake data)
  • Structs: Fully recursive population, including nested structs
  • Optionals: ?T (Randomly evaluates to null or a generated T)
  • Enums: Randomly selects a valid variant
  • Arrays: [N]T fills all elements

Example Usages

To use ZigFaker, you simply initialize it with an allocator. Because it needs to generate dynamic strings, it uses an ArenaAllocator under the hood to make cleanup a breeze when deinit() is called.

Anonymous Primitive Types

Creating basic built-in types is straightforward:

const std = @import("std");
const zigfaker = @import("zigfaker");

test "create anonymous primitives" {
    var faker = zigfaker.ZigFaker.init(std.testing.allocator);
    defer faker.deinit();

    const id     = try faker.create(i32);     // e.g. 1453820643
    const score  = try faker.create(f64);     // e.g. 812345.67
    const active = try faker.create(bool);    // e.g. true
    const token  = try faker.create([]const u8); // e.g. "a3f1b2c4-9e8d-7f6a-5b4c-3d2e1f0a9b8c"
}

Anonymous Structs

The real power of ZigFaker comes when you need to populate entire structs. You don’t need to specify anything about the struct’s internals; ZigFaker figures it out at compile time.

const User = struct {
    id: i32,
    score: f64,
    active: bool,
};

test "create anonymous struct" {
    var faker = zigfaker.ZigFaker.init(std.testing.allocator);
    defer faker.deinit();

    const user = try faker.create(User);
    // user.id, user.score, and user.active are all populated with random values
}

Creating Collections

If you need a list of dummy data, createMany has you covered. The resulting slice is owned by the ZigFaker arena, so you don’t need to free it manually.

test "create multiple users" {
    var faker = zigfaker.ZigFaker.init(std.testing.allocator);
    defer faker.deinit();

    const users = try faker.createMany(User, 3);
    // users is a []User with 3 elements, all automatically populated
}

Realistic Fake Data

There are times when completely random, anonymous UUID-like strings don’t make much sense—especially in data-centric scenarios or integration tests where you might want to look at the data being passed around.

ZigFaker handles this gracefully. By initializing it with initWithFakeData, it looks at your struct’s field names (e.g., first_name, email, ipv4) and generates contextually appropriate string data.

const Person = struct {
    id: i32,
    first_name: []const u8,
    last_name: []const u8,
    job: []const u8,
    email: []const u8,
    city: []const u8,
    country: []const u8,
    ipv4: []const u8,
    ipv6: []const u8,
    hostname: []const u8,
    currency_name: []const u8,
    currency_code: []const u8,
};

test "create realistic fake data" {
    var faker = zigfaker.ZigFaker.initWithFakeData(std.testing.allocator);
    defer faker.deinit();

    const person = try faker.create(Person);

    // person.first_name => "Jennifer"
    // person.last_name  => "Martinez"
    // person.job        => "Cloud Architect"
    // person.email      => "jenniferw42@gmail.com"
    // person.city       => "San Francisco"
    // person.country    => "Germany"
    // person.ipv4       => "192.168.24.100"
    // person.ipv6       => "8f3c:0a2b:4d1e:7f9c:1b2a:3e4d:5f6c:7a8b"
    // person.hostname   => "api12.smith.io"
}

Nested Structs

The comptime reflection naturally extends to nested structures without any additional configuration.

const Address = struct {
    street: []const u8,
    city: []const u8,
    country: []const u8,
};

const Employee = struct {
    id: i32,
    first_name: []const u8,
    last_name: []const u8,
    job: []const u8,
    address: Address,
};

test "create nested struct" {
    var faker = zigfaker.ZigFaker.initWithFakeData(std.testing.allocator);
    defer faker.deinit();

    const emp = try faker.create(Employee);
    // emp.address.city => "Los Angeles"
    // emp.address.country => "Canada"
}

Enums and Optionals

Handling variants and nullable types is baked right in:

const Status = enum { pending, active, suspended, closed };

test "enums and optionals" {
    var faker = zigfaker.ZigFaker.init(std.testing.allocator);
    defer faker.deinit();

    const status = try faker.create(Status); // Randomly picks one of the four variants
    const maybe_id = try faker.create(?i32); // Randomly evaluates to null or a valid i32
}

Reproducible Output

Finally, flaky unit tests are the absolute worst. If you need your randomly generated data to be deterministic across test runs, you can seed the generator:

test "seeded generation" {
    var faker = zigfaker.ZigFaker.initWithSeed(std.testing.allocator, 42);
    defer faker.deinit();

    const v1 = try faker.create(i32); // This will always generate the same value for seed 42
}

Building ZigFaker was an excellent crash course in Zig’s comptime reflection. While it might have started simply as an excuse to write more Zig code, I genuinely believe it can cut down on the boilerplate required for heavily data-driven tests in Zig.

You can check out the full source code and documentation on GitHub: christianhelle/zigfaker.