Tutorial: Building Isolated Apex Test Classes with Dependency Injection

Step-by-step tutorial to build isolated, fast Apex test classes using dependency injection, test data factories, and stubs for Salesforce engineering teams.

## Why isolation matters in Apex tests

Effective unit tests for Salesforce must be fast, deterministic, and easy to maintain. Isolation ensures tests verify behavior of the unit under test, not the behavior of external collaborators such as database shape, callouts, or static state. This tutorial shows a practical approach using dependency injection, test data factories, and lightweight stubs to keep Apex test classes isolated and stable.

## Prerequisites

- Familiarity with Apex classes, interfaces, and @isTest

- Basic understanding of Test.startTest()/Test.stopTest()

- org set to SeeAllData=false for unit tests

## Step 1 — Introduce an interface for external dependencies

Create small interfaces for collaborators your class uses (repositories, external services, wrappers). For example, an IAccountRepository with methods like getAccountsByCriteria and upsertAccounts. Coding to interfaces makes swapping in test doubles straightforward and keeps production logic unchanged.

### Practical tip

Keep interfaces narrow. One or two responsibilities per interface makes stubbing easier and tests clearer.

## Step 2 — Implement dependency injection

Use constructor injection when possible. Provide a production default that composes real collaborators, and allow tests to pass fakes or stubs.

- Production usage: new MyService(new AccountRepository());

- Test usage: new MyService(new FakeAccountRepository());

If you can’t change constructors (e.g., legacy code), use protected factory methods that tests can override to supply fakes.

## Step 3 — Build a test data factory

Centralize creation of SObjects with a TestDataFactory class. The factory should return minimal, valid records and accept overrides for fields important to the test.

Benefits:

- Avoids duplicate record setup

- Makes intent explicit

- Keeps tests compact

Example approach: TestDataFactory.createAccount(Map<String, Object> overrides)

## Step 4 — Create lightweight stubs/fakes

Implement fakes that record method calls and return predictable results. Keep behavior explicit — if a stub returns different data under different parameters, make that behavior visible in the test setup.

Avoid heavy logic in fakes; they should be deterministic and focused on the scenario you’re validating.

## Step 5 — Assert behavior, not implementation

Write assertions on observable outcomes: DML effects, returned values, and interactions recorded by fakes. Don’t assert internal state of collaborators.

### Fast-test checklist

- Use @isTest(SeeAllData=false)

- Use Test.startTest() and Test.stopTest()

- Reset static state between tests

- Keep each test to one scenario

- Use small, focused test methods with clear setup/assert phases

## Integrating with Test Class Generator

Tools like Test Class Generator accelerate writing test scaffolding: they can scaffold dependency injections, generate TestDataFactory methods, and produce basic stubs based on your interfaces. Use generated tests as a starting point, then refine assertions and edge cases manually.

## Conclusion / Call-to-action

Isolated tests reduce flakiness and speed up CI feedback. Apply small patterns — interfaces, constructor injection, test data factories, and simple stubs — to make Apex test classes maintainable. Try generating a scaffold for one service with Test Class Generator, then iterate to add assertions and edge cases.