Monday, 10 August 2026

Transactions in Apex Integration Testing

Image generated by OpenAI GPT-5.5 from a prompt by Bob Buzzard

Introduction

In the first post in this series I covered a brief overview of the new Apex Integration Testing that is available in Developer Preview in Summer '26. In this instalment I'll take a deeper look at the how transactions work in this new testing paradigm. If you want to play along at home, you can find code samples to verify the behaviour at this Github repository. It might seem a little odd that I'm asserting behaviour rather than simply logging it etc, but I find this to be a useful approach when the implementation is likely to change. Come Winter '27 I'll just execute my tests again and if any fail I'll know something has changed. Tests as a tripwire FTW!

As always, please bear in mind that this functionality is in Developer Preview and the implementation could well change, rendering any findings in this post null and void. It's good fun to figure this stuff out though, so let's have at it.

Integration Tests Execute in a Single Transaction

This is probably the key takeaway of the current implementation. Just because records can be persisted to the Salesforce database as part of an integration test, the transaction doesn't end at that point. The docs for the new IntegrationTest.commitTestOnly() method make this abundantly clear:

Commits data to the database mid-transaction so it’s visible to service threads such as Agentforce and Data 360. It resets the uncommitted work checkpoint and mixed DML tracking for the new transaction boundary. 

The key word(s) here being mid-transaction - we are only part way through, and the transaction will continue for the rest of the test. Sadly this also means that DML limits are not reset by the mid-transaction commit, so we still have to squeeze all of our testing into a single limits context, although like unit tests we do still have the new limits context available through Test.startTest() once we've set up all our data. (See LimitUsageIntegrationTest.cls)

The other interesting part of the docs is the resetting of mixed DML tracking - I foresee that once this is GA we'll all be moving unit tests that have to workaround this particular issue over to integration tests. 

SavePoints Do Not Survive Mid-Transaction Commits

(Thanks to Steve Fouracre for the question about SavePoints - make sure to sign up for our AI events series).

As the transaction continues after a mid-point commit, you could be forgiven for thinking that any SavePoints created earlier in the transaction are still available for use, but sadly (again) that isn't the case. 

Attempting to use a SavePoint (rolling back or releasing) after a commit gives the following error:

18:34:22:291 EXCEPTION_THROWN [15]|System.TypeException:
                                                              Savepoint does not exist in this context

(See SavePointIntegrationTest.cls)

Integration Test Transactions Autocommit

If you make changes to the database but don't carry out a mid-transaction commit, those changes will be auto-committed when the test completes. In this respect an integration test is just like any other Salesforce request. You can see proof of this in AutoCommitIntegrationTest.cls, and note that I'm verifying this through an assertion in the @TearDown method:

@TearDown
static void tearDown() {    
    List<Account> accs=[SELECT Id, Name 
                        FROM Account
                        WHERE Name LIKE 'Integration Test Account%'
                        WITH USER_MODE]; Assert.areEqual(2, accs.size(),
               'Expected to find mid-transaction and auto committed test accounts'); delete as user accs; }

while certainly unusual, there's nothing prohibited about using asserts outside of test methods, and in this case it's the only way I can be sure I'm outside of the integration test transaction boundary. I'd steer clear of it in production tests though, as it will likely cause confusion.

TearDown Executes in a New Transaction

When the @TearDown method executes, it does so in a new transaction context and thus has a full set of governor limits to use to cleanup data. The teardown method autocommits, so as long as it completes successfully your cleanup changes will be persisted. It's worth point out again that teardown methods can error, so you need to use defensive coding to ensure you expect the unexpected. 

Talking of unexpected, it also appears that if you have multiple @TearDown methods, they each get their own transaction In TearDownTestTXIntegrationTest.cls I have two of these and in each I assert that the DML statements consumed is 0, which succeeds even though one of them has to have already executed and handled the delete:

@TearDown
static void tearDown() {
    Assert.areEqual(0, Limits.getDMLStatements(), 'Should be 0 DML statements consumed in teardown');
    delete as user [SELECT Id FROM Account WHERE Name = 'Integration Test Account' WITH USER_MODE];
}

@TearDown
static void tearDown2() {
    Assert.areEqual(0, Limits.getDMLStatements(), 'Should be 0 DML statements consumed in teardown');
    delete as user [SELECT Id FROM Account WHERE Name = 'Integration Test Account' WITH USER_MODE];
}

More Information


Follow on LinkedIn


No comments:

Post a Comment