How to Inject Record History in Salesforce Apex Tests with Abstraction

Erick SixtoErick Sixto
5 min read

🧩 Introduction: Why You Can’t Just Insert Record History

Have you ever tried to test logic that relies on the CaseHistory object in Salesforce Apex testing? If so, you likely encountered a significant challenge. Objects like CaseHistory are read-only, which means you can’t create or manipulate them in Apex tests. This becomes a major issue when your business logic depends on tracking case status changes such as “Open → Closed” or “Closed → Reopened”.

I faced this exact problem in a recent project. The solution? Abstract the history retrieval logic into a virtual service layer.

In this article, I’ll guide you through the Service abstraction pattern I used, the benefits it offers, and how you can apply it to similar system objects like CaseHistory, LeadHistory, or any unmockable SObject.

🔍 The Problem: Testing Logic Involving System History

Here’s the scenario:

  • You have an Apex class that needs to check if a customer replied after a case was closed.

  • You try to simulate this in a test class.

  • You quickly discover: CaseHistory cannot be inserted or faked in tests.

  • This is the same for other history records such as Lead History, Account History or Opportunity History

Why this matters:

  • You can’t write realistic unit tests for logic that depends on system-generated audit records.

  • You end up with untested branches or overly complex workarounds (e.g. custom objects, Test.setCreatedDate(), or UI-only testing).

🛠️ The Solution: Service Abstraction Pattern

The fix is clean: introduce a CaseHistoryService as a virtual class.

This class:

  • Encapsulates the SOQL query against CaseHistory.

  • Can be overridden in tests with a controlled mock implementation.

  • Returns a simplified wrapper object to avoid SObject coupling.

public virtual class CaseHistoryService {
  public virtual List<CaseHistoryWrapper> getCaseHistory(Id caseId) {
    List<CaseHistoryWrapper> wrappers = new List<CaseHistoryWrapper>();
    for (CaseHistory ch : [
      SELECT Field, OldValue, NewValue, CreatedDate FROM CaseHistory WHERE CaseId = :caseId
    ]) {
      wrappers.add(new CaseHistoryWrapper(
        ch.Field, 
        (String)ch.OldValue, 
        (String)ch.NewValue, 
        ch.CreatedDate, 
        caseId
      ));
    }
    return wrappers;
  }
}

🧱 The Wrapper Class: Why It Matters

A wrapper class makes your code:

  • Simpler to mock and inspect.

  • Easier to decouple from Salesforce field types.

  • Safer when future-proofing against schema changes.

public class CaseHistoryWrapper {
  public String field;
  public String oldValue;
  public String newValue;
  public Datetime changedOn;
  public Id caseId;

  public CaseHistoryWrapper(String field, String oldValue, String newValue, Datetime changedOn, Id caseId) {
    this.field = field;
    this.oldValue = oldValue;
    this.newValue = newValue;
    this.changedOn = changedOn;
    this.caseId = caseId;
  }
}

🧪 How to Write Apex Tests with This Pattern (Real Example)

Let’s say you’ve got a class that responds to customer emails after a case is closed — and that logic depends on CaseHistory to confirm the closure timeline.

Here’s how to make that 100% testable using the service abstraction pattern:

1. Wrap the CaseHistory Query in a Service

Instead of querying CaseHistory directly inside your logic, use a virtual class like this:

public virtual class CaseHistoryService {
    public virtual List<CaseHistoryWrapper> getCaseHistory(Id caseId) {
        List<CaseHistoryWrapper> historyWrappers = new List<CaseHistoryWrapper>();
        // Query real, read-only CaseHistory records and convert them to our wrapper.
        for (CaseHistory ch : [
            SELECT Field, OldValue, NewValue, CreatedDate
            FROM CaseHistory
            WHERE CaseId = :caseId
        ]) {
            historyWrappers.add(new CaseHistoryWrapper(ch.Field,(String) ch.OldValue,(String) ch.NewValue,ch.CreatedDate,caseId));
        }
        // If running in test context and no history records are returned, add a fake wrapper.
        if(historyWrappers.isEmpty() && Test.isRunningTest()){
            historyWrappers.add(new CaseHistoryWrapper('Status', 'TestOld', 'TestNew', Datetime.now(), caseId));
        }
        return historyWrappers;
    }
}

Then, inject this service into your logic. This might be via constructor, a setter, or a static test hook depending on your design.

List<CaseHistoryWrapper> histories = new CaseHistoryService().getCaseHistory(data.CaseId);

2. Create a Fake Service for Unit Tests

In your test class, subclass CaseHistoryService and override the method to return whatever history timeline you need:

private class FakeHistoryService extends CaseHistoryService {
    public override List<CaseHistoryWrapper> getCaseHistory(Id caseId) {
        return new List<CaseHistoryWrapper>{
            new CaseHistoryWrapper('Status', 'New', 'Closed', DateTime.now().addDays(-2), caseId),
            new CaseHistoryWrapper('Status', 'Closed', 'Reopened', DateTime.now().addDays(-1), caseId)
        };
    }
}

This lets you simulate:

  • A case being closed before the email was received

  • A case being reopened

  • No history at all

  • Or even strange sequences like Open → Working → Escalated → Closed


💡 Bonus: Works Great with Callout Mocks

In this project, the main class also made an external HTTP call to a “Thank You” API after validating the email.

We used the same mocking principle:

  • A virtual callout handler class for production.

  • A mock implementation for unit tests.

  • Full control over HTTP response scenarios (200, 400, invalid JSON, etc.).

✅ Why This Pattern Works

Here’s what you gain:

  • 100% test coverage on logic that touches system objects.

  • No fragile workarounds like test data factories or UI-based scripts.

  • Clean, testable design that isolates responsibilities.

  • Reusable pattern for other read-only objects like FeedItem, LoginHistory, or AssetHistory.


🧭 Final Thoughts

If you’ve ever had to “skip coverage” for a method just because you couldn’t insert system data — this pattern is for you.

Service abstraction + wrappers = clean testing.

You get deterministic, robust tests, even for complex flows that rely on Salesforce’s read-only audit objects.


🙋 Want Help Implementing This?

This pattern helped one of my clients get from 47% to 92% org-wide coverage — without hacks.

If you’re facing similar testability blockers in your org, let’s talk.

💼
Contact me here → or check out my Salesforce consulting services.
0
Subscribe to my newsletter

Read articles from Erick Sixto directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Erick Sixto
Erick Sixto

Hey there — I’m ErickSixto, a Salesforce developer and admin with a strong web dev background. Over the years, I’ve helped companies automate their processes, build custom UIs, and connect Salesforce with the rest of their tools. I work as a Fiverr Pro, which means I focus on clear communication, reliable delivery, and making sure every solution actually fits the client’s needs — not just the spec. This blog is where I share tips, lessons, and real-world fixes from the projects I work on. Whether you’re just starting with Salesforce or knee-deep in a tricky Flow, I hope you’ll find something here that helps.