Factories + Matchers Together#
Factories and matchers are two sides of the same coin. Together, they create a powerful testing pattern.
The Pattern#
Factories: Generate test data with defaults → “Build with what matters”
Matchers: Validate with partial matching → “Assert what matters”
func TestUserCreation(t *testing.T) {
p := specta.New()
// Build: specify only what we're setting
input := factory.User().
Name("Alice").
Email("alice@example.com").
Build(p)
result := CreateUser(input)
// Match: assert only what we care about
specta.AssertThat(t, result, factory.UserMatches().
Name(specta.Equal("Alice")).
Email(specta.Equal("alice@example.com")).
ID(specta.Not(specta.IsEmpty[string]())))
}Why This Works#
Clear Test Dependencies#
When you add a new field to User:
type User struct {
ID string
Name string
Email string
Age int
CreatedAt time.Time
UpdatedAt time.Time
LastLogin time.Time // NEW FIELD
}Without factories/matchers:
- Tests creating
User{}get zero value forLastLogin - Unclear if that’s intentional or an oversight
- Hard to tell what each test actually depends on
- If the new field needs a non-zero default, you must update every test
With factories/matchers:
- Factory provides a sensible default for
LastLoginin one place - Tests continue working with reasonable data
- Clear which fields each test cares about (only those in the matcher)
- One update to the factory helper if needed, not N test updates
Focus Tests on Intent#
Each test asserts exactly what it’s testing:
// Test 1: Only care about name normalization
func TestNameNormalization(t *testing.T) {
p := specta.New()
user := CreateUser(factory.User().
Name(" ALICE ").
Build(p))
specta.AssertThat(t, user, factory.UserMatches().
Name(specta.Equal("Alice")))
}
// Test 2: Only care about email domain validation
func TestEmailDomain(t *testing.T) {
p := specta.New()
user := CreateUser(factory.User().
Email("alice@company.com").
Build(p))
specta.AssertThat(t, user, factory.UserMatches().
Email(specta.HasSuffix("@company.com")))
}Practical Examples#
Testing CRUD Operations#
func TestCreateUser(t *testing.T) {
p := specta.New()
db := setupTestDB(t)
// Arrange: Build input
userData := factory.User().
Name("Alice").
Email("alice@example.com").
Build(p)
// Act
created := db.CreateUser(userData)
// Assert: Validate result
specta.AssertThat(t, created, factory.UserMatches().
ID(specta.Not(specta.IsEmpty[string]())). // DB generated
Name(specta.Equal(userData.Name)).
Email(specta.Equal(userData.Email)).
CreatedAt(specta.Not(specta.IsZero[time.Time]())))
}
func TestUpdateUser(t *testing.T) {
p := specta.New()
db := setupTestDB(t)
// Create initial user
user := db.CreateUser(factory.User().Build(p))
// Update name
updates := factory.User().
Name("New Name").
Build(p)
updated := db.UpdateUser(user.ID, updates)
// Assert only what changed
specta.AssertThat(t, updated, factory.UserMatches().
ID(specta.Equal(user.ID)). // Same ID
Name(specta.Equal(updates.Name)). // Updated
CreatedAt(specta.Equal(user.CreatedAt)). // Unchanged
UpdatedAt(specta.GreaterThan(user.UpdatedAt)))
}API Response Validation#
func TestGetUserAPI(t *testing.T) {
p := specta.New()
// Setup: Create user in DB
user := factory.User().
Name("Alice").
Email("alice@example.com").
Build(p)
db.CreateUser(user)
// Make API request
resp := httpGet(t, "/api/users/"+user.ID)
// Parse response
var result User
json.Unmarshal(resp.Body, &result)
// Validate response
specta.AssertThat(t, result, factory.UserMatches().
ID(specta.Equal(user.ID)).
Name(specta.Equal(user.Name)).
Email(specta.Equal(user.Email)))
}Note: You could extract this matcher into a reusable function:
func MatchesDBUser(user User) specta.Matcher[User] {
return factory.UserMatches().
ID(specta.Equal(user.ID)).
Name(specta.Equal(user.Name)).
Email(specta.Equal(user.Email))
}
// Usage
specta.AssertThat(t, result, MatchesDBUser(user))This is especially useful when testing multiple API endpoints that return the same user representation.
Advanced Patterns#
Table-Driven Tests#
func TestValidation(t *testing.T) {
p := specta.New()
tests := []struct {
name string
user User
wantError bool
}{
{
name: "valid user",
user: factory.User().
Email("valid@example.com").
Build(p),
wantError: false,
},
{
name: "invalid email",
user: factory.User().
Email("invalid").
Build(p),
wantError: true,
},
{
name: "missing name",
user: factory.User().
Name("").
Build(p),
wantError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateUser(tt.user)
if tt.wantError {
specta.AssertThat(t, err, specta.IsError())
} else {
specta.AssertThat(t, err, specta.NoErr())
}
})
}
}Recursive Partial Matching with AsEqualMatcher#
One of the most powerful patterns is using AsEqualMatcher() with nested recipes. Partial matching works recursively - if you set specific fields on nested types, only those nested fields will be checked:
func TestOrderWithPartialUserMatch(t *testing.T) {
p := specta.New()
// Create test data - full objects with all fields
order := factory.Order().
Total(150.0).
Status("pending").
UserFromRecipe(
factory.User().
FirstName("Alice").
Email("alice@example.com"),
).
Build(p)
// Act
result := processOrder(order)
// Assert with partial matching - only check what matters
// This checks:
// - Order.Status
// - Order.User.FirstName (ONLY this user field)
// Everything else (Order.Total, Order.ID, User.Email, User.LastName, etc.) is ignored
specta.AssertThat(t, result, factory.Order().
Status("completed").
UserFromRecipe(
factory.User().FirstName("Alice"), // Only FirstName is checked
).
AsEqualMatcher())
}How it differs from DeepEqual:
// DeepEqual checks EVERYTHING
user := factory.User().FirstName("Alice").Build(p)
specta.AssertThat(t, result, specta.DeepEqual(user))
// Fails if ANY field differs (ID, Email, LastName, CreatedAt, etc.)
// AsEqualMatcher checks ONLY what you set
matcher := factory.User().FirstName("Alice").AsEqualMatcher()
specta.AssertThat(t, result, matcher)
// Only checks FirstName, ignores all other fieldsThis recursive partial matching is especially useful for:
- Testing data transformations - Only assert what the transformation should change
- API response validation - Check critical fields, ignore server-generated metadata
- Integration tests - Focus on business logic, ignore infrastructure details
When to use each approach:
- Explicit matchers (
factory.UserMatches().FirstName(...)) - When you want fine-grained control over matching logic (e.g.,Contains,GreaterThan) - AsEqualMatcher - When you want partial equality checking with nested objects
- DeepEqual - When you need exact equality of entire objects
Benefits Summary#
Factories + Matchers provide:
- Less duplication - Reusable test data and assertions
- Focused tests - Each test asserts its specific concern
- Resilient to change - Adding fields doesn’t break tests
- Readable - Clear intent, less noise
- Deterministic - Reproducible test data
- Composable - Build complex scenarios from simple pieces
Next Steps#
- Property-Based Testing - Use factories for PBT
- Advanced Topics - Custom matchers, patterns
- Examples - More real-world examples