Skip to main content
New tool CRON Expression Builder — preview next run times before you schedule Apex. Open the builder →
A complex 3D circuit board illustrating an Apex trigger test class data flow issue.
Apex

Apex Trigger Not Updating: Test Class Field Value

Your trigger looks like it is ignoring the field values your test class sets. Usually it is stale in-memory data, a missing input field, the wrong context variable, or async work that has not run yet.

The short answer

When a trigger looks like it is ignoring a field your test class set, the cause is almost always one of four things: stale in-memory data that was never re-queried, a required input field left unset, the wrong context variable, or asynchronous work that has not run yet.

Key takeaways Always re-query: after DML in a test class, re-query the records before asserting on values a trigger or other automation modified. Test input fields: populate every field the trigger reads as input in your test data. Understand context: know the difference between Trigger.new and Trigger.old, and make sure the test observes the right one. Asynchronous testing: use Test.startTest() and Test.stopTest() so async work the trigger invokes runs and completes inside the test. Debug extensively: use System.debug() statements and the Apex Debugger to trace data flow and variable values.

Apex trigger not updating: why your test class field value isn't taking hold

You write a test for a trigger, set a field in the test class, and the trigger behaves as though that value was never there. The assertion fails, the test is unreliable, and you start doubting automation that is probably fine. The causes are almost always the same handful.

Understanding the test context in Apex

When you run tests, the platform creates a separate, isolated context so your tests cannot touch production data and each one runs independently. That isolation is where the confusion starts.

Data you create or modify during a test is visible only inside that test method, and it is rolled back when the method finishes, so the org is unchanged. Updates you make to records in a test method are available only to Apex running within that same test context. If your trigger reads information that was never committed or sits outside the test's scope, it will not behave the way you expect.

Key concepts

  • Test isolation: each test method runs in its own mini-transaction, and the data it creates is temporary.
  • Rollback: after a test method finishes, all changes are undone.
  • Test data visibility: data created inside a test method is visible only inside that test method.

Common pitfalls: why your trigger might be missing updates

A handful of mistakes account for nearly every case of a trigger not seeing field updates from a test class.

1. Not querying records after DML in tests

This is probably the most frequent offender. When you run a DML operation in a test class (an insert or an update), your local variable still holds what it held in memory beforehand. If you do not re-query the record after the DML, you are asserting against stale data.

You insert an Account in your test and immediately read a field the trigger is supposed to populate. Without a re-query after the insert, you are still looking at the original, pre-trigger values.

This version fails:

@isTest
private class AccountTriggerTest {
    @isTest
    static void testTriggerUpdatesField() {
        Account acc = new Account(Name = 'Test Account');
        insert acc;

        // Problem: 'acc' variable still holds the original values, 
        // not the ones potentially updated by the trigger.
        System.assertEquals('Expected Value', acc.MyCustomField__c, 'Trigger did not update field.');
    }
}

Re-query the record after the DML whenever your test asserts on values that a trigger or other automation modified:

@isTest
private class AccountTriggerTest {
    @isTest
    static void testTriggerUpdatesField() {
        Account acc = new Account(Name = 'Test Account');
        insert acc;

        // Re-query the account to get the latest values after DML and trigger execution
        Account updatedAcc = [SELECT Id, MyCustomField__c FROM Account WHERE Id = :acc.Id];

        System.assertEquals('Expected Value', updatedAcc.MyCustomField__c, 'Trigger did not update field.');
    }
}

2. Trigger logic relying on fields the test never sets

The trigger can be perfectly good and still do nothing, because the test class never set the specific fields it reads as input.

A trigger sets Account.BillingState from Account.ShippingState. Your test inserts an Account and sets only Account.Name. The trigger has no ShippingState to read, so BillingState stays where it was.

Here is the test that proves nothing:

@isTest
private class ContactTriggerTest {
    @isTest
    static void testTriggerLogic() {
        Contact con = new Contact(LastName = 'Test Last');
        // Missing to set a field that the trigger depends on
        insert con;

        Contact updatedCon = [SELECT Id, OtherField__c FROM Contact WHERE Id = :con.Id];
        System.assertNotEquals('Some Value', updatedCon.OtherField__c, 'Trigger should not have updated this field.');
    }
}

Populate every field the trigger reads as input:

@isTest
private class ContactTriggerTest {
    @isTest
    static void testTriggerLogic() {
        Contact con = new Contact(LastName = 'Test Last', TriggerInputField__c = 'Specific Value');
        insert con;

        Contact updatedCon = [SELECT Id, OtherField__c FROM Contact WHERE Id = :con.Id];
        System.assertEquals('Expected Value', updatedCon.OtherField__c, 'Trigger did not update OtherField__c based on TriggerInputField__c.');
    }
}

3. The wrong context variable (Trigger.new vs. Trigger.old)

Triggers get context variables: Trigger.new holds the new version of the records and Trigger.old the old one. Assert against a value that lives in Trigger.old while expecting it in Trigger.new, or the reverse, and the assertion fails. The test updated the record fine; it is observing the wrong version of it.

