Back to insights

Testing & Engineering Governance

A test data factory is a domain model—not a convenience class

JSBC Labs8 min read

Insertable is not the same as meaningful

Many Salesforce test suites have a class named TestDataFactory. Too often it is a collection of methods that populate every required field, insert a record and return it. That removes repetitive DML, but it does not create trustworthy tests. A record can satisfy validation rules while representing a business state that no user, integration or automation would ever produce.

The JSBC Labs position is that test-data code is part of the domain architecture. It should express the states the application promises to handle: a qualified opportunity with an active account, a case awaiting an entitlement decision, or an order whose payment has failed. When a factory models those states clearly, the test documents intent. When it only fills fields, every test must rediscover the business model.

Start from isolation, not from the org

Salesforce isolates Apex tests from pre-existing business data by default for tests on API version 24.0 and later. That constraint is valuable: a test should construct the evidence required for its result, run the behaviour and make a precise assertion. It should not pass because a familiar account, price book entry or configuration accident happens to exist in one sandbox.

Avoid SeeAllData=true as a routine escape hatch. Salesforce explicitly recommends creating test data instead. Org-dependent tests are difficult to reproduce, fail after data refreshes and hide undeclared dependencies. If a test cannot explain the minimum records and configuration it needs, the production design may also contain an implicit dependency that deserves to be made visible.

Model business states with named builders

Prefer an API such as anAccount().withActiveContract().inRegion('ZA') to a generic createSObject(Map<String,Object>) helper. The first vocabulary tells a reviewer which business conditions matter. The second couples every test to field names, defaults and construction order. A useful factory hides irrelevant mechanics while keeping decisive values obvious at the call site.

Separate construction from persistence. A builder that returns an unsaved record lets the test override one decisive attribute before insert, assemble related graphs and choose whether DML belongs in setup or inside the measured transaction. Provide explicit save methods for common graphs, but do not insert invisibly from every fluent setter. Hidden DML makes limit consumption and automation side effects hard to reason about.

Make valid defaults boring and intentional

Defaults should produce the smallest valid state, not a miniature production database. Give each required field a deterministic value, create only relationships demanded by the scenario and avoid time-sensitive defaults such as Date.today() unless the behaviour under test is genuinely relative to today. Random data improves superficial variety but often makes a failure impossible to replay.

Keep boundary cases named rather than probabilistic: withExpiredContract(), withMaximumDiscount(), withoutExternalId(). A reviewer can then see which branch is exercised. Store stable identifiers returned by the factory instead of querying by display names. Determinism shortens diagnosis because a failing test describes the same inputs on a laptop, in CI and during a production deployment.

Share setup without sharing assumptions

Salesforce's @TestSetup method creates records once for a test class and makes a clean copy available to each test method. Use it for genuinely common reference data: one catalogue, a baseline account hierarchy or reusable configuration records. It can reduce runtime and repetition, but it should not become a dumping ground for every scenario the class might need.

Each test should still arrange the state that makes its outcome unique. If twelve tests depend on a giant shared graph, changing one validation rule can break them all before their target behaviour runs. Keep shared fixtures small and immutable in spirit; create scenario-specific records through named factory methods. Optimise clarity first, then remove measured setup cost where reuse is safe.

Treat personas as part of the fixture

Data without an execution identity is an incomplete Salesforce scenario. Record sharing, ownership, queues, permission sets and user licences can change the result even when the records are identical. Define persona builders for the roles the design supports, then execute the relevant behaviour with System.runAs. Salesforce documents that runAs enforces record sharing in test context; object and field permissions still require deliberate production-code enforcement and suitable tests.

User creation also intersects with setup-object DML. Salesforce documents techniques for mixed DML in tests, including separating operations with runAs. Centralise that complexity instead of letting every test invent its own user records. A governed persona factory should make licence, profile, permission assignments and ownership assumptions explicit, while avoiding an all-powerful administrator as the default test user.

Use files for data sets, builders for behaviour

Test.loadData can populate records from a CSV static resource and is useful when a stable tabular data set is clearer than hundreds of construction statements. It fits reference matrices, calculation examples and migration fixtures. Version the file with the tests, keep column meanings documented and make relationships understandable to someone reviewing a change.

CSV is less effective for dynamic graphs, persona-sensitive scenarios and tests that need one decisive variation. A builder makes those differences readable in code. Choose the representation by what the test must communicate: a file for a durable table of examples, a builder for a business story, and direct construction for a tiny one-off record. One universal factory mechanism creates abstraction without clarity.

Protect the transaction you are measuring

Create prerequisite data before Test.startTest() so factory queries, DML and automation do not consume the fresh limits intended for the behaviour under test. Salesforce documents that startTest and stopTest provide the measured test context, and stopTest also causes queued asynchronous work to execute in test context. Assertions should inspect business outcomes after that boundary.

Do not use the reset to disguise an unrealistic transaction. If production code creates the same supporting records inside the real request, the test must include that cost. Factories should establish preconditions, not move production work out of sight. Add bulk scenarios with collections, skewed relationships and multiple personas where those conditions affect limits, locking or sharing. Unit-test convenience must not erase operational reality.

Govern factories like production interfaces

Assign ownership, review factory changes and keep their public surface small. When a new validation rule appears, decide whether it changes the minimum valid domain state or only one scenario. Updating a single default to force thousands of tests green can conceal a behavioural change. Sometimes the correct response is a new named state and explicit migration of the tests whose assumptions changed.

Track factory health through test runtime, setup failures, duplicated builders and the number of tests using SeeAllData. Run suites in clean development environments and representative sandboxes. Salesforce Well-Architected recommends production-scale testing for reliability; unit factories complement that work by providing deterministic behavioural evidence. The goal is not the cleverest helper class. It is a suite whose data tells the truth about the system it protects.

Official references

Continue reading