How to Inject Record History in Salesforce Apex Tests with Abstraction

Table of contents
- 🧩 Introduction: Why You Can’t Just Insert Record History
- 🔍 The Problem: Testing Logic Involving System History
- 🛠️ The Solution: Service Abstraction Pattern
- 🧱 The Wrapper Class: Why It Matters
- 🧪 How to Write Apex Tests with This Pattern (Real Example)
- 💡 Bonus: Works Great with Callout Mocks
- ✅ Why This Pattern Works
- 🧭 Final Thoughts
- 🙋 Want Help Implementing This?

🧩 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.
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.