Pages

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


Wednesday, 29 July 2026

Apex Integration Testing in Summer '26

Image eventually created by OpenAI GPT-5.5 after
about 5 prompts by Bob Buzzard

Introduction

A feature that seems to have flown under the radar in the Summer '26 release of Salesforce is the new Apex Integration Testing capability. For the first time integration testing (albeit quite limited) is a first-class discipline on the platform. With a bit of luck this foreshadows an end to our reliance on external tooling and Heath Robinson (or Rube Goldberg for our friends across the pond) combinations of unit tests and mocks masquerading as integration tests. 

Before we get too excited it's worth bearing in mind that this is currently in Developer Preview for scratch orgs only, and is limited to interactions with the Data 360 and Agentforce Salesforce services, but hopefully this will be relaxed in the future and we'll be able to make callouts to any authorised endpoint that we choose. 

If you'd like to know more about Apex integration testing, I've covered it in depth (as much as possible for a developer preview feature, obviously) in my new book: Software Testing on the Salesforce Platform

Integration Tests

If you haven't come across integration testing before, you can think of it as the next programmatic testing step after unit testing. Unit tests check whether a piece of code works in total isolation, while integration tests check that the various unit tested pieces still work correctly when they are connected to each other. 

An Apex class/method becomes an integration test through the simple application of the @IntegrationTest annotation where a unit test would have the @IsTest annotation. Aside from that, they start out rather similar to Apex unit tests: 

  • Set up data 
  • Execute code under test
  • Verify results.
Where they diverge (but align with just about every other unit test framework in the world) is tearing down the changes made. In Salesforce world we've never had to care about that before, as the Apex unit test framework rolls back any changes we've made automatically. Integration tests commit changes to the Salesforce database, so there's clearly a need to reset the database to a known, stable state. 

Teardown methods are identified through the @TearDown annotation, are executed after every test regardless of success or failure, and commit automatically. You can also have more than one of them in a class, in which case they will all execute after every test and you can't specify the order that they execute in. 

It's also really important to bear in mind that just because teardown methods run after every test, they won't necessarily complete successfully. They can error, just like any other piece of code. If they do error, the test will be marked as a failure, as well as the database being left in an indeterminate state. For this reason, defensive code is a must for teardown methods. 

Executing Integration Tests

As integration testing is in developer preview, it can only be used in scratch orgs at present. Enable integration testing through the ApexIntegrationTests feature:


Once you have created a scratch org with this configuration, you can deploy integration test classes to it. You can execute integration test classes like unit test classes, through setup, the developer console or the Salesforce CLI, although remember they have to run asynchronously.

You can only have a single integration test running in an org at any point in time. This isn't an issue in a scratch org, but could be entertaining in sandboxes - everyone gets a couple of hours a week to run tests in peace maybe? Although I'm sure that restriction will be loosened as the functionality approaches GA. 

Apex governor limits apply to integration tests, which probably restricts how useful they will actually be. This is another aspect I wouldn't be surprised to see loosened over time, they are running asynchronously after all, but it's unlikely we'll get to parity with external tools that can span multiple transactions. 

More Information

Follow on LinkedIn


Monday, 19 January 2026

Run Relevant Tests Annotations in Spring '26

Image created by ChatGPT5.2 based on a prompt by Bob Buzzard

Introduction


In my first post on this new functionality I did a bit of digging into which tests are chosen when code is changed. If leaving it up to Salesforce doesn't quite cut it, there's the option to influence things via the two new parameters for the @isTest annotation. 