Say your trigger updates a field when Trigger.old.SomeField__c differs from Trigger.new.SomeField__c. The test creates a record, updates it, then asserts against Trigger.new while looking for a value that only ever existed in Trigger.old.

When to use which:

  • Trigger.new: Contains the new version of records that caused the trigger to fire.
  • Trigger.old: Contains the old version of records. Available only for update and delete trigger events.

When you test a trigger that relies on Trigger.old, have the test method perform an update and then query the records to see the effect.

// Example Trigger Logic (simplified)

public class AccountTriggerHandler {
    public static void handleBeforeUpdate(List<Account> newAccounts, Map<Id, Account> oldMap) {
        for (Account acc : newAccounts) {
            Account oldAcc = oldMap.get(acc.Id);
            if (oldAcc.Industry != acc.Industry) {
                // Logic based on the change
                acc.Description = 'Industry changed from ' + oldAcc.Industry + ' to ' + acc.Industry;
            }
        }
    }
}

// Example Test Class
@isTest
private class AccountTriggerTest {
    @isTest
    static void testIndustryChangeUpdatesDescription() {
        Account acc = new Account(Name = 'Test Account', Industry = 'Technology');
        insert acc;

        // Update the account
        acc.Industry = 'Finance';
        update acc;

        // Re-query to get the post-update values including trigger modifications
        Account updatedAcc = [SELECT Id, Description FROM Account WHERE Id = :acc.Id];

        // Assert against the 'new' version of the record after the update
        System.assertEquals('Industry changed from Technology to Finance', updatedAcc.Description, 'Description was not updated as expected.');
    }
}

4. Asynchronous trigger logic and test execution order

This one is rarer on simple before-update triggers. If your trigger kicks off asynchronous work (@future methods, Queueable Apex, Platform Events), that work is not guaranteed to finish inside the same test method's transaction. Assert on the results straight away and the test fails.

Testing asynchronous Apex:

  • Use Test.startTest() and Test.stopTest().
  • Test.stopTest() executes any queued asynchronous jobs (@future methods, System.enqueueJob) synchronously, which is the whole point when you are testing asynchronous logic.

Your before update trigger fires an @future method that updates a related record. The test inserts and updates the record, then asserts on the related record before the @future method has run.

This one asserts too early:

@isTest
private class FutureTriggerTest {
    @isTest
    static void testFutureMethodCall() {
        Account acc = new Account(Name = 'Test Account');
        insert acc;

        // Assume trigger calls a @future method to update related custom setting
        // This assertion happens BEFORE the future method has a chance to run
        Custom_Setting__c cs = Custom_Setting__c.getInstance();
        System.assertEquals('Updated Value', cs.Field__c, 'Future method did not update setting.'); 
    }
}

Wrap the DML that kicks off the async code in Test.startTest() and Test.stopTest():

@isTest
private class FutureTriggerTest {
    @isTest
    static void testFutureMethodCall() {
        Account acc = new Account(Name = 'Test Account');
        insert acc;

        Test.startTest();
        // Perform the DML that kicks off the async operation
        acc.SomeFieldToTriggerFuture__c = 'Trigger';
        update acc;
        Test.stopTest(); // This ensures the future method runs synchronously

        // Now assert the outcome of the future method
        Custom_Setting__c cs = Custom_Setting__c.getInstance();
        System.assertEquals('Updated Value', cs.Field__c, 'Future method did not update setting.');
    }
}

Debugging strategies for triggers and tests

When you hit this, work through the following.

  1. System.debug() statements. Sprinkle them liberally through the trigger and the test class. Log field values before and after DML in the test, and log Trigger.new and Trigger.old inside the trigger. That traces the data flow.

    • In the test:

      Account acc = new Account(Name = 'Test Account');
      System.debug('--- Before Insert: ' + acc);
      insert acc;
      Account insertedAcc = [SELECT Id, Name, MyCustomField__c FROM Account WHERE Id = :acc.Id];
      System.debug('--- After Insert (in test): ' + insertedAcc);
      // ... assertions ...
      
    • In the trigger:

      // Inside your trigger's before update context
      for (Account a : Trigger.new) {
          Account oldA = Trigger.oldMap.get(a.Id);
          System.debug('--- Trigger Before Update - New: ' + a + ', Old: ' + oldA);
          // ... trigger logic ...
      }
      
  2. Apex Debugger. Use it in the Salesforce Developer Console or in your IDE (VS Code with the Salesforce Extension Pack). Set breakpoints in the trigger and the test class, step through line by line, and inspect the variables. It shows you exactly what is happening at each step.

  3. Test visibility. Test code only sees data created within that test context. If your trigger depends on a record on another object that the test never created, it is not there. Create and populate the parent and related records too.

  4. Governor limits. Less likely to be behind a field update not being seen, but worth a look. A trigger that blows a limit partway through leaves the field unset, which looks identical from the test's side. Check the debug logs for limit errors.

Newsletter

One email every Tuesday

New guides, tool updates, and the release-note changes that break things.

No spam. Unsubscribe in one click.

Comments

Loading comments...

Leave a Comment