# 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)
```apex
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)
```apex
@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

```apex
@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

```apex
@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)
```apex
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)
```apex
@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

```apex
@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

```apex
@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)
```apex
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)
```apex
@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
```apex
@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:

```apex
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.

```apex
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.

```apex
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](/apex-stem/docs/apex-eloquent-api-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.|

:::info
**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.

## 🔗 Back to Guide

← [Back to ApexEloquent Developer Guide](/apex-stem/docs/apex-eloquent-guide)