As in the first post, I've got a small set of classes with dependencies that are sometimes static and sometimes dynamic. As a refresher the key classes are:
  • OpportunityUtils
    The protagonist in my little drama is a class named  This implements an interface (OpportunityUtilsIF) with a single method, getBigDeals(), which receives a collection of opportunities and returns a new collection containing just those opportunities with a value greater than or equal to 250,000.

    There is a dedicated test class (OpportunityUtilsTest) which directly instantiates the class and executes a zero/one test. 
  • OpportunityEOD
    This contains a single method (EODProcessing) that extracts all opportunities created today, creates an instance of OpportunityUtils, extracts just the big deals, appends ' - BIG DEAL' to their name and updates them.

    There is a dedicated test class (OpportunityEODTest) that inserts test opportunities of varying values, instantiates OpportunityEOD and executes the EODProcessing method, then extracts all opportunities from the database that are greater than or equal to 250,000 and asserts each name contains ' - BIG DEAL'.

  • OpportunityWrapLevel1
    This contains a single method (EODProcessingWrapLevel1) that instantiates the OpportunityEOD class and executes the EODProcessing method.

    There is a dedicated test class (OpportunityWrapLevel1Test) that inserts test opportunities of varying levels, instantiates OpportunityEODWrapLevel1, executes the EODProcessingWrapLevel1 method, then extracts all opportunities from the database that are greater than or equal to 250,000 and asserts each name contains ' - BIG DEAL'.

  • OpportunityEODInjection
    This  contains a replica of the EODProcessing() method, but rather than directly instantiating OpportunityUtils it is passed a parameter (implementing the OpportunityUtilsIF interface. There is a dedicated test class (OpportunityEODInjectionTest) that delegates to a test factory to dynamically create an instance of OpportunityUtils based on the class name - at no point is OpportunityUtils directly referred to. The test mirrors the other EOD tests, inserting opportunities, carrying out the EOD processing and verifying that ' - BIG DEAL' is appended where appropriate.

As we are now in the scratch org preview window, I was able to use my pre-release developer edition as a Dev Hub and create a Spring '26 scratch org, which speeds things up enormously and lets me scrap everything and start again from scratch with minimum effort.

@IsTest(critical=true)


This parameter tells the Salesforce test engine that the tests in this class must execute when a deployment takes place. I originally misread the docs on this and thought the test only executed if the payload contained Apex changes, but that isn't the case. Setting a test class as critical means it always executes for a deployment that runs relevant tests regardless of what is being deployed. To confirm this, I returned to the classic example of a configuration change that breaks tests with ease - the validation rule!

I marked my OpportunityEODTest class as critical, attempted to deploy an Opportunity validation rule that required one of Description or Lead Source to be populated, and duly watched the test execute and the deployment fail! 

This is a great addition, as the critical tests are identified without the deployer having to remember which tests they should always run. That said the deployer does still have to remember to deploy with the RunRelevantTests option, so it's not a silver bullet.

@IsTest(testFor='<classes_and_triggers>')


This parameter comes in handy if the Salesforce test engine isn't picking up all the tests that matter. In my first post I explained how it isn't really reasonable to expect the test engine to pick up dynamic instantiation of the OpportunityUtils class based on its name, which is how it's used in OpportunityEODInjection. Using the testFor parameter I can give the test engine the helping hand it needs:
    @isTest(testFor='ApexClass:OpportunityUtils')
    private class OpportunityEODInjectionTest {
        @isTest
        static void TestEODProcessing() {
           ...
        }
    }
  
Note that I don't have to include OpportunityEODInjection in the list of classes identified in the testFor parameter. I'm adding to the tests that are executed, not overriding them. This test is also executed if I change the OpportunityEODInjection class, as it has a direct dependency on it which the Salesforce test engine can pick up.

Even though that is the case, I think in a real-world environment I'd prefer to list all the classes/triggers that the test class is associated with, as it improves clarity and saves a developer having to figure things out manually, even if there is overhead to create and maintain this information. 

More Information


Follow on LinkedIn



Tuesday, 13 January 2026

Run Relevant Tests in Spring '26

Image created by GPT5.2 based on a prompt by Bob Buzzard

Introduction


The Spring '26 release of Salesforce introduces a new way to execute tests when deploying - Run Relevant Tests. Specifying this option essentially hands over responsibility to Salesforce to identify and execute the tests associated with any Apex code in the deployment payload. Note that this is in beta in Spring '26, and everything following was based on trying it out in a pre-release org in the first half of January 2026, so really early days for it. 

This is the archetypal double-edged sword in my view. One the one hand, deployments can run way faster with little or no human effort, especially compared to manually specifying the tests that should be executed. On the other hand, it's abdicating responsibility for the quality of the deployment. Given that we're already looking at abdicating responsibility for the development of software to AI tools, does ceding another aspect of the lifecycle really matter? 

Who Chooses the Tests?


While conceptually this is something that should require many sleepless nights and lengthy discussions, I think in the majority of cases, especially outside of ISVs, it really doesn't matter. Yes, the test engine might not get it 100% right 100% of the time, but neither will most developers. If we're being honest the test suite itself is likely sub-optimal, especially in mature orgs at the Enterprise level where lots of disconnected parties have focused on getting things live over the years. In this scenario, if the odd test gets missed or an extra test gets run, it doesn't really change much from the quality perspective. 

This is not the case for ISVs though, who tend to put a lot of effort into designing a robust test suite, given that they need their solution to function under pretty much any scenario. Likely also not true for recent orgs that have been following good DevOps and Quality Assurance principles from the start, given that Salesforce and third-party tooling now makes this relatively straightforward to achieve. In these cases, the two new parameters for the @IsTest annotation allow tight coupling of tests to classes/deployments. Note that these only apply when the test level for the deployment is RunRelevantTests:

  • @IsTest(critical=true)

    I really like this one. If you've ever built an app that works with real money, you'll know that there are areas that must not fail or losses will be incurred. Executing tests for key areas, regardless of what changed, is a nice new feature.

  • @IsTest(testFor='<classes_and_triggers>')

    This is for the well-managed codebases. It allows you to guarantee that this test class will be executed if new/modified versions any of the identified dependencies are included in the payload. While this might feel like development overhead, my view is it's exactly what is needed in a robust test suite. Good development teams will likely hold this information elsewhere anyway, and apply it via RunSpecifiedTests, so I can see in many cases it will short cut that process.

How Does Salesforce Choose?


This is the $64,000 question, but right now we just don't know. The docs say :
the RunRelevantTests test engine analyzes the deployment payload and automatically runs a subset of tests based on that analysis.
which tells us what happens, but no detail about the analysis carried out. This isn't unusual in my experience, and by the time this feature goes GA I'd expect significantly more information to be available. That said I can't just sit idly and wait, so I've been doing some digging using a sample codebase with the following actors:
  • OpportunityUtils
    The protagonist in my little drama is a class named  This implements an interface (OpportunityUtilsIF) with a single method, getBigDeals(), which receives a collection of opportunities and returns a new collection containing just those opportunities with a value greater than or equal to 250,000.

    There is a dedicated test class (OpportunityUtilsTest) which directly instantiates the class and executes a zero/one test. 
  • OpportunityEOD
    This contains a single method (EODProcessing) that extracts all opportunities created today, creates an instance of OpportunityUtils, extracts just the big deals, appends ' - BIG DEAL' to their name and updates them.

    There is a dedicated test class (OpportunityEODTest) that inserts test opportunities of varying values, instantiates OpportunityEOD and executes the EODProcessing method, then extracts all opportunities from the database that are greater than or equal to 250,000 and asserts each name contains ' - BIG DEAL'.

  • OpportunityWrapLevel1
    This contains a single method (EODProcessingWrapLevel1) that instantiates the OpportunityEOD class and executes the EODProcessing method.

    There is a dedicated test class (OpportunityWrapLevel1) that inserts test opportunities of varying levels, instantiates OpportunityEODWrapLevel1, executes the EODProcessingWrapLevel1 method, then extracts all opportunities from the database that are greater than or equal to 250,000 and asserts each name contains ' - BIG DEAL'.

  • OpportunityEODInjection
    This ups the ante somewhat, is it contains a replica of the EODProcessing() method, but rather than directly instantiating OpportunityUtils it is passed a parameter (implementing the OpportunityUtilsIF interface. There is a dedicated test class (OpportunityEODInjectionTest) that delegates to a test factory to dynamically create an instance of OpportunityUtils based on the class name - at no point is OpportunityUtils directly referred to. The test mirrors the other EOD tests, inserting opportunities, carrying out the EOD processing and verifying that ' - BIG DEAL' is appended where appropriate.

After deploying these to a Spring '26 pre-release developer edition, I then changed the OpportunityUtils code to consider opportunities with a value of 300,000 and over as big deals, then tried to deploy it using the new -l RunRelevantTests Salesforce CLI option. There were unit test failures, but which ones?
  • OpportunityUtilsTest
    This test class was chosen, and the test for a single big deal failed. All as expected.

  • OpportunityEODTest
    This test class was chosen, and the EOD processing test failed. All as expected.

  • OpportunityWrapLevel1Test
    This test class was not chosen. I was surprised at this, as there is a dependency chain leading to OpportunityUtils.
    In case the selection of the OpportunityEODTest skewed the results, I removed the tests from that class and re-ran from the start. It still wasn't chosen, which suggests to me that currently only tests for classes with direct dependencies on the changed Apex will be chosen. 

  • OpportunityEODInjectionTest
    This test class was not chosen. This does not surprise me at all, as it would be really hard to pick up dynamically instantiated instances. The example I've given is straightforward, but the name could be generated by combining strings, through a lookup collection, or even configuration, so the only way to tell is to actually run the real code. I think this is a scenario where it would be up to the developer to ensure that this code was tested when the dependencies changed via the @IsTest(testFor='...') annotation.

I don't think this is too bad at all. I'd have liked the tests from the dependency chain to be picked up too, but I can see that is a bit of a balancing act. If every class that can possibly reach the changed code is executed, you could end up executing all of your tests every time. Taking the opposing view, if classes that could be impacted by a change aren't tested, what does that mean for our confidence in the deployed code? In this case I'm assuming good intentions, given this is in beta, and expecting this to be tightened up as the functionality makes its way towards general availability.

Conclusion


I like this new feature, but it requires careful consideration before relying on it for production (which, of course, you can't as it's in beta!). Personally I like to execute all tests whenever I deploy, but that isn't always feasible, especially if there is a large set of tests that have to execute serially and a high frequency release cadence. In that scenario I'd likely use the @IsTest(testFor='...') annotation approach to retain tight control. If, however, I was working in a mature org that showed clear signs of the big ball of mud anti-pattern, I'd happily leave it up to Salesforce.

Oh, and the UI hasn't quite caught up with this new functionality, as the deployment status always says that tests weren't required even if some of them failed:


so if you want to know which tests were actually picked, you need to use the Salesforce CLI with the --json flag and parse the output.

If you are interested in learning more about Apex testing, check out my in-progress book Software Testing on the Salesforce Platform.

More Information