Skip to main content

Prove Behavior, Not Coverage %: Mastering Test Data Factories in Salesforce

In plain words: 75% coverage is the absolute bare minimum, not the final goal. The real goal of testing is to write asserts that fail when behavior breaks. To make tests maintainable, build all test data using one central factory. When an admin adds a new required field, you fix one recipe instead of manually updating 400 separate tests. Remember: tests cannot make real callouts—you must use HttpCalloutMock, and you should use StubProvider to mock class dependencies. Finally, never rely on existing org data; ban SeeAllData=true unless absolutely necessary.

Module Map — 360° View

MODULE 8 root: 'Prove behavior, not coverage %'
├─ Test data patterns (8.1) — TestDataFactory, @testSetup, no SeeAllData [Current Topic]
├─ Mocking callouts & dependencies (8.2) — HttpCalloutMock, StubProvider
└─ Coverage % vs test QUALITY (8.3) — asserts on behavior + negatives

LINKS → Design patterns enable mocking (7) · CI gates run tests (12)

8.1 Test Data Factories

๐Ÿง  One Kitchen: All test data should be cooked in one factory. A business rule change means fixing one recipe, not replacing 400 plates.

A test data factory centralizes how you create records for your test classes. By using a single utility class to stamp out records with sensible defaults, you build a safety net against org changes.

  • Single Source of Truth: One class produces valid sObjects. It handles the mandatory fields and sensible defaults while letting you override specific values per test.
  • DRY Principle (Don't Repeat Yourself): When a new required field or validation rule lands in production, you fix one class instead of hunting down 400 failing test methods.
  • Performance Boosts: Combine the factory with @testSetup to create shared data just once per test class. Salesforce rolls back the database state between methods, drastically cutting your suite's runtime.
  • Isolation: SeeAllData=false is the default and should be strictly enforced. Tests must construct their own universe.

The Pain of Inline Test Data

๐Ÿ“Œ The Breaking Point: Let's say your admin makes "Industry" a required field on the Account object. Overnight, 400 tests fail because every test manually instantiates an Account (new Account(Name = 'Test')) without the new field. You now have to update 400 files. If you used TestDataFactory.createAccount(), the fix is exactly one line of code.
๐ŸŽฌ Real-Life Scenario: The Rule Change That Broke Everything

A new validation rule is deployed: Every Delivery__c record now requires a Service_Type__c. By the next morning, 400 test methods are glowing red.

  • The Bad Way: Every test built its own records from scratch. You now have 400 copies of new Delivery__c(...) missing the field. Fixing these is pure noise and wastes an entire day. Furthermore, each test hides private, undocumented assumptions about what constitutes a "valid" delivery.
  • The Good Way: You have one TestDataFactory with builder methods like makeDeliveries(n, overrides). The factory provides defaults that keep records valid, while allowing overrides for specific test scenarios. Insertion is optional (pure logic tests don't need real DML), and graph helpers can build entire hierarchies (Account → Contacts → Deliveries) in one call.
The Payoff: The next validation rule requires a single edit in the factory, turning the suite green in minutes.

๐Ÿงญ 360 Card — Test Data Factories

  • Rule: All test data is built in one central utility class, providing sensible defaults and allowing specific overrides.
  • Gain: A new required field or validation rule breaks exactly one recipe, not hundreds of tests.
  • Price: The factory becomes a massive shared dependency. A sloppy change to a default value can unintentionally ripple through and break the entire suite.
  • Limits: SeeAllData=true is almost never defensible. Use @testSetup to create records once per test class. Make DML insertion optional in your factory methods because pure-logic tests run faster on in-memory records.
  • The Trap: Writing test data inline is fast the first time. It is also the exact habit that creates the 400-plate problem later.
  • At Volume: If a test suite takes 90 minutes to run, it's usually because data is built per method instead of per class, pure logic tests are hitting the database, or automation is firing wildly during the setup phase.

๐ŸŽฏ Core Q&A

Q: What does a good factory look like, and what failure does it prevent besides duplication?
Say this first: It acts as a single factory with sensible defaults and optional override parameters. It stops 400 tests from breaking when one validation rule changes. You fix the factory, not the suite.

Build your factory using a builder-style pattern:

  • Provide methods like make(n, overrides) that return valid records.
  • Make insertion optional. Many unit tests testing pure logic only need in-memory records.
  • Create composition helpers that build out entire object graphs (e.g., an Account containing Contacts containing Cases).

The value goes far beyond DRY. It prevents test-data drift. When developers invent their own records inline, they embed hidden assumptions. A validation rule change will break a random scatter of tests with misleading errors. A factory ensures that tribal org knowledge (like mandatory record types or lookups) becomes executable, centralized code.

Code Implementation

// 1. One factory that builds test data for every test in the org.
@isTest
public class TestFactory {
    
    // 2. Ask for how many records, and whether to insert them into the DB.
    // One recipe, many tests.
    public static List<Account> createAccounts(Integer n, Boolean doInsert) {
        List<Account> accs = new List<Account>();
        
        for (Integer i = 0; i < n; i++) {
            // Apply sensible defaults required by the org
            accs.add(new Account(Name = 'Acct ' + i, Industry = 'Technology'));
        }
        
        // 3. Insert only when the test needs real records in the database.
        if (doInsert) {
            insert accs;
        }
        
        return accs;
    }
}

Follow-ups (Scenario-Based)

Q: When is SeeAllData=true ever reasonable?

A: Almost never. The only survivors are objects that genuinely cannot be created in a test context. This used to include things like standard pricebooks, but Test.getStandardPricebookId() eliminated that excuse. Every test using SeeAllData=true is permanently coupled to its specific environment. It might pass in a sandbox, fail immediately after a data refresh, and physically cannot run in a scratch org. If a developer claims they need it, they are usually missing a factory recipe or dealing with an architecture smell that should be redesigned.

Q: The test suite takes 90 minutes and is blocking deployments. How do you attack this?

A: Measure first. Use the Tooling API to expose per-class runtimes; the distribution is always skewed. The top offenders usually share four fatal flaws:

  • Data is created per-method instead of being centralized once in a @testSetup block.
  • Gratuitous volume. Inserting 251 records to test bulkification everywhere, when 2 records prove the logic just fine. Keep a few true 200-record bulk tests strictly on the trigger paths.
  • DML-heavy tests for pure logic. Stubbing dependencies makes these instant.
  • Cascading automation firing during the data setup phase. Use a bypass switch (e.g., custom metadata or static variables) to disable triggers during the data-arrangement phase, since that automation isn't what you are actively testing.
Parallel execution warning: Turning on parallel test execution will help speed things up, but only if you have fixed your row-lock collisions. If your tests are not properly isolated with unique test data per method, parallel execution will cause massive failures.