Skip to content

Factory Guide

ZSpec Bot edited this page Nov 26, 2025 · 1 revision

Factory Guide

ZSpec includes a powerful Factory module inspired by Ruby's FactoryBot for generating test data.

Quick Reference

const Factory = zspec.Factory;

// Define a factory
const UserFactory = Factory.define(User, .{
    .id = Factory.sequence(u32),
    .email = Factory.sequenceFmt("user{d}@example.com"),
    .name = "John Doe",
    .age = 25,
});

// Create instances
const user1 = UserFactory.build(.{});
const user2 = UserFactory.build(.{ .name = "Jane" });

// Create variants with traits
const AdminFactory = UserFactory.trait(.{ .role = "admin" });

Core Features

Factory.define()

Define a factory with default values:

const User = struct {
    id: u32,
    name: []const u8,
    email: []const u8,
    active: bool,
};

const UserFactory = Factory.define(User, .{
    .id = 1,
    .name = "Test User",
    .email = "test@example.com",
    .active = true,
});

Factory.sequence()

Auto-incrementing numeric values:

const UserFactory = Factory.define(User, .{
    .id = Factory.sequence(u32),  // 1, 2, 3, ...
    .name = "User",
});

const user1 = UserFactory.build(.{});  // id: 1
const user2 = UserFactory.build(.{});  // id: 2
const user3 = UserFactory.build(.{});  // id: 3

Factory.sequenceFmt()

Formatted sequence strings:

const UserFactory = Factory.define(User, .{
    .id = Factory.sequence(u32),
    .email = Factory.sequenceFmt("user{d}@example.com"),
});

const user1 = UserFactory.build(.{});  // email: "user1@example.com"
const user2 = UserFactory.build(.{});  // email: "user2@example.com"

Important: Use arena allocator to avoid memory leaks:

test "with arena allocator" {
    var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    const user = UserFactory.buildWith(alloc, .{});
}

Factory.trait()

Create factory variants:

const UserFactory = Factory.define(User, .{
    .name = "John",
    .role = "user",
    .active = true,
});

const AdminFactory = UserFactory.trait(.{ .role = "admin" });
const InactiveFactory = UserFactory.trait(.{ .active = false });

const admin = AdminFactory.build(.{});      // role: "admin"
const inactive = InactiveFactory.build(.{}); // active: false

build() and buildPtr()

// Build value
const user = UserFactory.build(.{});

// Build pointer (heap-allocated)
const user_ptr = UserFactory.buildPtr(.{});
defer std.testing.allocator.destroy(user_ptr);

// With custom allocator
const user = UserFactory.buildWith(my_allocator, .{});
const user_ptr = UserFactory.buildPtrWith(my_allocator, .{});

Factory.resetSequences()

Reset all sequence counters:

test "tests:beforeAll" {
    Factory.resetSequences();
}

Advanced Features

Lazy Values

Compute values at build time:

const ItemFactory = Factory.define(Item, .{
    .value = Factory.lazy(getValue),
});

fn getValue() u32 {
    return 42;
}

Lazy Values with Allocator

const ItemFactory = Factory.define(Item, .{
    .data = Factory.lazyAlloc(getData),
});

fn getData(alloc: std.mem.Allocator) []const u8 {
    return alloc.dupe(u8, "computed") catch unreachable;
}

Associations

Create nested factories:

const AddressFactory = Factory.define(Address, .{
    .street = "123 Main St",
    .city = "Springfield",
});

const CompanyFactory = Factory.define(Company, .{
    .name = "Acme Inc",
    .address = Factory.assoc(AddressFactory),
});

const company = CompanyFactory.build(.{});
// company.address is automatically created

Common Patterns

Test Isolation with beforeAll

pub const MyTests = struct {
    test "tests:beforeAll" {
        Factory.resetSequences();
    }

    test "test 1" {
        const user = UserFactory.build(.{});
        // user.id == 1
    }

    test "test 2" {
        const user = UserFactory.build(.{});
        // user.id == 2 (sequence continues)
    }
};

Per-Test Reset

pub const MyTests = struct {
    test "tests:before" {
        Factory.resetSequences();
    }

    test "test 1" {
        const user = UserFactory.build(.{});
        // user.id == 1
    }

    test "test 2" {
        const user = UserFactory.build(.{});
        // user.id == 1 (reset each test)
    }
};

Factory Composition

// Base factory
const UserFactory = Factory.define(User, .{
    .id = Factory.sequence(u32),
    .name = "User",
    .role = "user",
});

// Specialized variants
const AdminFactory = UserFactory.trait(.{ .role = "admin" });
const ModeratorFactory = UserFactory.trait(.{ .role = "moderator" });
const GuestFactory = UserFactory.trait(.{ .role = "guest" });

// Chain traits
const InactiveAdminFactory = AdminFactory.trait(.{ .active = false });

Memory Management

Using Arena Allocators

Recommended for tests using sequenceFmt:

pub const MyTests = struct {
    var arena: std.heap.ArenaAllocator = undefined;
    var test_alloc: std.mem.Allocator = undefined;

    test "tests:before" {
        arena = std.heap.ArenaAllocator.init(std.heap.page_allocator);
        test_alloc = arena.allocator();
    }

    test "tests:after" {
        arena.deinit();
    }

    test "my test" {
        const user = UserFactory.buildWith(test_alloc, .{});
        // No manual cleanup needed
    }
};

Best Practices

  1. Always reset sequences at appropriate scope (beforeAll or before)
  2. Use traits for variants instead of duplicating factory definitions
  3. Use arena allocators with sequenceFmt to avoid leak reports
  4. Keep factories close to types - define factories near struct definitions
  5. Name factories consistently - [Type]Factory pattern
  6. Leverage sequences for unique identifiers

Examples

See examples/factory_test.zig for comprehensive examples.

See Also