Building factory_boy Spatial Factories
factory_boy turns fixture creation from hand-written geometry literals into declarative factories that produce reproducible, parameterized spatial records on demand. This guide sits beneath synthetic vector data generation and shows how to build spatial factories: geometry attributes backed by seeded coordinate generators, CRS-aware traits, integration with GeoDjango or SQLAlchemy models, and sub-factories that emit the edge cases a suite must survive. The reason a spatial factory needs its own treatment is determinism — a factory that generates random coordinates without a fixed seed produces a different fixture every run, which is the opposite of what a regression test needs.
Why factories beat geometry literals
Hand-written fixtures — a Polygon([(0,0),(1,0),...]) pasted into each test — do not scale and do not vary: they are tedious to author, impossible to parameterize, and encourage copy-paste that drifts. A factory declares how to build a record once, then produces as many variants as a test needs, with only the fields under test overridden. For spatial data the payoff is sharper because geometry is verbose; a factory hides the coordinate generation behind a named trait like “valid parcel” or “self-intersecting” so tests read as intent, not coordinates.
Factory component reference
| Concern | factory_boy primitive | Spatial use |
|---|---|---|
| Reproducibility | factory.random.reseed_random |
Fixed seed → identical geometry |
| Derived field | LazyAttribute |
Build geometry from other fields |
| Variant | trait / Params |
“polar”, “anti_meridian”, “invalid” |
| Nested record | SubFactory |
Feature with related attributes |
| Bulk | create_batch |
A collection for a coverage test |
| Sequence | Sequence |
Unique ids across a batch |
Step-by-step implementation
The pattern targets factory_boy 3.x, Shapely 2.x and a seeded generator so fixtures are byte-reproducible.
Step 1 — Seed for determinism
import factory
# Call once in conftest.py so every run builds identical geometry.
def pytest_configure(config):
factory.random.reseed_random("geo-suite-seed")
Step 2 — A geometry factory with a LazyAttribute
Build the geometry from generated coordinates so the shape is derived, reproducible and parameterizable.
import factory
from shapely.geometry import Point, box
from shapely import to_wkt
class ParcelFactory(factory.Factory):
class Meta:
model = dict # or a GeoDjango / SQLAlchemy model
id = factory.Sequence(lambda n: n + 1)
srid = 25832
_x = factory.Faker("pyfloat", min_value=400000, max_value=600000)
_y = factory.Faker("pyfloat", min_value=5600000, max_value=5700000)
land_use = factory.Faker("random_element",
elements=["residential", "commercial", "agricultural"])
geom = factory.LazyAttribute(
lambda o: to_wkt(box(o._x, o._y, o._x + 50, o._y + 50))) # 50 m square
Step 3 — Traits for edge cases
Name the pathological variants as traits so a test asks for the defect it wants to exercise, echoing the edge-case catalogue in edge case spatial data creation.
class ParcelWithTraits(ParcelFactory):
class Params:
invalid = factory.Trait(
geom=factory.LazyAttribute(
lambda o: "POLYGON((0 0,1 1,1 0,0 1,0 0))")) # self-intersecting
empty = factory.Trait(geom="POLYGON EMPTY")
Step 4 — Build collections for coverage tests
parcels = ParcelFactory.create_batch(1000) # a reproducible coverage
invalid = ParcelWithTraits(invalid=True) # one known-bad record
Verification pattern
Prove reproducibility: two factory runs under the same seed must produce identical geometry, and a trait must produce the defect it names.
from shapely import from_wkt, is_valid
def test_factory_is_reproducible():
factory.random.reseed_random("geo-suite-seed")
a = ParcelFactory()["geom"]
factory.random.reseed_random("geo-suite-seed")
b = ParcelFactory()["geom"]
assert a == b # identical under the same seed
def test_invalid_trait_is_invalid():
assert not is_valid(from_wkt(ParcelWithTraits(invalid=True)["geom"]))
Failure modes and edge cases
- Unseeded randomness. Without
reseed_random, every run builds different geometry and regression baselines never match; seed inconftest.py. - Faker locale drift. A Faker provider whose output depends on locale can vary across machines; pin the locale or use numeric providers for coordinates.
- Geometry as a plain string. Storing WKT without a CRS field lets a fixture be reused in the wrong SRID; carry
sridon the factory. - Sequence collisions across batches. Reusing a factory across test modules without resetting the sequence can duplicate ids; scope the sequence or reset per test.
- Traits that overlap. Requesting
invalid=True, empty=Truetogether yields an ambiguous geometry; make edge-case traits mutually exclusive or document precedence.
Conclusion
A factory_boy spatial factory replaces brittle geometry literals with seeded, declarative, trait-driven fixtures that are reproducible across runs and expressive about intent. With a fixed seed, derived geometry via LazyAttribute, and named edge-case traits, a suite gets exactly the spatial records it needs without pasting coordinates. For the broader generation context, return to synthetic vector data generation.