Complete False Positive Detection Guide
This comprehensive guide covers ApexEloquent's MockEntry false positive detection system with detailed examples, real-world use cases, and advanced testing patterns.
The Core Problem
Traditional Salesforce testing relies on TestDataFactory and database inserts. When developers move to mock-based testing for performance benefits, they create a dangerous gap: mock tests pass when production fails.
This happens because mock objects in tests have all fields populated in memory, while production SOQL queries may miss required fields. ApexEloquent's MockEntry closes this gap by validating field access against your actual query structure, ensuring mocks behave like real SOQL results.
🧪 Use Case 1: Detecting Omissions on the Main Object
Let's see a real-world example with a service class that forgets to select the Name field:
The Service Class (with a bug)
public with sharing class OppNameUpdater {
private final Id oppId;
private final IEloquent eloquent;
public OppNameUpdater(Id oppId, IEloquent eloquent) {
this.oppId = oppId;
this.eloquent = eloquent;
}
public Opportunity execute() {
Scribe scribe = Scribe.source(Opportunity.getSObjectType())
.field('Id') // ← Forgetting to select 'Name'
.whereEqual('Id', this.oppId);
IEntry oppEntry = this.eloquent.first(scribe);
// This line will throw an exception because 'Name' was not selected.
System.debug('Opportunity Name: ' + oppEntry.getName());
Opportunity opp = (Opportunity) oppEntry.getRecord();
opp.Name = 'Updated Name';
return(Opportunity) this.eloquent.doUpdate(opp);
}
}
The Test Class (Catches the Error)
@isTest
private class OppNameUpdaterTest {
@isTest
static void testExecute_WhenNameNotSelected_ThrowsException() {
// Arrange
IEloquent mockEloquent = new MockEloquent(
MockEntry.of(Opportunity.getSObjectType()).autoId('1')
);
OppNameUpdater updater = new OppNameUpdater('006000000000001AAA', mockEloquent);
// Act & Assert
try {
updater.execute();
Assert.fail('Expected a QueryException to be thrown.');
} catch(QueryException e) {
// Assert that the helpful error message is correct
String expectedMessage = 'The specified field is not selected in Scribe. object name: Opportunity, field name: Name';
Assert.areEqual(expectedMessage, e.getMessage());
}
}
}
👏 Perfect! We detected the missing Name field during our unit test, well before deploying to production.
🔄 Detailed Field Access Validation
MockEntry provides comprehensive validation for all field access patterns:
1. Direct Field Access
@isTest
static void testDirectFieldAccess() {
// Only 'Id' selected, not 'Name'
Scribe scribe = Scribe.source(Account.getSObjectType()).field('Id');
MockEntry mockEntry = MockEntry.of(Account.getSObjectType()).autoId(1);
mockEntry = (MockEntry) mockEntry.setFieldStructure(scribe.buildFieldStructure());
// ❌ This throws QueryException
String name = (String) mockEntry.get('Name');
}
2. getId() and getName() Methods
@isTest
static void testGetIdMethod() {
// Only 'Name' selected, not 'Id'
Scribe scribe = Scribe.source(Account.getSObjectType()).field('Name');
MockEntry mockEntry = MockEntry.of(Account.getSObjectType()).add('Name', 'Test Account');
mockEntry = (MockEntry) mockEntry.setFieldStructure(scribe.buildFieldStructure());
try {
Id accountId = mockEntry.getId(); // ❌ Id not selected
Assert.fail('Expected QueryException');
} catch(QueryException e) {
Assert.areEqual(
'The specified field is not selected in Scribe. object name: Account, field name: Id',
e.getMessage()
);
}
}
🔗 Use Case 2: Detecting Omissions in Relationships
This powerful validation works for parent and child relationships, too. Here, the code forgets to select the parent Account's Name:
The Service Class (with relationship bug)
public Opportunity execute() {
Scribe scribe = Scribe.source(Opportunity.getSObjectType())
.field('Id')
.parentField(
Scribe.asParent('AccountId').field('Id') // ← Forgetting to select Account's 'Name'
)
.whereEqual('Id', this.oppId);
IEntry oppEntry = this.eloquent.first(scribe);
IEntry accountEntry = oppEntry.getParent('AccountId');
// This line will now throw an exception
System.debug('Account Name: ' + accountEntry.getName());
// ...
}
The Test Class (Catches the Relationship Error)
@isTest
static void testExecute_WhenAccountNameNotSelected_ThrowsException() {
// Arrange
IEloquent mockEloquent = new MockEloquent(
MockEntry.of(Opportunity.getSObjectType())
.autoId(1)
.addParent('AccountId', MockEntry.of(Account.getSObjectType()).autoId(1))
);
OppNameUpdater updater = new OppNameUpdater('006000000000001AAA', mockEloquent);
// Act & Assert
try {
updater.execute();
Assert.fail('Expected a QueryException to be thrown.');
} catch(QueryException e) {
// Assert that the error message correctly identifies the missing field in the parent
String expected = 'The specified field is not selected in Scribe. object name: Account, field name: Name';
Assert.areEqual(expected, e.getMessage());
}
}
3. Parent Relationship Access Validation
@isTest
static void testParentAccess() {
// Missing AccountId in field selection
Scribe scribe = Scribe.source(Opportunity.getSObjectType()).field('Id');
MockEntry mockEntry = MockEntry.of(Opportunity.getSObjectType())
.autoId(1)
.setFieldStructure(scribe.buildFieldStructure());
try {
IEntry parent = mockEntry.getParent('AccountId'); // ❌ AccountId not selected
Assert.fail('Expected QueryException');
} catch(QueryException e) {
Assert.areEqual(
'The specified parentIdFieldName is not set in Scribe. object name: Opportunity, parent Id field name: AccountId',
e.getMessage()
);
}
}
4. Child Relationship Access
@isTest
static void testChildAccess() {
// Child fields not properly selected
Scribe scribe = Scribe.source(Account.getSObjectType())
.field('Id')
.withChildren(
Scribe.asChild(Contract.getSObjectType()).field('Id') // Missing 'Name' field
);
MockEntry mockEntry = MockEntry.of(Account.getSObjectType())
.autoId(1)
.addChildren(
'Contract',
new List<MockEntry>{
MockEntry.of(Contract.getSObjectType()).add('Name', 'Mock Contract 1'), // set Name field value
MockEntry.of(Contract.getSObjectType()).add('Name', 'Mock Contract 2')
}
);
mockEntry = (MockEntry) mockEntry.setFieldStructure(scribe.buildFieldStructure());
List<IEntry> contracts = mockEntry.getChildren('Contract');
try {
String contractName = (String) contracts[0].get('Name'); // ❌ Name value is set but not selected in scribe!
Assert.fail('Expected QueryException');
} catch(QueryException e) {
Assert.areEqual(
'The specified field is not selected in Scribe. object name: Contract, field name: Name',
e.getMessage()
);
}
}
🧮 Use Case 3: Aggregate Query Validation
MockEntry also validates field access for aggregate queries, ensuring that aliases are correctly used:
The Service Class (with aggregate bug)
public class OpportunityAnalytics {
private IEloquent eloquent;
public OpportunityAnalytics(IEloquent eloquent) {
this.eloquent = eloquent;
}
public void processAverages() {
Scribe scribe = Scribe.source(Opportunity.getSObjectType())
.field('StageName')
.average('Amount', 'avgAmount');
List<IEntry> results = this.eloquent.get(scribe);
for(IEntry result : results) {
Decimal avgAmount = (Decimal) result.get('avgAmount'); // ✅ Correct alias
Decimal totalAmount = (Decimal) result.get('totalAmount'); // ❌ Wrong alias
System.debug('Average: ' + avgAmount + ', Total: ' + totalAmount);
}
}
}
The Test Class (Catches the Aggregate Error)
@isTest
static void testAnalytics_WhenAccessingWrongAlias_ThrowsException() {
// Arrange: Set up the mock aggregate result
IEloquent mockEloquent = new MockEloquent(
new MockEntry(new Map<String, Object>{
'StageName' => 'Prospecting',
'avgAmount' => 5000
})
);
OpportunityAnalytics service = new OpportunityAnalytics(mockEloquent);
// Act & Assert
try {
service.processAverages();
Assert.fail('Expected a QueryException to be thrown.');
} catch(QueryException e) {
// Assert that the error message correctly identifies the missing alias
String expected = 'The specified field or alias is not exist in Scribe. field or alias name: totalAmount';
Assert.areEqual(expected, e.getMessage());
}
}
Additional Aggregate Validation Example
@isTest
static void testAggregateValidation() {
Scribe scribe = Scribe.source(Opportunity.getSObjectType())
.field('StageName')
.average('Amount', 'avgAmount');
MockEntry mockEntry = MockEntry.of(Opportunity.getSObjectType());
mockEntry = (MockEntry) mockEntry.setFieldStructure(scribe.buildFieldStructure());
mockEntry = mockEntry.add('StageName', 'Closed Won')
.add('avgAmount', 5000);
try {
// ❌ Accessing non-existent field in aggregate result
Object maxAmount = mockEntry.get('maxAmount');
Assert.fail('Expected QueryException');
} catch(QueryException e) {
Assert.areEqual(
'The specified field or alias is not exist in Scribe. field or alias name: maxAmount',
e.getMessage()
);
}
}
✅ This proves that the safety net works for both standard fields and aggregate aliases.
🔧 Integration with Business Logic
MockEntry validation works seamlessly with your business logic classes:
public class AccountService {
private IEloquent eloquent;
public AccountService(IEloquent eloquent) {
this.eloquent = eloquent;
}
public List<Account> getAccountsWithIndustry() {
// ❌ Query missing 'Industry' field
Scribe scribe = Scribe.source(Account.getSObjectType())
.fields(new List<String>{'Id', 'Name'})
.whereEqual('Type', 'Customer');
List<IEntry> entries = eloquent.get(scribe);
List<Account> accounts = new List<Account>();
for(IEntry entry : entries) {
Account acc = new Account();
acc.Id = entry.getId();
acc.Name = (String) entry.get('Name');
acc.Industry = (String) entry.get('Industry'); // ❌ Will fail here
accounts.add(acc);
}
return accounts;
}
}
@isTest
static void testAccountService() {
// Arrange
MockEloquent mockEloquent = new MockEloquent();
AccountService service = new AccountService(mockEloquent);
// Act & Assert - Test catches the missing field selection
try {
service.getAccountsWithIndustry();
Assert.fail('Expected QueryException for missing Industry field');
} catch(QueryException e) {
// Test correctly identifies the production bug
Assert.isTrue(e.getMessage().contains('Industry'));
}
}
Two Safety Nets Beyond SELECT Omission
All four cases above came down to "check the Scribe's SELECT clause against the access". Two other mechanisms close the same "green while verifying nothing" hole.
Forgetting to feed the mock throws
In a test that uses labels, forgetting attach('X', ...) while fetching under label('X') — or mistyping the label — makes that query return zero rows. You fall into the "nothing to do, skip" branch and the test goes green having verified nothing.
Today, calling get / first / firstOrFail under a label that was never attached throws, and the error lists the labels that were attached.
When you genuinely want the zero-row path, attach an empty list to declare it.
MockEloquent mock = (new MockEloquent())
.attach(MyUsecase.LBL_FETCH, new List<IEntry>());
The point of the design is that "not attached" and "attached empty" are distinct states.
Entries handed over directly can carry a contract too
The four cases above concerned entries returned through MockEloquent; they pass through a Scribe, so the contract attaches automatically.
Entries handed straight to the SUT have no contract. The classic case is a batch: records reaching execute(bc, scope) never pass through IEloquent, so production is covered by the platform (they are real query results) while tests check nothing.
fetchedBy(scribe) attaches the contract after the fact.
MockEntry card = MockEntry.of(BusinessCard__c.class)
.autoId(1)
.set('CompanyName__c', 'Acme')
.fetchedBy(RematchCompanyCardsHandler.scope());
Pull the query construction into a @TestVisible method and your test hands over exactly the same Scribe production uses. See API Reference: Scribe.
📝 Summary
| Feature | Description |
|---|---|
| Automatic Validation | MockEntry throws an exception if you access a field not selected in Scribe. |
| High-Fidelity Mocks | The mock system reproduces the exact validation behavior of a live query. |
| Full Relationship Support | This validation works across parent, child, and many-to-many relationships. |
| Aggregate Query Support | Validates access to aggregate function aliases and prevents wrong field access. |
| Prevents Production Errors | Gives you confidence that a test that passes will also work in production. |
Database-less tests that don't miss query defects.
This is the core of ApexEloquent's testing philosophy. It provides a robust safety net that elevates your testing strategy to the next level.
MockEntry's false positive detection ensures your tests reflect real production behavior, giving you true confidence in your code quality and preventing unexpected runtime failures. By catching field selection errors during development rather than in production, you can deploy with confidence knowing your SOQL queries are complete and correct.