Testing time-sensitive business logic is a common requirement in Salesforce development. Whether you are building an escalation rule for Cases, a cleanup utility for old Leads, or a custom SLA tracker, your code likely relies on the CreatedDate field. However, as an audit field, CreatedDate is notoriously difficult to manipulate because it is read-only under normal circumstances.

In this guide, you will learn multiple strategies to overcome this limitation, ranging from the official Test.setCreatedDate method to advanced mocking techniques using JSON deserialization. By the end of this article, you will be able to write robust unit tests that accurately simulate records created in the past.

The Challenge: Why CreatedDate is Read-Only

Salesforce maintains strict integrity for audit fields like CreatedDate, LastModifiedDate, and CreatedById. These fields are automatically populated by the system and cannot be modified through standard DML operations. While this is excellent for security and auditing, it presents a significant hurdle for developers.

If you need to test a trigger that only fires when a Case is more than three days old, you cannot simply wait three days for your unit test to run. You need a way to "time travel" within your test context to verify that your logic handles older records correctly.

Method 1: Using the Official Test.setCreatedDate Method

Since the Spring '16 release, Salesforce has provided a built-in method specifically designed for this scenario: Test.setCreatedDate(recordId, createdDatetime). This is the most reliable and recommended way to handle this requirement.

This method allows you to update the CreatedDate of a record that has already been inserted into the test database. Here is a practical example of how to use it:

Account account = new Account(Name = 'Test Account');
insert account;

// Set the CreatedDate to exactly 24 hours ago
Datetime yesterday = Datetime.now().addDays(-1);
Test.setCreatedDate(account.Id, yesterday);

// Verify the change
Account updatedAccount = [SELECT CreatedDate FROM Account WHERE Id = :account.Id];
System.assertEquals(yesterday, updatedAccount.CreatedDate);

Important Constraints

While Test.setCreatedDate is powerful, it has a few strict rules: 1. Record must exist: You must insert the record before calling this method. 2. No SeeAllData: You cannot use this method in test classes annotated with @isTest(SeeAllData=true). 3. Rollback behavior: Like all test database changes, these updates are rolled back at the end of the test. 4. No future dates: Setting the date to a future time can cause unexpected platform behavior and should be avoided.

Method 2: The JSON Deserialization Trick

Sometimes you need to test logic that doesn't necessarily require the record to be in the database, or you need to mock other read-only fields like LastModifiedDate. In these cases, JSON deserialization is a brilliant workaround.

By converting an sObject to a JSON string, you can manually inject values into read-only fields and then deserialize it back into an object. This creates an in-memory instance of the record with the fields you've specified.

// Construct a JSON string representing the SObject
String caseJSON = '{"attributes":{"type":"Case"},"Id":"500000000000001","CreatedDate":"2023-10-04T17:54:26.000+0000"}';

// Deserialize back into a Case object
Case c = (Case) JSON.deserialize(caseJSON, Case.class);

System.debug('Mocked CreatedDate: ' + c.CreatedDate); // Outputs 2023-10-04...

Pros: Works for almost any read-only field and doesn't require DML. Cons: The record is only in memory. If your code performs a SOQL query later, it won't find this mocked record unless you use a service layer or dependency injection pattern.

Method 3: Enabling "Set Audit Fields" Permissions

For complex integration tests or data migration simulations, you might want to set the CreatedDate directly during the initial insert operation. This is possible by enabling specific organizational permissions.

  1. Go to Setup > User Interface.
  2. Check the box for Enable "Set Audit Fields upon Record Creation".

Once enabled, you can assign a Permission Set to your test user that includes the PermissionsCreateAuditFields permission. In your Apex test, you can then use System.runAs() to insert records with a custom CreatedDate.

// Note: This requires the permission set to be created and assigned
System.runAs(userWithAuditPermissions) {
    Account a = new Account(
        Name = 'Audit Account', 
        CreatedDate = Datetime.now().addDays(-10)
    );
    insert a;
}

Method 4: Decoupling Logic with Time Offsets

If you prefer to keep your tests simple and avoid platform-specific workarounds, you can design your business logic to be "test-aware." This is often done using Custom Metadata or a static variable to act as a time offset.

For example, instead of hardcoding Datetime.now(), you can use a helper method:

public class DateService {
    @TestVisible private static Datetime mockNow;

    public static Datetime now() {
        return (Test.isRunningTest() && mockNow != null) ? mockNow : Datetime.now();
    }
}

In your business logic, you compare CreatedDate against DateService.now(). In your test, you simply set DateService.mockNow = Datetime.now().addDays(5), effectively making the record look "old" relative to your mocked current time.

Frequently Asked Questions

Can I use Test.setCreatedDate for multiple records at once?

Yes, but you must call the method for each individual record ID. There is currently no bulk version of Test.setCreatedDate, so you should wrap the calls in a loop if you are generating large sets of test data.

Why does my SOQL query return the original date after using JSON.deserialize?

JSON deserialization only creates a copy of the object in the application's heap memory. It does not update the database. If your code under test performs its own [SELECT ...] query, it will fetch the real data from the database, ignoring your in-memory mock.

Is there a way to set LastModifiedDate in a test?

Test.setCreatedDate only works for the CreatedDate field. To mock LastModifiedDate, you must use the JSON deserialization method or the "Set Audit Fields" permission approach mentioned above.

Wrapping Up

Testing time-dependent logic in Apex doesn't have to be a headache. For most scenarios, Test.setCreatedDate is the cleanest and most supported approach. However, understanding alternative methods like JSON deserialization and permission-based inserts gives you a full toolkit to handle even the most complex edge cases.

By mastering these techniques, you ensure that your automation—whether it's an aging-based escalation or a scheduled cleanup—is fully verified and production-ready. Always remember to keep your tests isolated and avoid future-dating your records to maintain the stability of your test environment.