Part 66: Unit Testing and Apex Mocks
Welcome back to the Salesforce series. In previous posts, we covered the Apex Enterprise Patterns — the Unit of Work, Selector Layer, Service Layer, and Domain Layer from the fflib Apex Common library. You now know how to structure a Salesforce application with clean separation of concerns. But there is a problem. If you write tests the traditional way, every test method hits the database, creates records, runs triggers, fires automations, and takes seconds to execute. When your test suite grows to hundreds of methods, your deployment takes 30 minutes or more.
This post fixes that. We are going to learn true unit testing in Apex — tests that never touch the database, run in milliseconds, and verify that your code behaves correctly by isolating it from every external dependency. The tool that makes this possible is the Apex Mocks library.
This is Part 66 of the series. We will cover five areas:
- What is unit testing and when to use it — Understanding the difference between unit and integration tests, the testing pyramid, and why most Apex tests are integration tests.
- Creating a unit test without the Apex Mocks library — Manual dependency injection, hand-rolled stubs, and the limitations of doing it yourself.
- What is the Apex Mocks library — History, capabilities, installation, and its relationship to Apex Common.
- Creating a unit test with the Apex Mocks library — Mocking selectors, services, and domains, verifying method calls, argument matchers, and wiring it all together with the Application factory.
- PROJECT: Unit testing an fflib application — A complete test class that mocks all three layers, tests a service method end-to-end, and verifies every interaction.
By the end, you will be able to write Apex tests that execute in under a second and catch logic bugs with surgical precision.
What is Unit Testing and When to Use It
The Two Types of Tests in Salesforce
Every Apex developer writes test classes. But almost nobody in the Salesforce ecosystem writes true unit tests. Let me explain the difference.
An integration test creates real records in the database, executes your code, queries the results, and asserts the outcome. This is what 99% of Apex tests look like:
@IsTest
static void testOpportunityService() {
Account acc = new Account(Name = 'Test Account');
insert acc;
Opportunity opp = new Opportunity(
Name = 'Test Opp',
AccountId = acc.Id,
StageName = 'Prospecting',
CloseDate = Date.today().addDays(30),
Amount = 50000
);
insert opp;
Test.startTest();
OpportunityService.applyDiscount(new Set<Id>{ opp.Id }, 10);
Test.stopTest();
Opportunity result = [SELECT Amount FROM Opportunity WHERE Id = :opp.Id];
System.assertEquals(45000, result.Amount, 'Discount should reduce amount by 10%');
}
This test works, but it does a lot of things that have nothing to do with testing the applyDiscount method. It inserts an Account. It inserts an Opportunity. It fires any triggers on those objects. It executes any automation rules, flows, or process builders that exist in the org. It runs a SOQL query to verify the result. Every one of those operations takes time and introduces potential failure points that are not related to the code under test.
A unit test does none of that. It isolates the method you want to test, provides fake inputs, and checks the output. No database. No triggers. No automation. Just your logic, verified in isolation.
@IsTest
static void testApplyDiscountLogic() {
// No insert. No database. Just fake data.
Opportunity opp = new Opportunity(Amount = 50000);
List<Opportunity> opps = new List<Opportunity>{ opp };
// Call the pure logic method directly
OpportunityDiscountCalculator.applyDiscount(opps, 10);
System.assertEquals(45000, opps[0].Amount, 'Discount should reduce amount by 10%');
}
This second test is faster, simpler, and more focused. It tests exactly one thing: does the discount calculation work? If this test fails, you know the bug is in the discount logic, not in a trigger, not in a validation rule, not in a query.
The Testing Pyramid
Software engineering has a concept called the testing pyramid. At the base of the pyramid, you have many fast unit tests. In the middle, you have fewer integration tests. At the top, you have a small number of end-to-end tests that simulate real user behavior.
Most Salesforce orgs have an inverted pyramid — lots of slow integration tests and zero unit tests. This is because the platform makes it easy to insert records and hard to mock dependencies. The Apex Mocks library flips this by making true unit tests straightforward.
Here is what a healthy test pyramid looks like for an fflib project:
- Unit tests (70%) — Test service logic, domain logic, and calculations in isolation. Mock the selector so you never query the database. Mock the Unit of Work so you never commit DML. These run in milliseconds.
- Integration tests (25%) — Test that your layers wire together correctly. Insert records, call a service method, and verify the database state. These confirm that your selectors return the right data and your DML actually persists.
- End-to-end tests (5%) — Test the full stack including controllers, Visualforce pages, or LWC. These are slow but verify the user experience.
When True Unit Tests Matter
Unit tests become essential when:
- Your test suite is slow. If running all tests takes more than 10 minutes, you need unit tests. Developers stop running tests when they are slow, and untested code accumulates.
- You have complex business logic. A method that calculates pricing tiers, applies discounts, evaluates eligibility rules, and determines shipping costs has dozens of code paths. Integration tests make it painful to cover every path because each test requires inserting records. Unit tests let you cover every path with simple data setup.
- You need isolation. When a test fails, you want to know immediately whether the bug is in your code or in an unrelated automation. Unit tests guarantee isolation.
- You are using the fflib patterns. The Apex Enterprise Patterns (Selector, Service, Domain, Unit of Work) are designed for dependency injection. If you are already using fflib, unit tests with mocks are a natural fit.
- You want fast feedback during development. Running a single unit test takes under a second. Running a single integration test can take 5 to 10 seconds. When you are iterating on logic, that difference adds up.
How to Create a Unit Test Without the Apex Mocks Library
Before we introduce the Apex Mocks library, let us understand the underlying technique: dependency injection. You can write unit tests in Apex without any library. It requires more boilerplate, but it teaches you the fundamental pattern.
The Problem: Tight Coupling
Consider a service method that queries the database directly:
public class OpportunityService {
public static void applyDiscount(Set<Id> oppIds, Decimal discountPercent) {
List<Opportunity> opps = [
SELECT Id, Amount
FROM Opportunity
WHERE Id IN :oppIds
];
for (Opportunity opp : opps) {
opp.Amount = opp.Amount * (1 - discountPercent / 100);
}
update opps;
}
}
This method is impossible to unit test. The SOQL query requires real records in the database. The DML statement writes to the database. There is no way to test the discount logic without inserting records first.
The Solution: Inject Dependencies Through Interfaces
The fix is to extract the database operations behind interfaces and inject them.
First, define an interface for the data access:
public interface IOpportunitySelector {
List<Opportunity> selectByIds(Set<Id> ids);
}
Then define an interface for the data persistence:
public interface IUnitOfWork {
void registerDirty(SObject record);
void commitWork();
}
Now create the real implementations:
public class OpportunitySelectorImpl implements IOpportunitySelector {
public List<Opportunity> selectByIds(Set<Id> ids) {
return [
SELECT Id, Amount
FROM Opportunity
WHERE Id IN :ids
];
}
}
public class UnitOfWorkImpl implements IUnitOfWork {
private List<SObject> dirtyRecords = new List<SObject>();
public void registerDirty(SObject record) {
dirtyRecords.add(record);
}
public void commitWork() {
update dirtyRecords;
}
}
Rewrite the service to accept dependencies through the constructor:
public class OpportunityService {
private IOpportunitySelector selector;
private IUnitOfWork uow;
public OpportunityService(IOpportunitySelector selector, IUnitOfWork uow) {
this.selector = selector;
this.uow = uow;
}
public void applyDiscount(Set<Id> oppIds, Decimal discountPercent) {
List<Opportunity> opps = selector.selectByIds(oppIds);
for (Opportunity opp : opps) {
opp.Amount = opp.Amount * (1 - discountPercent / 100);
}
for (Opportunity opp : opps) {
uow.registerDirty(opp);
}
uow.commitWork();
}
}
Creating Manual Mock Classes
Now you can create mock implementations for testing:
@IsTest
public class MockOpportunitySelector implements IOpportunitySelector {
private List<Opportunity> recordsToReturn;
public MockOpportunitySelector(List<Opportunity> recordsToReturn) {
this.recordsToReturn = recordsToReturn;
}
public List<Opportunity> selectByIds(Set<Id> ids) {
return recordsToReturn;
}
}
@IsTest
public class MockUnitOfWork implements IUnitOfWork {
public List<SObject> dirtyRecords = new List<SObject>();
public Boolean commitCalled = false;
public void registerDirty(SObject record) {
dirtyRecords.add(record);
}
public void commitWork() {
commitCalled = true;
}
}
Writing the Unit Test
With the mocks in place, the test is clean and fast:
@IsTest
static void testApplyDiscountUnitTest() {
// Arrange: Create fake data — no insert, no database
Opportunity fakeOpp = new Opportunity(
Amount = 50000
);
List<Opportunity> fakeOpps = new List<Opportunity>{ fakeOpp };
MockOpportunitySelector mockSelector = new MockOpportunitySelector(fakeOpps);
MockUnitOfWork mockUow = new MockUnitOfWork();
OpportunityService service = new OpportunityService(mockSelector, mockUow);
// Act: Call the method under test
service.applyDiscount(new Set<Id>{ '006000000000001' }, 10);
// Assert: Verify the logic
System.assertEquals(45000, fakeOpp.Amount, 'Discount should reduce amount by 10%');
System.assert(mockUow.commitCalled, 'Unit of Work should have been committed');
System.assertEquals(1, mockUow.dirtyRecords.size(), 'One record should be registered as dirty');
}
This test never touches the database. It runs in milliseconds. And it tests exactly one thing: the discount calculation and the coordination logic in the service method.
Limitations of Manual Mocking
Manual mocking works, but it has serious drawbacks:
- Boilerplate explosion. Every interface needs a corresponding mock class. If your selector has 10 methods, your mock class needs 10 method implementations, even if your test only cares about one.
- No automatic verification. You have to manually track whether methods were called, how many times, and with what arguments. This means adding tracking variables to every mock class.
- Maintenance burden. When you add a method to an interface, you must update every mock class that implements it. This discourages refactoring.
- No argument matching. You cannot easily verify that a method was called with specific arguments without writing custom comparison logic in every mock.
- Fragile assertions. Your assertions depend on the internal structure of your mock classes. If you restructure a mock, all tests that use it may break.
These limitations are exactly what the Apex Mocks library solves.
What is the Apex Mocks Library
History and Origin
The Apex Mocks library is part of the fflib (FinancialForce Library) family of open-source Apex libraries. It was originally developed by FinancialForce.com (now Certinia) and is maintained as an open-source project on GitHub. The library was inspired by popular mocking frameworks in other languages like Mockito for Java and Moq for .NET.
The fflib ecosystem includes two main repositories:
- fflib-apex-common (also called Apex Common) — Provides the enterprise patterns:
fflib_SObjectSelector,fflib_SObjectDomain,fflib_SObjectUnitOfWork, and theApplicationfactory class. - fflib-apex-mocks (also called Apex Mocks) — Provides the mocking framework:
fflib_ApexMocks,fflib_Match,fflib_Answer, and the verification API.
These two libraries are designed to work together. Apex Common gives you the architecture (selectors, domains, services, unit of work). Apex Mocks gives you the tools to test that architecture without touching the database.
What Apex Mocks Provides
The library gives you several capabilities:
- Dynamic mock generation. Instead of writing a mock class for every interface, you call
mocks.mock(IOpportunitySelector.class)and the library generates a mock object at runtime. - Stubbing with
thenReturn. You define what a mock method should return when called: “whenselectByIdsis called, return this list of Opportunities.” - Verification. After your code runs, you verify that specific methods were called the expected number of times: “verify that
registerDirtywas called exactly twice.” - Argument matchers. You can verify that methods were called with specific arguments using the
fflib_Matchclass: “verify thatselectByIdswas called with a set containing this specific Id.” - Answer interfaces. For advanced scenarios, you can provide custom logic that executes when a mocked method is called.
- Integration with the Application factory. Apex Mocks hooks into the
Applicationclass from Apex Common, allowing you to inject mocks at the factory level so all code that usesApplication.Selector.newInstance()automatically gets the mock.
How to Install Apex Mocks
You can install the Apex Mocks library in several ways:
Option 1: Deploy from GitHub. Clone the repository from https://github.com/apex-enterprise-patterns/fflib-apex-mocks and deploy the source to your org using Salesforce CLI.
git clone https://github.com/apex-enterprise-patterns/fflib-apex-mocks.git
cd fflib-apex-mocks
sf project deploy start --source-dir sfdx-src/classes --target-org your-org-alias
Option 2: Install as an unlocked package. The fflib team publishes unlocked packages that you can install with a single command. Check the repository README for the latest package version Id.
sf package install --package 04t... --target-org your-org-alias --wait 10
Option 3: Include in your SFDX project. Copy the Apex Mocks source files directly into your project’s force-app directory. This gives you full control over the source and makes it easy to review changes.
You must also have fflib-apex-common installed, since the mocking framework is designed to mock the interfaces and base classes defined in Apex Common.
Key Classes in the Library
Here are the classes you will use most often:
| Class | Purpose |
|---|---|
fflib_ApexMocks | The main mocking engine. Creates mocks, records stubs, and performs verifications. |
fflib_Match | Provides argument matchers like anyId(), anyString(), eq(), isNotNull(). |
fflib_Answer | Interface for custom answer logic when a mocked method is called. |
fflib_System (internal) | Used internally for mock object generation via StubProvider. |
The library leverages the Apex Stub API (System.StubProvider) which Salesforce introduced to allow dynamic mock creation. This is why Apex Mocks does not require you to write any mock classes by hand.
How to Create a Unit Test with the Apex Mocks Library
Now let us build a real unit test using Apex Mocks. We will test a service method that coordinates between a selector (to read data), business logic (to transform data), and a unit of work (to persist changes).
The Code Under Test
Assume we have the following fflib-structured classes:
Selector:
public class OpportunitiesSelector extends fflib_SObjectSelector
implements IOpportunitiesSelector {
public interface IOpportunitiesSelector {
List<Opportunity> selectByIdWithLineItems(Set<Id> ids);
}
public List<Schema.SObjectField> getSObjectFieldList() {
return new List<Schema.SObjectField>{
Opportunity.Id,
Opportunity.Name,
Opportunity.Amount,
Opportunity.StageName,
Opportunity.AccountId
};
}
public Schema.SObjectType getSObjectType() {
return Opportunity.SObjectType;
}
public List<Opportunity> selectByIdWithLineItems(Set<Id> ids) {
fflib_QueryFactory query = newQueryFactory();
new OpportunityLineItemsSelector().addQueryFactorySubSelect(query);
return (List<Opportunity>) Database.query(query.setCondition('Id IN :ids').toSOQL());
}
}
Domain:
public class Opportunities extends fflib_SObjectDomain
implements IOpportunities {
public interface IOpportunities {
void applyDiscount(Decimal discountPercent);
}
public Opportunities(List<Opportunity> records) {
super(records);
}
public void applyDiscount(Decimal discountPercent) {
Decimal multiplier = 1 - (discountPercent / 100);
for (Opportunity opp : (List<Opportunity>) Records) {
if (opp.Amount != null) {
opp.Amount = opp.Amount * multiplier;
}
}
}
public class Constructor implements fflib_SObjectDomain.IConstructable {
public fflib_SObjectDomain construct(List<SObject> records) {
return new Opportunities(records);
}
}
}
Service:
public class OpportunitiesService {
public static void applyDiscounts(Set<Id> oppIds, Decimal discountPercent) {
fflib_SObjectUnitOfWork uow = Application.UnitOfWork.newInstance();
IOpportunitiesSelector selector =
(IOpportunitiesSelector) Application.Selector.newInstance(Opportunity.SObjectType);
List<Opportunity> opps = selector.selectByIdWithLineItems(oppIds);
IOpportunities domain =
(IOpportunities) Application.Domain.newInstance(opps);
domain.applyDiscount(discountPercent);
for (Opportunity opp : opps) {
uow.registerDirty(opp);
}
uow.commitWork();
}
}
Application Factory:
public class Application {
public static final fflib_Application.UnitOfWorkFactory UnitOfWork =
new fflib_Application.UnitOfWorkFactory(
new List<SObjectType>{
Account.SObjectType,
Opportunity.SObjectType,
OpportunityLineItem.SObjectType
}
);
public static final fflib_Application.SelectorFactory Selector =
new fflib_Application.SelectorFactory(
new Map<SObjectType, Type>{
Opportunity.SObjectType => OpportunitiesSelector.class
}
);
public static final fflib_Application.DomainFactory Domain =
new fflib_Application.DomainFactory(
Application.Selector,
new Map<SObjectType, Type>{
Opportunity.SObjectType => Opportunities.Constructor.class
}
);
public static final fflib_Application.ServiceFactory Service =
new fflib_Application.ServiceFactory(
new Map<Type, Type>{
IOpportunitiesService.class => OpportunitiesService.class
}
);
}
Step 1: Create the Mocks Instance
Every Apex Mocks test starts by creating an instance of fflib_ApexMocks:
fflib_ApexMocks mocks = new fflib_ApexMocks();
This object is the central engine. It creates mocks, records stubbing rules, and performs verifications.
Step 2: Create Mock Instances
Use mocks.mock() to create mock objects for each dependency:
IOpportunitiesSelector mockSelector =
(IOpportunitiesSelector) mocks.mock(OpportunitiesSelector.class);
fflib_SObjectUnitOfWork mockUow =
(fflib_SObjectUnitOfWork) mocks.mock(fflib_SObjectUnitOfWork.class);
IOpportunities mockDomain =
(IOpportunities) mocks.mock(Opportunities.class);
Notice that for the selector and domain, we mock the concrete class but cast to the interface. The Stub API intercepts all method calls on the mock object, regardless of how it is cast.
Step 3: Define Stubbing Rules
Now tell the mocks what to return when specific methods are called. This is the “given” phase of your test:
// Prepare fake data
Opportunity fakeOpp1 = new Opportunity(
Id = fflib_IDGenerator.generate(Opportunity.SObjectType),
Name = 'Test Opp 1',
Amount = 100000,
StageName = 'Negotiation'
);
Opportunity fakeOpp2 = new Opportunity(
Id = fflib_IDGenerator.generate(Opportunity.SObjectType),
Name = 'Test Opp 2',
Amount = 50000,
StageName = 'Proposal'
);
List<Opportunity> fakeOpps = new List<Opportunity>{ fakeOpp1, fakeOpp2 };
Set<Id> fakeOppIds = new Set<Id>{ fakeOpp1.Id, fakeOpp2.Id };
// Stub the selector: when selectByIdWithLineItems is called, return our fake data
mocks.startStubbing();
mocks.when(mockSelector.selectByIdWithLineItems(fakeOppIds))
.thenReturn(fakeOpps);
mocks.when(mockSelector.sObjectType())
.thenReturn(Opportunity.SObjectType);
mocks.stopStubbing();
The startStubbing() and stopStubbing() calls bracket the stubbing phase. Between them, every method call on a mock is recorded as a rule rather than executed. The when(...).thenReturn(...) syntax should be familiar if you have used Mockito in Java.
Notice that we also stub sObjectType(). The Application factory uses this method to route the selector to the correct SObject type. Without this stub, the factory injection will fail.
Step 4: Inject Mocks into the Application Factory
The Application factory needs to return our mocks instead of the real implementations:
Application.Selector.setMock(mockSelector);
Application.UnitOfWork.setMock(mockUow);
Application.Domain.setMock(mockDomain);
The setMock() method on each factory tells it to return the provided mock object the next time newInstance() is called. This is the magic of the fflib Application class — it was designed from the start to support mock injection.
Step 5: Execute the Code Under Test
Now call the service method. It will use our mocks instead of real database operations:
OpportunitiesService.applyDiscounts(fakeOppIds, 10);
When this method runs:
Application.Selector.newInstance(Opportunity.SObjectType)returnsmockSelectorinstead of a realOpportunitiesSelector.mockSelector.selectByIdWithLineItems(fakeOppIds)returnsfakeOppsinstead of querying the database.Application.Domain.newInstance(opps)returnsmockDomaininstead of constructing a realOpportunitiesdomain.Application.UnitOfWork.newInstance()returnsmockUowinstead of a real Unit of Work.mockUow.registerDirty(opp)andmockUow.commitWork()do nothing instead of performing DML.
No database access. No SOQL. No DML. The entire execution happens in memory.
Step 6: Verify Interactions
The final step is verification. Instead of asserting database state (which does not exist in a unit test), you verify that the correct methods were called with the correct arguments:
// Verify the selector was called once with the correct Ids
((IOpportunitiesSelector) mocks.verify(mockSelector, 1))
.selectByIdWithLineItems(fakeOppIds);
// Verify the domain's applyDiscount was called with 10
((IOpportunities) mocks.verify(mockDomain, 1))
.applyDiscount(10);
// Verify each opportunity was registered as dirty
((fflib_SObjectUnitOfWork) mocks.verify(mockUow, 2))
.registerDirty(fflib_Match.anySObject());
// Verify commitWork was called exactly once
((fflib_SObjectUnitOfWork) mocks.verify(mockUow, 1))
.commitWork();
The mocks.verify(mockObject, times) call tells the framework to check that the subsequent method was called exactly times times. If the verification fails, the test throws an assertion error with a clear message.
Verification Options
The verify method accepts several forms:
// Verify called exactly N times
mocks.verify(mockObj, 2)
// Verify called at least N times
mocks.verify(mockObj, fflib_ApexMocks.atLeast(1))
// Verify called at most N times
mocks.verify(mockObj, fflib_ApexMocks.atMost(3))
// Verify called between N and M times
mocks.verify(mockObj, fflib_ApexMocks.between(1, 5))
// Verify never called
mocks.verify(mockObj, fflib_ApexMocks.never())
Argument Matchers with fflib_Match
When verifying method calls, you often do not care about the exact argument values. The fflib_Match class provides flexible matchers:
// Match any SObject
fflib_Match.anySObject()
// Match any Id
fflib_Match.anyId()
// Match any String
fflib_Match.anyString()
// Match any Integer
fflib_Match.anyInteger()
// Match any Boolean
fflib_Match.anyBoolean()
// Match any List
fflib_Match.anyList()
// Match a specific value
fflib_Match.eq(10)
fflib_Match.eq('Closed Won')
// Match null
fflib_Match.isNull()
// Match not null
fflib_Match.isNotNull()
// Match SObject with specific field value
fflib_Match.sObjectWith(
new Map<Schema.SObjectField, Object>{
Opportunity.StageName => 'Closed Won'
}
)
// Match SObject with specific Id
fflib_Match.sObjectWithId(opportunityId)
Argument matchers are particularly powerful when verifying Unit of Work registrations. Instead of checking the exact SObject reference, you can verify that a record with the right field values was registered:
((fflib_SObjectUnitOfWork) mocks.verify(mockUow, 1))
.registerDirty(
fflib_Match.sObjectWith(
new Map<Schema.SObjectField, Object>{
Opportunity.Amount => 90000,
Opportunity.StageName => 'Negotiation'
}
)
);
This verifies that registerDirty was called with an Opportunity whose Amount is 90000 and StageName is Negotiation, regardless of any other field values.
The Complete Unit Test
Putting it all together, here is the full test method:
@IsTest
static void testApplyDiscountsService() {
// Arrange
fflib_ApexMocks mocks = new fflib_ApexMocks();
// Create mocks
IOpportunitiesSelector mockSelector =
(IOpportunitiesSelector) mocks.mock(OpportunitiesSelector.class);
fflib_SObjectUnitOfWork mockUow =
(fflib_SObjectUnitOfWork) mocks.mock(fflib_SObjectUnitOfWork.class);
// Prepare fake data
Id fakeOppId1 = fflib_IDGenerator.generate(Opportunity.SObjectType);
Id fakeOppId2 = fflib_IDGenerator.generate(Opportunity.SObjectType);
List<Opportunity> fakeOpps = new List<Opportunity>{
new Opportunity(Id = fakeOppId1, Name = 'Big Deal', Amount = 100000),
new Opportunity(Id = fakeOppId2, Name = 'Small Deal', Amount = 20000)
};
Set<Id> fakeOppIds = new Set<Id>{ fakeOppId1, fakeOppId2 };
// Stub the selector
mocks.startStubbing();
mocks.when(mockSelector.selectByIdWithLineItems(fakeOppIds))
.thenReturn(fakeOpps);
mocks.when(mockSelector.sObjectType())
.thenReturn(Opportunity.SObjectType);
mocks.stopStubbing();
// Inject mocks
Application.Selector.setMock(mockSelector);
Application.UnitOfWork.setMock(mockUow);
// Act
Test.startTest();
OpportunitiesService.applyDiscounts(fakeOppIds, 15);
Test.stopTest();
// Assert — verify the selector was called
((IOpportunitiesSelector) mocks.verify(mockSelector, 1))
.selectByIdWithLineItems(fakeOppIds);
// Assert — verify dirty records were registered
((fflib_SObjectUnitOfWork) mocks.verify(mockUow, 2))
.registerDirty(fflib_Match.anySObject());
// Assert — verify commit was called
((fflib_SObjectUnitOfWork) mocks.verify(mockUow, 1))
.commitWork();
}
Mocking the Selector Layer in Detail
The selector is the most commonly mocked layer because it is the primary source of data in your service and domain methods. Let us explore the patterns you will use repeatedly.
Basic Selector Mock
The standard pattern is to mock the selector, stub the query method, and stub sObjectType():
fflib_ApexMocks mocks = new fflib_ApexMocks();
IAccountsSelector mockSelector =
(IAccountsSelector) mocks.mock(AccountsSelector.class);
List<Account> fakeAccounts = new List<Account>{
new Account(
Id = fflib_IDGenerator.generate(Account.SObjectType),
Name = 'Acme Corp',
Industry = 'Technology',
AnnualRevenue = 5000000
)
};
mocks.startStubbing();
mocks.when(mockSelector.selectByIds(
new Set<Id>{ fakeAccounts[0].Id }
)).thenReturn(fakeAccounts);
mocks.when(mockSelector.sObjectType())
.thenReturn(Account.SObjectType);
mocks.stopStubbing();
Application.Selector.setMock(mockSelector);
Stubbing Multiple Selector Methods
If your service calls multiple selector methods, stub them all in the same stubbing block:
mocks.startStubbing();
mocks.when(mockOppSelector.selectByIdWithLineItems(oppIds))
.thenReturn(fakeOpps);
mocks.when(mockOppSelector.selectByAccountIds(accountIds))
.thenReturn(fakeOppsByAccount);
mocks.when(mockOppSelector.sObjectType())
.thenReturn(Opportunity.SObjectType);
mocks.stopStubbing();
Returning Empty Results
To test how your code handles empty query results, stub the selector to return an empty list:
mocks.startStubbing();
mocks.when(mockSelector.selectByIds(emptyIds))
.thenReturn(new List<Opportunity>());
mocks.when(mockSelector.sObjectType())
.thenReturn(Opportunity.SObjectType);
mocks.stopStubbing();
This is much simpler than the integration test equivalent, which would require inserting no records and relying on the query returning nothing.
Mocking the Domain Layer in Detail
Mocking the domain is important when your service method delegates business logic to a domain class. You want to verify that the service calls the domain correctly, without testing the domain logic itself (which should have its own test class).
Basic Domain Mock
fflib_ApexMocks mocks = new fflib_ApexMocks();
IOpportunities mockDomain =
(IOpportunities) mocks.mock(Opportunities.class);
// Stub — domain methods that return void need no thenReturn
mocks.startStubbing();
// If the domain has methods that return values, stub them here
mocks.stopStubbing();
Application.Domain.setMock(mockDomain);
For domain methods that return void (like applyDiscount), you do not need to define a thenReturn. The mock will simply do nothing when the method is called.
Verifying Domain Method Calls
After calling the service, verify that the domain method was invoked:
((IOpportunities) mocks.verify(mockDomain, 1))
.applyDiscount(10);
This confirms that the service correctly delegated to the domain with the right discount percentage.
Mocking Domain Methods That Return Values
If your domain has a method that returns a value, stub it just like a selector:
mocks.startStubbing();
mocks.when(mockDomain.getHighValueOpportunities())
.thenReturn(filteredOpps);
mocks.stopStubbing();
Mocking the Service Layer
You may need to mock a service when testing a controller or another service that calls it. The pattern is the same:
fflib_ApexMocks mocks = new fflib_ApexMocks();
IOpportunitiesService mockService =
(IOpportunitiesService) mocks.mock(OpportunitiesServiceImpl.class);
mocks.startStubbing();
mocks.when(mockService.calculateTotalRevenue(accountId))
.thenReturn(1500000);
mocks.stopStubbing();
Application.Service.setMock(IOpportunitiesService.class, mockService);
The service factory uses a slightly different setMock signature — you pass the interface type as the first argument so the factory knows which interface to mock.
Testing a Controller with a Mocked Service
Here is a test for a Visualforce controller that calls a service:
@IsTest
static void testControllerApplyDiscount() {
fflib_ApexMocks mocks = new fflib_ApexMocks();
IOpportunitiesService mockService =
(IOpportunitiesService) mocks.mock(OpportunitiesServiceImpl.class);
// No stubbing needed — applyDiscounts returns void
Application.Service.setMock(IOpportunitiesService.class, mockService);
// Simulate controller action
OpportunityDiscountController controller = new OpportunityDiscountController();
controller.selectedOppIds = new List<Id>{ fakeOppId };
controller.discountPercent = 10;
controller.applyDiscount();
// Verify the controller called the service correctly
((IOpportunitiesService) mocks.verify(mockService, 1))
.applyDiscounts(
(Set<Id>) fflib_Match.anyObject(),
fflib_Match.eqDecimal(10)
);
}
PROJECT: Unit Testing an fflib Application with Apex Mocks
Let us build a comprehensive test class that demonstrates every technique covered in this post. We will test the OpportunitiesService.applyDiscounts method and the OpportunitiesService.closeOpportunities method, mocking all three layers.
The Service Methods Under Test
public class OpportunitiesService {
public static void applyDiscounts(Set<Id> oppIds, Decimal discountPercent) {
fflib_SObjectUnitOfWork uow = Application.UnitOfWork.newInstance();
IOpportunitiesSelector selector =
(IOpportunitiesSelector) Application.Selector.newInstance(
Opportunity.SObjectType
);
List<Opportunity> opps = selector.selectByIdWithLineItems(oppIds);
IOpportunities domain =
(IOpportunities) Application.Domain.newInstance(opps);
domain.applyDiscount(discountPercent);
for (Opportunity opp : opps) {
uow.registerDirty(opp);
}
uow.commitWork();
}
public static void closeOpportunities(Set<Id> oppIds, String reason) {
fflib_SObjectUnitOfWork uow = Application.UnitOfWork.newInstance();
IOpportunitiesSelector selector =
(IOpportunitiesSelector) Application.Selector.newInstance(
Opportunity.SObjectType
);
List<Opportunity> opps = selector.selectByIdWithLineItems(oppIds);
if (opps.isEmpty()) {
throw new OpportunitiesServiceException('No opportunities found for the given Ids.');
}
IOpportunities domain =
(IOpportunities) Application.Domain.newInstance(opps);
domain.closeWithReason(reason);
for (Opportunity opp : opps) {
uow.registerDirty(opp);
}
uow.commitWork();
}
public class OpportunitiesServiceException extends Exception {}
}
The Complete Test Class
@IsTest
private class OpportunitiesServiceTest {
// ─────────────────────────────────────────────────────────
// Helper: Build a standard set of mocks for the Opportunities domain
// ─────────────────────────────────────────────────────────
private static fflib_ApexMocks setupMocks;
private static IOpportunitiesSelector mockSelector;
private static fflib_SObjectUnitOfWork mockUow;
private static IOpportunities mockDomain;
private static void initMocks() {
setupMocks = new fflib_ApexMocks();
mockSelector = (IOpportunitiesSelector) setupMocks.mock(
OpportunitiesSelector.class
);
mockUow = (fflib_SObjectUnitOfWork) setupMocks.mock(
fflib_SObjectUnitOfWork.class
);
mockDomain = (IOpportunities) setupMocks.mock(
Opportunities.class
);
}
// ─────────────────────────────────────────────────────────
// Helper: Generate fake Opportunity records
// ─────────────────────────────────────────────────────────
private static Id generateOppId() {
return fflib_IDGenerator.generate(Opportunity.SObjectType);
}
private static List<Opportunity> createFakeOpps(Integer count, Decimal amount) {
List<Opportunity> opps = new List<Opportunity>();
for (Integer i = 0; i < count; i++) {
opps.add(new Opportunity(
Id = generateOppId(),
Name = 'Test Opportunity ' + i,
Amount = amount,
StageName = 'Negotiation',
CloseDate = Date.today().addDays(30)
));
}
return opps;
}
private static Set<Id> extractIds(List<Opportunity> opps) {
Set<Id> ids = new Set<Id>();
for (Opportunity opp : opps) {
ids.add(opp.Id);
}
return ids;
}
// ─────────────────────────────────────────────────────────
// Helper: Configure standard selector stubbing
// ─────────────────────────────────────────────────────────
private static void stubSelector(Set<Id> oppIds, List<Opportunity> opps) {
setupMocks.startStubbing();
setupMocks.when(mockSelector.selectByIdWithLineItems(oppIds))
.thenReturn(opps);
setupMocks.when(mockSelector.sObjectType())
.thenReturn(Opportunity.SObjectType);
setupMocks.stopStubbing();
}
// ─────────────────────────────────────────────────────────
// Helper: Inject all mocks into the Application factory
// ─────────────────────────────────────────────────────────
private static void injectMocks() {
Application.Selector.setMock(mockSelector);
Application.UnitOfWork.setMock(mockUow);
Application.Domain.setMock(mockDomain);
}
// ═════════════════════════════════════════════════════════
// TEST: applyDiscounts — happy path with multiple records
// ═════════════════════════════════════════════════════════
@IsTest
static void applyDiscounts_givenMultipleOpps_shouldApplyDiscountAndCommit() {
// Arrange
initMocks();
List<Opportunity> fakeOpps = createFakeOpps(3, 100000);
Set<Id> fakeOppIds = extractIds(fakeOpps);
stubSelector(fakeOppIds, fakeOpps);
injectMocks();
// Act
Test.startTest();
OpportunitiesService.applyDiscounts(fakeOppIds, 20);
Test.stopTest();
// Assert — selector was called once with the correct Ids
((IOpportunitiesSelector) setupMocks.verify(mockSelector, 1))
.selectByIdWithLineItems(fakeOppIds);
// Assert — domain applyDiscount was called with 20%
((IOpportunities) setupMocks.verify(mockDomain, 1))
.applyDiscount(20);
// Assert — each opportunity was registered as dirty
((fflib_SObjectUnitOfWork) setupMocks.verify(mockUow, 3))
.registerDirty(fflib_Match.anySObject());
// Assert — commitWork was called exactly once
((fflib_SObjectUnitOfWork) setupMocks.verify(mockUow, 1))
.commitWork();
}
// ═════════════════════════════════════════════════════════
// TEST: applyDiscounts — single record
// ═════════════════════════════════════════════════════════
@IsTest
static void applyDiscounts_givenSingleOpp_shouldRegisterOneDirtyRecord() {
// Arrange
initMocks();
List<Opportunity> fakeOpps = createFakeOpps(1, 50000);
Set<Id> fakeOppIds = extractIds(fakeOpps);
stubSelector(fakeOppIds, fakeOpps);
injectMocks();
// Act
Test.startTest();
OpportunitiesService.applyDiscounts(fakeOppIds, 10);
Test.stopTest();
// Assert — only one record registered as dirty
((fflib_SObjectUnitOfWork) setupMocks.verify(mockUow, 1))
.registerDirty(fflib_Match.anySObject());
// Assert — commitWork called once
((fflib_SObjectUnitOfWork) setupMocks.verify(mockUow, 1))
.commitWork();
// Assert — discount applied with correct percentage
((IOpportunities) setupMocks.verify(mockDomain, 1))
.applyDiscount(10);
}
// ═════════════════════════════════════════════════════════
// TEST: applyDiscounts — zero discount
// ═════════════════════════════════════════════════════════
@IsTest
static void applyDiscounts_givenZeroDiscount_shouldStillProcessAndCommit() {
// Arrange
initMocks();
List<Opportunity> fakeOpps = createFakeOpps(2, 75000);
Set<Id> fakeOppIds = extractIds(fakeOpps);
stubSelector(fakeOppIds, fakeOpps);
injectMocks();
// Act
Test.startTest();
OpportunitiesService.applyDiscounts(fakeOppIds, 0);
Test.stopTest();
// Assert — domain receives zero discount
((IOpportunities) setupMocks.verify(mockDomain, 1))
.applyDiscount(0);
// Assert — records still registered and committed
((fflib_SObjectUnitOfWork) setupMocks.verify(mockUow, 2))
.registerDirty(fflib_Match.anySObject());
((fflib_SObjectUnitOfWork) setupMocks.verify(mockUow, 1))
.commitWork();
}
// ═════════════════════════════════════════════════════════
// TEST: applyDiscounts — empty selector result
// ═════════════════════════════════════════════════════════
@IsTest
static void applyDiscounts_givenNoMatchingOpps_shouldCommitWithNoRegistrations() {
// Arrange
initMocks();
Set<Id> fakeOppIds = new Set<Id>{ generateOppId() };
List<Opportunity> emptyResult = new List<Opportunity>();
stubSelector(fakeOppIds, emptyResult);
injectMocks();
// Act
Test.startTest();
OpportunitiesService.applyDiscounts(fakeOppIds, 10);
Test.stopTest();
// Assert — registerDirty was never called
((fflib_SObjectUnitOfWork) setupMocks.verify(mockUow, fflib_ApexMocks.never()))
.registerDirty(fflib_Match.anySObject());
// Assert — commitWork still called (empty commit is valid)
((fflib_SObjectUnitOfWork) setupMocks.verify(mockUow, 1))
.commitWork();
}
// ═════════════════════════════════════════════════════════
// TEST: closeOpportunities — happy path
// ═════════════════════════════════════════════════════════
@IsTest
static void closeOpportunities_givenValidOpps_shouldCloseAndCommit() {
// Arrange
initMocks();
List<Opportunity> fakeOpps = createFakeOpps(2, 80000);
Set<Id> fakeOppIds = extractIds(fakeOpps);
stubSelector(fakeOppIds, fakeOpps);
injectMocks();
// Act
Test.startTest();
OpportunitiesService.closeOpportunities(fakeOppIds, 'Customer requested');
Test.stopTest();
// Assert — selector queried the correct Ids
((IOpportunitiesSelector) setupMocks.verify(mockSelector, 1))
.selectByIdWithLineItems(fakeOppIds);
// Assert — domain closeWithReason called with the reason string
((IOpportunities) setupMocks.verify(mockDomain, 1))
.closeWithReason('Customer requested');
// Assert — two records registered as dirty
((fflib_SObjectUnitOfWork) setupMocks.verify(mockUow, 2))
.registerDirty(fflib_Match.anySObject());
// Assert — committed once
((fflib_SObjectUnitOfWork) setupMocks.verify(mockUow, 1))
.commitWork();
}
// ═════════════════════════════════════════════════════════
// TEST: closeOpportunities — no records found throws exception
// ═════════════════════════════════════════════════════════
@IsTest
static void closeOpportunities_givenNoOpps_shouldThrowException() {
// Arrange
initMocks();
Set<Id> fakeOppIds = new Set<Id>{ generateOppId() };
List<Opportunity> emptyResult = new List<Opportunity>();
stubSelector(fakeOppIds, emptyResult);
injectMocks();
// Act and Assert
Boolean exceptionThrown = false;
Test.startTest();
try {
OpportunitiesService.closeOpportunities(fakeOppIds, 'Lost deal');
} catch (OpportunitiesService.OpportunitiesServiceException e) {
exceptionThrown = true;
System.assertEquals(
'No opportunities found for the given Ids.',
e.getMessage(),
'Exception message should indicate no records found'
);
}
Test.stopTest();
System.assert(exceptionThrown, 'Expected OpportunitiesServiceException to be thrown');
// Assert — domain should never have been called
((IOpportunities) setupMocks.verify(mockDomain, fflib_ApexMocks.never()))
.closeWithReason((String) fflib_Match.anyString());
// Assert — nothing should have been registered or committed
((fflib_SObjectUnitOfWork) setupMocks.verify(mockUow, fflib_ApexMocks.never()))
.registerDirty(fflib_Match.anySObject());
((fflib_SObjectUnitOfWork) setupMocks.verify(mockUow, fflib_ApexMocks.never()))
.commitWork();
}
// ═════════════════════════════════════════════════════════
// TEST: applyDiscounts — verify argument matchers with sObjectWith
// ═════════════════════════════════════════════════════════
@IsTest
static void applyDiscounts_shouldRegisterCorrectSObjectFields() {
// This test demonstrates verifying specific field values
// on the SObjects passed to registerDirty.
//
// Note: Because the domain mock does nothing (it does not
// actually modify the Amount field), this test verifies
// the records as-is. In a real scenario where you skip
// the domain mock and test the service + domain together,
// you would see modified field values.
// Arrange
initMocks();
Id fakeOppId = generateOppId();
List<Opportunity> fakeOpps = new List<Opportunity>{
new Opportunity(
Id = fakeOppId,
Name = 'Enterprise Deal',
Amount = 200000,
StageName = 'Negotiation'
)
};
Set<Id> fakeOppIds = new Set<Id>{ fakeOppId };
stubSelector(fakeOppIds, fakeOpps);
injectMocks();
// Act
Test.startTest();
OpportunitiesService.applyDiscounts(fakeOppIds, 5);
Test.stopTest();
// Assert — verify registerDirty received an Opportunity
// with the specific Id we provided
((fflib_SObjectUnitOfWork) setupMocks.verify(mockUow, 1))
.registerDirty(
fflib_Match.sObjectWithId(fakeOppId)
);
}
// ═════════════════════════════════════════════════════════
// TEST: applyDiscounts — bulk scenario with 200 records
// ═════════════════════════════════════════════════════════
@IsTest
static void applyDiscounts_givenBulkRecords_shouldRegisterAllDirty() {
// Arrange
initMocks();
List<Opportunity> fakeOpps = createFakeOpps(200, 10000);
Set<Id> fakeOppIds = extractIds(fakeOpps);
stubSelector(fakeOppIds, fakeOpps);
injectMocks();
// Act
Test.startTest();
OpportunitiesService.applyDiscounts(fakeOppIds, 25);
Test.stopTest();
// Assert — all 200 records registered as dirty
((fflib_SObjectUnitOfWork) setupMocks.verify(mockUow, 200))
.registerDirty(fflib_Match.anySObject());
// Assert — still only one commit
((fflib_SObjectUnitOfWork) setupMocks.verify(mockUow, 1))
.commitWork();
// Assert — domain called once (it processes the entire list)
((IOpportunities) setupMocks.verify(mockDomain, 1))
.applyDiscount(25);
}
// ═════════════════════════════════════════════════════════
// TEST: applyDiscounts — verify selector NOT called with wrong args
// ═════════════════════════════════════════════════════════
@IsTest
static void applyDiscounts_shouldNotCallSelectorWithDifferentIds() {
// Arrange
initMocks();
List<Opportunity> fakeOpps = createFakeOpps(1, 60000);
Set<Id> fakeOppIds = extractIds(fakeOpps);
Set<Id> differentIds = new Set<Id>{ generateOppId() };
stubSelector(fakeOppIds, fakeOpps);
injectMocks();
// Act
Test.startTest();
OpportunitiesService.applyDiscounts(fakeOppIds, 10);
Test.stopTest();
// Assert — selector was never called with the different set of Ids
((IOpportunitiesSelector) setupMocks.verify(mockSelector, fflib_ApexMocks.never()))
.selectByIdWithLineItems(differentIds);
}
// ═════════════════════════════════════════════════════════
// TEST: closeOpportunities — verify atLeast usage
// ═════════════════════════════════════════════════════════
@IsTest
static void closeOpportunities_shouldCallRegisterDirtyAtLeastOnce() {
// Arrange
initMocks();
List<Opportunity> fakeOpps = createFakeOpps(5, 40000);
Set<Id> fakeOppIds = extractIds(fakeOpps);
stubSelector(fakeOppIds, fakeOpps);
injectMocks();
// Act
Test.startTest();
OpportunitiesService.closeOpportunities(fakeOppIds, 'Budget cut');
Test.stopTest();
// Assert — registerDirty called at least once
((fflib_SObjectUnitOfWork) setupMocks.verify(
mockUow, fflib_ApexMocks.atLeast(1)
)).registerDirty(fflib_Match.anySObject());
// Assert — registerDirty called at most 5 times
((fflib_SObjectUnitOfWork) setupMocks.verify(
mockUow, fflib_ApexMocks.atMost(5)
)).registerDirty(fflib_Match.anySObject());
}
}
What This Test Class Demonstrates
Let us review what we covered in this project:
- Helper methods for mock setup. The
initMocks(),stubSelector(), andinjectMocks()methods eliminate boilerplate. Every test method is focused on its specific scenario. - Fake Id generation.
fflib_IDGenerator.generate()creates realistic-looking Ids without touching the database. This is essential for mocking because many Salesforce APIs expect valid Id formats. - Happy path testing. The first two tests verify that the service correctly coordinates between the selector, domain, and Unit of Work for normal inputs.
- Edge case testing. We test zero discount, empty results, and bulk records — scenarios that are painful to set up with integration tests but trivial with mocks.
- Exception testing. The
closeOpportunities_givenNoOpps_shouldThrowExceptiontest verifies that the service throws a meaningful exception when no records are found, and also verifies that downstream methods (domain, UoW) were never called. - Argument matchers. We used
fflib_Match.anySObject(),fflib_Match.sObjectWithId(), andfflib_Match.anyString()to write flexible verifications. - Verification counts. We used exact counts (
verify(mock, 3)),never(),atLeast(), andatMost()to verify method call frequency. - Negative verification. The
shouldNotCallSelectorWithDifferentIdstest demonstrates verifying that a method was NOT called with specific arguments.
Running the Tests
When you run this test class, every method executes without any DML or SOQL. The entire class completes in under two seconds. Compare that to an equivalent integration test class that inserts Accounts, Contacts, Opportunities, and Line Items for every test method — that class would take 30 seconds or more.
To run the tests from the Salesforce CLI:
sf apex run test --class-names OpportunitiesServiceTest --result-format human --wait 5
You should see all test methods pass with zero SOQL queries and zero DML statements consumed.
Best Practices for Apex Mocks Testing
Before we close out, here are the practices that separate good mock-based tests from fragile ones.
Name Your Test Methods Descriptively
Use the pattern methodUnderTest_givenCondition_shouldExpectedBehavior:
applyDiscounts_givenZeroDiscount_shouldStillProcessAndCommit()
closeOpportunities_givenNoOpps_shouldThrowException()
This naming convention makes test failures self-documenting. When a test fails in a deployment, the method name tells you exactly what broke.
Test Behavior, Not Implementation
A common mistake with mocks is testing implementation details rather than behavior. Do not verify every single internal method call. Focus on the externally observable effects:
- Was the selector called with the right Ids?
- Was the Unit of Work committed?
- Was an exception thrown for invalid input?
If you verify every line of internal code, your tests become brittle. Refactoring the service (without changing its behavior) will break all your tests.
Keep One Assertion Theme Per Test
Each test method should verify one logical scenario. It is fine to have multiple verify calls if they all relate to the same scenario (for example, verifying that both registerDirty and commitWork were called). But do not test the happy path and the error path in the same method.
Use Helper Methods to Reduce Boilerplate
As we demonstrated in the project, extract mock setup into helper methods. This keeps your test methods focused on the scenario and makes the class easier to maintain.
Combine Unit Tests with Integration Tests
Unit tests and integration tests serve different purposes. Do not replace all your integration tests with unit tests. Instead, use unit tests for logic coverage and integration tests for wiring verification. A good rule of thumb: if the test is about “does my math work,” write a unit test. If the test is about “does this data actually get saved to the database,” write an integration test.
Mock at the Right Level
In an fflib application, mock at the factory level using Application.Selector.setMock(). This ensures that all code paths that use the factory will receive the mock. Do not try to mock individual SOQL queries or DML statements — mock the layer that owns them.
Summary
In this post, we covered the full spectrum of unit testing in Apex with the Apex Mocks library. We started by understanding the difference between unit tests and integration tests, and why most Salesforce tests are integration tests. We built a manual mock by hand to understand the pattern of dependency injection, then saw why that approach does not scale.
We introduced the Apex Mocks library — its history in the fflib ecosystem, its key classes (fflib_ApexMocks, fflib_Match), and how it leverages the Apex Stub API for dynamic mock generation. We walked through a complete test setup step by step: creating mocks, defining stubs with thenReturn, injecting mocks into the Application factory, executing the code under test, and verifying interactions with exact counts and argument matchers.
Finally, we built a complete test class with ten test methods covering happy paths, edge cases, exceptions, bulk scenarios, and negative verifications — all without a single database operation.
The combination of fflib Apex Common (for the architecture) and fflib Apex Mocks (for testing) gives you a development experience that is closer to what Java or C# developers expect: fast, isolated, focused tests that catch logic bugs instantly.
In the next post, Part 67, we will explore The Salesforce CLI — the command-line tool that every modern Salesforce developer uses for project creation, deployment, testing, data management, and automation. We will cover the full CLI workflow from scratch. See you there.