Advanced Usage Guide
Advanced patterns and techniques for getting the most out of factorio.
Complex Field Compositions
Nested Collections
Combine collection fields to create complex data structures:
from factorio import fields
from factorio.factories import Factory
@dataclass
class Matrix:
rows: list[list[int]]
class MatrixFactory(Factory[Matrix]):
rows = fields.ListField(
field=fields.ListField(
field=fields.IntegerField(min_value=0, max_value=100),
length=3
),
length=3
)
# Generates 3x3 matrices like:
# [[45, 67, 23], [12, 89, 34], [56, 78, 90]]
matrix = MatrixFactory.build()
assert len(matrix.rows) == 3
assert all(len(row) == 3 for row in matrix.rows)
Mixed-Type Dictionaries
Create dictionaries with varied key/value types:
@dataclass
class Config:
settings: dict[str, object]
class ConfigFactory(Factory[Config]):
settings = fields.DictField(
key_field=fields.StringField(min_chars=3, max_chars=10),
value_field=fields.ChoiceField([
fields.IntegerField(min_value=1, max_value=100),
fields.TextField("word"),
fields.BooleanField(),
]),
length=5
)
config = ConfigFactory.build()
for key, value in config.settings.items():
assert isinstance(key, str)
assert isinstance(value, (int, str, bool))
Note: The above won't work directly because ChoiceField expects values, not fields. Instead:
class ConfigFactory(Factory[Config]):
settings = fields.DictField(
key_field=fields.StringField(min_chars=3, max_chars=10),
value_field=fields.TextField("word"), # All string values
length=5
)
For truly mixed types, use a custom approach:
class FlexibleConfigFactory(Factory[Config]):
settings = fields.ConstantField(None)
@classmethod
def build(cls, **kwargs):
instance = super().build(**kwargs)
# Generate mixed-type dict manually
instance.settings = {
"count": fields.IntegerField(min_value=1, max_value=100)(),
"name": fields.TextField("name")(),
"active": fields.BooleanField()(),
"score": fields.FloatField(min_value=0, max_value=100)(),
}
return instance
Dynamic Field Selection
Conditional Fields Based on Context
Choose different field strategies based on test context:
class UserProfileFactory(Factory[UserProfile]):
name = fields.TextField("name")
bio = fields.ConstantField(None)
@classmethod
def build_complete(cls, **kwargs):
"""Build with all fields populated."""
return cls.build(
bio=fields.TextField("paragraph", nb_sentences=3),
**kwargs
)
@classmethod
def build_minimal(cls, **kwargs):
"""Build with minimal fields."""
return cls.build(bio=None, **kwargs)
# Usage
complete_profile = UserProfileFactory.build_complete()
assert complete_profile.bio is not None
minimal_profile = UserProfileFactory.build_minimal()
assert minimal_profile.bio is None
Runtime Field Configuration
Configure fields at runtime:
def create_user_factory(age_range: tuple[int, int]):
"""Create a factory with custom age range."""
min_age, max_age = age_range
class DynamicUserFactory(Factory[User]):
name = fields.TextField("name")
email = fields.TextField("email")
age = fields.IntegerField(min_value=min_age, max_value=max_age)
return DynamicUserFactory
# Create specialized factories
teen_factory = create_user_factory((13, 19))
adult_factory = create_user_factory((18, 65))
senior_factory = create_user_factory((60, 100))
teen = teen_factory.build()
assert 13 <= teen.age <= 19
adult = adult_factory.build()
assert 18 <= adult.age <= 65
Advanced Override Patterns
Partial Object Updates
Update only specific fields while keeping others generated:
class UserFactory(Factory[User]):
name = fields.TextField("name")
email = fields.TextField("email")
age = fields.IntegerField(min_value=18, max_value=65)
role = fields.ChoiceField(["user", "admin", "moderator"])
def test_role_permissions():
# Start with random user
user = UserFactory.build()
# Override just the role
admin = UserFactory.build(role="admin")
moderator = UserFactory.build(role="moderator")
assert admin.role == "admin"
assert moderator.role == "moderator"
# Other fields are still randomized
Cascading Overrides
Override fields that affect other fields:
class PricingFactory(Factory[Pricing]):
base_price = fields.DecimalField(min_value=10, max_value=100, accuracy=2)
discount_percent = fields.IntegerField(min_value=0, max_value=50)
final_price = fields.ConstantField(None)
@classmethod
def build(cls, **kwargs):
instance = super().build(**kwargs)
# Calculate final price based on other fields
discount_multiplier = 1 - (instance.discount_percent / 100)
instance.final_price = instance.base_price * Decimal(discount_multiplier)
return instance
pricing = PricingFactory.build()
expected = pricing.base_price * (1 - pricing.discount_percent / 100)
assert abs(pricing.final_price - expected) < Decimal("0.01")
Performance Optimization
Lazy Factory Initialization
Defer expensive factory setup:
class ExpensiveFactory(Factory[Model]):
# Expensive field generation
data = fields.TextField("paragraphs", nb=10)
_cache = {}
@classmethod
def build_cached(cls, cache_key: str = "default"):
if cache_key not in cls._cache:
cls._cache[cache_key] = cls.build()
return cls._cache[cache_key]
# Reuse cached instances when appropriate
obj1 = ExpensiveFactory.build_cached()
obj2 = ExpensiveFactory.build_cached()
assert obj1 is obj2 # Same instance
Warning: Only cache immutable objects or when you don't modify them!
Batch Generation with Generators
For very large datasets, use generators:
def user_generator(count: int):
"""Generate users lazily."""
for _ in range(count):
yield UserFactory.build()
# Process one at a time (memory efficient)
for user in user_generator(10000):
process(user)
# Only one user in memory at a time
Testing Strategies
Property-Based Testing
Combine factorio with property-based testing:
import hypothesis
from hypothesis import given
@given(
name=hypothesis.strategies.text(min_size=1, max_size=100),
age=hypothesis.integers(min_value=0, max_value=150),
)
def test_user_properties(name: str, age: int):
user = UserFactory.build(name=name, age=age)
# Properties should always hold
assert user.name == name
assert user.age == age
assert isinstance(user.email, str) # Generated field
Boundary Testing
Test edge cases systematically:
def test_boundary_ages():
boundary_cases = [0, 1, 17, 18, 64, 65, 150]
for age in boundary_cases:
user = UserFactory.build(age=age)
assert user.age == age
def test_empty_strings():
user = UserFactory.build(name="", bio="")
assert user.name == ""
assert user.bio == ""
def test_maximum_values():
product = ProductFactory.build(
price=fields.DecimalField(max_value=Decimal("999999.99"))
)
assert product.price <= Decimal("999999.99")
Regression Testing
Save and replay specific test cases:
import json
def test_with_saved_case():
# Load previously saved test case
with open("test_cases/user_case_1.json") as f:
case_data = json.load(f)
user = UserFactory.build(**case_data)
# Verify behavior with known inputs
assert user.is_adult == (user.age >= 18)
def save_test_case(user: User, case_name: str):
"""Save a test case for regression testing."""
case_data = {
"name": user.name,
"age": user.age,
"email": user.email,
}
with open(f"test_cases/{case_name}.json", "w") as f:
json.dump(case_data, f, indent=2)
Integration Patterns
Factory Composition
Compose multiple factories for complex scenarios:
class OrderSystem:
def __init__(self):
self.user_factory = UserFactory
self.product_factory = ProductFactory
self.order_factory = OrderFactory
def create_complete_order(self):
user = self.user_factory.build()
products = [self.product_factory.build() for _ in range(3)]
order = self.order_factory.build(
customer=user,
items=products
)
return order
system = OrderSystem()
order = system.create_complete_order()
Factory Registry Pattern
Centralize factory management:
class FactoryRegistry:
_factories: dict[type, type[Factory]] = {}
@classmethod
def register(cls, model: type, factory: type[Factory]):
cls._factories[model] = factory
@classmethod
def get(cls, model: type) -> type[Factory]:
if model not in cls._factories:
raise ValueError(f"No factory registered for {model}")
return cls._factories[model]
@classmethod
def build(cls, model: type, **kwargs):
factory = cls.get(model)
return factory.build(**kwargs)
# Register factories
FactoryRegistry.register(User, UserFactory)
FactoryRegistry.register(Product, ProductFactory)
FactoryRegistry.register(Order, OrderFactory)
# Build instances
user = FactoryRegistry.build(User, name="John")
product = FactoryRegistry.build(Product, price=29.99)
Mock Integration
Use factories with mocking libraries:
from unittest.mock import Mock, patch
def test_with_mock():
# Create realistic mock data
user = UserFactory.build()
# Mock external service
with patch("myapp.services.UserService") as mock_service:
mock_service.get_user.return_value = user
# Test code that uses the service
result = my_function_that_uses_service()
assert result.name == user.name
mock_service.get_user.assert_called_once()
Debugging Techniques
Field Inspection
Inspect what fields a factory will generate:
def inspect_factory(factory_class: type[Factory]):
"""Print all fields defined in a factory."""
print(f"Factory: {factory_class.__name__}")
print(f"Model: {factory_class.get_model().__name__}")
print("Fields:")
for name, value in factory_class.__dict__.items():
if isinstance(value, AbstractField):
print(f" - {name}: {type(value).__name__}")
inspect_factory(UserFactory)
# Output:
# Factory: UserFactory
# Model: User
# Fields:
# - name: TextField
# - age: IntegerField
# - email: TextField
Value Sampling
Sample generated values to verify distributions:
def sample_field(factory_class: type[Factory], field_name: str, samples: int = 100):
"""Sample values from a specific field."""
values = []
for _ in range(samples):
instance = factory_class.build()
values.append(getattr(instance, field_name))
print(f"Field: {field_name}")
print(f"Samples: {samples}")
print(f"Unique values: {len(set(values))}")
print(f"Min: {min(values)}")
print(f"Max: {max(values)}")
if isinstance(values[0], (int, float)):
print(f"Average: {sum(values) / len(values):.2f}")
sample_field(UserFactory, "age")
Validation Testing
Ensure generated data passes validation:
def test_all_generated_data_valid():
"""Verify that all generated instances pass validation."""
errors = []
for i in range(100):
try:
user = UserFactory.build()
validate_user(user) # Your validation function
except Exception as e:
errors.append((i, str(e)))
if errors:
print(f"Found {len(errors)} invalid instances:")
for idx, error in errors[:10]: # Show first 10
print(f" Instance {idx}: {error}")
raise AssertionError(f"{len(errors)} instances failed validation")
print("All 100 instances passed validation ✓")
Anti-Patterns to Avoid
❌ Over-Engineering Factories
# ❌ Too complex
class OverengineeredFactory(Factory[Model]):
field1 = fields.StringField(
min_chars=5,
max_chars=10,
prefix="PRE-",
suffix="-SUF"
)
# ✅ Simple and clear
class SimpleFactory(Factory[Model]):
field1 = fields.TextField("word")
❌ Hardcoded Test Data in Factories
# ❌ Defeats the purpose
class BadFactory(Factory[User]):
name = fields.ConstantField("John Doe")
email = fields.ConstantField("john@example.com")
# ✅ Randomized data
class GoodFactory(Factory[User]):
name = fields.TextField("name")
email = fields.TextField("email")
❌ Ignoring Type Safety
# ❌ No type hints
class UntypedFactory(Factory):
name = fields.TextField("name")
# ✅ Properly typed
class TypedFactory(Factory[User]):
name = fields.TextField("name")
❌ Circular Dependencies
# ❌ Circular reference
class AFactory(Factory[A]):
b = fields.FactoryField(BFactory)
class BFactory(Factory[B]):
a = fields.FactoryField(AFactory) # Infinite loop!
# ✅ Break the cycle
class AFactory(Factory[A]):
b_id = fields.IntegerField(min_value=1, max_value=100)
class BFactory(Factory[B]):
a_id = fields.IntegerField(min_value=1, max_value=100)
See Also
- Field Reference - Complete field documentation
- Cookbook - Common patterns and recipes
- Usage Guide - Basic to intermediate usage