Carbonteq
Best Practices/Backend/Architecture/Domain

Testing Domain

Learn how to build robust test factories for domain entities using Effect Schema, Faker, and type-safe patterns

Entity Test Factory Patterns

Building robust test factories is crucial for maintaining reliable test suites. This guide demonstrates how to create sophisticated test factories for domain entities using Effect Schema, Faker, and type-safe patterns. We'll use a Host entity as our example, but these patterns apply to any domain entity.

Overview

Test factories solve the problem of creating realistic test data while maintaining type safety and flexibility. We'll explore how to build a comprehensive factory system that handles complex domain entities with optional fields, nested value objects, and probability-based generation.

The patterns demonstrated here work for any domain entity - whether it's a User, Product, Order, or in our case, a Host entity. The key principles remain consistent across different entity types.

1
Schema Foundation with Faker Annotations

Start by extending your base entity schema with faker-based annotations. This provides the foundation for generating realistic test data while maintaining type safety.

2
Handling Union Types with Members

Union types in Effect Schema require special handling. Use members[0] to access the first member of a union and add annotations to it.

3
Smart Optional Field Generation

Optional fields need special handling with S.encodedBoundSchema and probability-based generation. This allows you to control how often optional fields are populated.

4
Organizing Complex Generators

Move complex generation logic into dedicated objects for better organization and reusability. This keeps the schema clean and makes generators testable.

5
Nested Value Object Generators

Handle complex nested structures like addresses with dedicated generator objects. This maintains separation of concerns and makes the code more maintainable.

6
Base Generator Function

Create the main generator function that uses FastCheck to sample from your schema. This provides type-safe generation with override capabilities.

7
Scenario-Based Helper Methods

Build helper methods for common testing scenarios. These methods use the base generator internally but provide convenient presets for specific use cases.

8
Database Integration

Create database-specific factory functions that handle the repository pattern and proper type conversion for database insertion.

9
Entity Creation Utilities

Build Effect-based utilities for creating domain entities directly. This provides functional error handling and composability for complex test scenarios.

import { Host, HostSchema } from "@domain/entities/user/host.entity";
import { UUID } from "@domain/utils/refined.types";
import { faker } from "@faker-js/faker";
import { Arbitrary, Effect as E, FastCheck, Schema as S } from "effect";
import { refined } from "../utils/arbitrary.utils";
import type { SerializedHost } from "@domain/entities/user/host.entity";
export const TestHostSchema = S.Struct({
id: HostSchema.fields.id.annotations(refined.uuid()),
Generates valid UUID strings
createdAt: HostSchema.fields.createdAt.annotations(refined.dateTime.past()),
Creates realistic past timestamps
updatedAt: HostSchema.fields.updatedAt.annotations(refined.dateTime.recent()),
Creates recent timestamps
userId: HostSchema.fields.userId.annotations(refined.uuid()),
});

Key Takeaways

This approach ensures your test factories are maintainable, type-safe, and produce realistic test data that closely mirrors production scenarios across all your domain entities.

Essential Entity Testing Patterns

Now that we have robust test factories, let's explore essential testing patterns for domain entities. This section demonstrates a basic but comprehensive testing approach - a condensed version of what would typically be a much larger test suite. We'll focus on the key testing concepts every domain entity should cover.

1
Basic Entity Creation & Validation

Start with fundamental entity creation tests using your factory. Test both successful creation and validation failures to ensure your domain rules are properly enforced.

2
Factory Constraint Testing

Validate that your factory generates data within expected constraints. This ensures your test data remains realistic and doesn't accidentally violate domain rules.

3
Option Type Handling

Test optional fields thoroughly since they're common in domain entities. Verify both presence and absence scenarios work correctly.

4
Computed Properties & Business Logic

Test computed properties and business logic methods. These often contain complex domain rules that need thorough validation.

5
Factory Override Testing

Test factory overrides systematically to ensure your factories behave predictably when customizing test data for specific scenarios.

6
Error Handling & Edge Cases

Test validation errors and edge cases to ensure your domain boundaries are properly enforced.

7
Serialization & Data Integrity

Test serialization round-trips and data integrity to ensure entities can be safely stored and retrieved.

import { describe, expect, it } from "vitest";
import { Host } from "@domain/entities/user/host.entity";
import { HostValidationError } from "@domain/entities/user/host.error";
import { generateTestHost, createTestHostEntity } from "../../../factories/host.factory";
import { TestPatterns } from "../../../utils/test.helpers";
describe("Host Entity - Essential Patterns", () => {
describe("Entity Creation", () => {
it("should create valid entities using factory", () => {
const hostData = generateTestHost({
userId: "550e8400-e29b-41d4-a716-446655440001",
phoneNumber: 5551234567
});
const host = TestPatterns.Effect.expectSuccess(Host.create(hostData));
Unwrap Effect success values safely
expect(host).toBeInstanceOf(Host);
expect(host.userId).toBe("550e8400-e29b-41d4-a716-446655440001");
const phoneNumber = TestPatterns.Option.expectSome(host.phoneNumber);
Assert Option has value and extract it
expect(phoneNumber).toBe(5551234567);
});
it("should handle validation errors", () => {
const invalidData = generateTestHost({
phoneNumber: 123 // Invalid - too short
});
const error = TestPatterns.Effect.expectFailure(
Test validation failures safely
Host.create(invalidData),
HostValidationError
);
expect(error).toBeInstanceOf(HostValidationError);
});
});

Testing Strategy Summary

Essential Test Categories

Entity Creation & Validation: Test successful creation and validation failures to ensure domain rules are enforced. Use factory overrides to test specific scenarios.

Factory Constraints: Validate that test factories generate realistic data within domain boundaries. Test uniqueness and constraint compliance automatically.

Option Type Handling: Thoroughly test optional fields in both present and absent states. Use specialized helpers for safe Option unwrapping.

Business Logic: Test computed properties, domain methods, and business rules. Focus on the core logic that makes your entity valuable.

Error Scenarios: Test validation errors and edge cases systematically. Ensure domain boundaries are properly enforced.

Data Integrity: Test serialization round-trips and async operations. Verify entities maintain consistency through storage and retrieval.

Key Principles

  • Use Type-Safe Helpers: Leverage TestPatterns utilities for safe Effect and Option handling
  • Test Business Value: Focus on domain logic, computed properties, and business rules
  • Validate Constraints: Ensure factories generate realistic data and entities enforce boundaries
  • Systematic Coverage: Use automated helpers for constraint validation and override testing
  • Error Handling: Test both success and failure scenarios comprehensively

This basic testing approach provides comprehensive coverage while remaining maintainable and focused on the essential patterns every domain entity should validate.