Note: This release is in preview. Features described here don’t become generally available until the latest general availability date that Salesforce announces for this release. Before then, and where features are noted as beta, pilot, or developer preview, we can’t guarantee general availability within any particular time frame or at all. Make your purchase decisions only on the basis of generally available products and features.

Apex Integration Tests (Developer Preview)

Use Apex integration tests to write end-to-end tests that exercise real interactions between your Salesforce org and external services, including Agentforce and Data 360, as well as HTTP endpoints. Unlike standard Apex unit tests, integration tests relax callout restrictions and transaction rollback semantics, so your tests can make real service calls, commit data mid-transaction, and make assertions on expected outcomes. As a developer preview feature, integration tests are available only in scratch orgs. You can’t run them in production orgs or during metadata deployments.

The Apex Integration Tests feature is available as a developer preview in scratch orgs in Summer ’26 (API version 67.0) and later. The feature isn’t generally available unless or until Salesforce announces its general availability in documentation or in press releases or public statements. All commands, parameters, and other features are subject to change or deprecation at any time, with or without notice. Don't implement functionality developed with these commands or tools in your production package.

Note

How Integration Tests Compare to Unit Tests

Integration tests complement, rather than replace, existing Apex unit tests. Unit tests remain the right choice for testing isolated business logic, triggers, and flows. Integration tests are for scenarios that require real service interactions across transaction boundaries. Use integration tests used in these ways.

  • Test HTTP callouts and verify their side effects on org data.
  • Run real SOQL queries against Data 360 data model objects (DMOs) without stub mocks.
  • Test behavior that depends on committed data, such as field history tracking across updates.

This table shows the key differences between standard Apex unit tests (@IsTest) and integration tests (@IntegrationTest).

Unit Tests Integration Tests
HTTP endpoints, Agentforce, Data 360, and other external services Blocked; require mocks Allowed
Test setup Use @TestSetup. Changes to setup data are rolled back after each test method execution. Use @BeforeTest. Changes to setup data persists between test method executions. Test data isn’t cleaned up.
Transaction behavior Auto-rollback Data committed. Use @TearDown for cleanup, which runs after every test method execution.
Data visibility Test data silo by default SeeAllData=true by default
Code coverage Counts toward deployment code coverage requirements Doesn’t count toward deployment code coverage requirements
Metadata deployments Included. Unit tests are executed in metadata deployment. Excluded
Execution Synchronous or asynchronous Asynchronous only. Allowed only 1 concurrent execution per org.
Maximum runtime Standard limits 10 minutes

Enable Integration Tests in a Scratch Org Definition File

Before you can run Apex integration tests, you must have a scratch org with the ApexIntegrationTests feature enabled.

To enable the feature, add ApexIntegrationTests to the features array in the config/project-scratch-def.json of your Salesforce DX project.

1{
2  "orgName": "Company",
3  "edition": "Developer",
4  "features": ["ApexIntegrationTests"]
5}

To create a scratch org with this definition, use the org create scratch Salesforce CLI command.

For more information about scratch org development, see Scratch Orgs in the Salesforce DX Developer Guide.

Create an Integration Test Class

To create an Apex integration test class, use these annotations and methods.

  • @IntegrationTest: This annotation marks a class and its test methods as integration tests. A class annotated with @IntegrationTest can only contain integration test methods. You can’t annotate a class with both @IntegrationTest and @IsTest annotations. You can’t call integration test methods from non-test contexts or from @IsTest test methods. However, integration tests can call methods in @IsTest utility classes such as shared test data factories.
  • @BeforeClass: This annotation marks one static void method that runs once before all integration test methods in a class. Only one @BeforeClass method is allowed in a class. Use this method to create test data shared by the test methods. The platform automatically commits the transaction when the method completes successfully. If the method fails, all integration test methods in the class fail. Static variables set in @BeforeClass aren’t available to subsequent test methods. Changed test data persists between test method executions and isn’t rolled back. You can’t call IntegrationTest.commitTestOnly() from a @BeforeClass method.
  • @TearDown: This annotation marks a static cleanup method that runs after each test method completes, regardless of whether the test passes or fails. Only one @TearDown method is allowed in a class. Use this method to clean up data created by the test method. Don’t delete data created by @BeforeClass, because subsequent test methods can’t access it. The teardown transaction auto-commits at the end of the execution. You can’t call IntegrationTest.commitTestOnly() from a @TearDown method. If the @TearDown method fails due to an uncaught exception, the prior integration test is marked as a failure.
  • IntegrationTest.commitTestOnly(): This method commits data to the database mid-transaction so that it’s visible to other threads and services. It resets the uncommitted work checkpoint, so subsequent callouts don’t fail. The method also resets tracking for mixed DML operations, so you can perform setup sObject and non-setup sObject DML in separate commit windows. The method can only be called from an @IntegrationTest method. Calling it from outside an integration test context results in a runtime error: Test.commitTestOnly() can only be called from integration testMethods.

This example uses @BeforeClass to create an account and contact that are available to two integration test methods. One method calls an external service through a Named Credential and stores the returned ID on the account. The other invokes an Agentforce action that summarizes the shared account data. The @TearDown method clears the field updated by the external service but preserves the shared records so that each test method can access them.

1@IntegrationTest
2public class AccountServiceIntegrationTest {
3
4    @BeforeClass
5    static void setup() {
6        Account a = new Account(Name = 'IntegrationAccount', Industry = 'Technology');
7        insert a;
8
9        Contact c = new Contact(FirstName = 'Shared', LastName = 'Contact', AccountId = a.Id);
10        insert c;
11        // Platform auto-commits — do NOT call commitTestOnly() here
12    }
13
14    @IntegrationTest
15    public static void testExternalServiceSeesAccount() {
16        Account a = [SELECT Id FROM Account WHERE Name = 'IntegrationAccount'];
17
18        Http h = new Http();
19        HttpRequest req = new HttpRequest();
20        req.setEndpoint('callout:MyService/accounts/' + a.Id);
21        req.setMethod('GET');
22        HttpResponse res = h.send(req);
23        Assert.areEqual(200, res.getStatusCode());
24
25        a.ExternalId__c = res.getBody();
26        update a;
27    }
28
29    @IntegrationTest
30    public static void testAgentSummarizesAccount() {
31        Invocable.Action action = Invocable.Action.createCustomAction(
32            'generateAiAgentResponse', 'Copilot_for_Salesforce@1.1.0');
33        action.setInvocationParameter('userMessage', 'Summarize IntegrationAccount');
34        List<Invocable.Action.Result> results = action.invoke();
35        String response = results[0].getOutputParameters().get('agentResponse').toString();
36        Assert.isTrue(response.contains('Technology'));
37    }
38
39    @TearDown
40    public static void tearDown() {
41
42        // Clear Account External ID
43        Account a = [SELECT Id FROM Account WHERE Name = 'IntegrationAccount'];
44        a.ExternalId__c = null;
45        update a;
46
47    }
48}

Test an Agent Action

An integration test can invoke an agent with an invocable action and assert on the response. The commitTestOnly() call before the agent invocation is critical. Without it, the record you created exists only in your pending transaction and is invisible to the Agentforce planner service.

1@IntegrationTest
2public with sharing class AgentforceIntegrationTest {
3
4    @IntegrationTest
5    public static void testAgentSummarizesAccount() {
6
7        Account a = new Account(Name = 'AgentDemoAccount', AnnualRevenue = 1000000);
8        insert as user a;
9        IntegrationTest.commitTestOnly();
10
11        Invocable.Action action = Invocable.Action.createCustomAction(
12            'generateAiAgentResponse',
13            'Demo_Action'
14        );
15        action.setInvocationParameter('userMessage', 'Summarize my Account ' + a.Id);
16
17        List<Invocable.Action.Result> results = action.invoke();
18        String response = results[0].getOutputParameters().get('agentResponse').toString();
19
20        Assert.isNotNull(response, 'Agent should return a response');
21        Assert.isTrue(response.contains('AgentDemoAccount'),
22            'Response should reference the account name');
23    }
24
25    @TearDown
26    public static void tearDown() {
27        delete as user [SELECT Id FROM Account WHERE Name = 'AgentDemoAccount'];
28    }
29}

This example uses the AdvancedInputBindings agent script recipe. It tests the generation of a sales report for Q2 2026.

1@IntegrationTest
2public with sharing class AdvancedInputBindingsIntegrationTest {
3    @IntegrationTest
4    public static void testAgentUpdate() {
5        // Invoke Agentforce service (callout allowed in integration tests)
6        Invocable.Action action = Invocable.Action.createCustomAction(
7            'generateAiAgentResponse',
8            'AdvancedInputBindings'
9        );
10    
11        String prompt = 'Generate a sales report for Q2 2026';
12        action.setInvocationParameter('userMessage', prompt);
13        List<Invocable.Action.Result> results = action.invoke();
14
15        String response = results[0].getOutputParameters().get('agentResponse').toString();
16        Schema.DescribeSObjectResult r = ASR_Report_Log__c.sObjectType.getDescribe();
17        String keyPrefix = r.getKeyPrefix();
18        Assert.isTrue(response.contains(keyPrefix), 'Expected agent response to create an sObject');
19    
20        // query and assert the report was created
21        List<ASR_Report_Log__c> logs = [
22            SELECT Id, Report_Type__c, User_ID__c, Format__c
23            FROM ASR_Report_Log__c
24            WHERE Start_Date__c = 2026-04-01
25        ];
26        Assert.areEqual(1, logs.size(), 'Report log should be created');
27    }
28
29    @TearDown
30    public static void destroyTestRecords() {
31        List<ASR_Report_Log__c> testRecords = [
32            SELECT Id, Report_Type__c, User_ID__c, Format__c
33            FROM ASR_Report_Log__c
34            WHERE Start_Date__c = 2026-04-01
35        ];
36    
37        if (testRecords.size() > 0) {
38            delete testRecords;
39        }
40    }
41}

Test a Data 360 SOQL Query

Query Data 360 data model objects (DMOs) directly in integration tests without using SoqlStubProvider or Test.createSoqlStub. In standard unit tests, SOQL queries against DMO objects require stub mocks. Integration tests bypass this restriction for Data 360 entities.

1@IntegrationTest
2public with sharing class DataCloudQueryIntegrationTest {
3
4    @IntegrationTest
5    public static void testDMOQuery() {
6        List<SObject> rows = Database.query('SELECT Id FROM Account__dlm WITH USER_MODE LIMIT 1');
7        Assert.areEqual(1, rows.size(), 'Data 360 query should return 1 row');
8    }
9}

Test as a Specific User

Use System.runAs() to run integration test logic as a specific user, including setting up the necessary permission set assignments.

1@IntegrationTest
2public with sharing class RunAsIntegrationTest {
3
4    @IntegrationTest
5    public static void testAgentResponseAsStandardUser() {
6        User u = [SELECT Id FROM User WHERE Alias = 'tstUsr' LIMIT 1];
7
8        System.runAs(u) {
9            Account a = new Account(Name = 'AgentDemoAccount');
10            insert as user a;
11            IntegrationTest.commitTestOnly();
12
13            Invocable.Action action = Invocable.Action.createCustomAction(
14                'generateAiAgentResponse',
15                'Demo_Action'
16            );
17            String prompt = 'Summarize my Account ' + a.Id;
18            action.setInvocationParameter('userMessage', prompt);
19            List<Invocable.Action.Result> results = action.invoke();
20
21            String response = results[0].getOutputParameters()
22                .get('agentResponse').toString();
23            Assert.isTrue(response.contains('AgentDemoAccount'));
24        }
25    }
26
27    @TearDown
28    public static void tearDown() {
29        delete as user [SELECT Id FROM Account WHERE Name = 'AgentDemoAccount'];
30    }
31}

Test External HTTP Callouts

Integration tests can make real HTTP callouts to external endpoints, including endpoints configured with Named Credentials and External Services, without a mock. If you register a mock with Test.setMock(), the mock takes precedence over real callouts. Remove registered mocks to test real endpoints.

1@IntegrationTest
2public class ExternalServiceIntegrationTest {
3    @BeforeClass
4    static void setup() {
5        Account a = new Account(Name = 'PaymentTestAccount');
6        insert a;
7        // Platform auto-commits — no commitTestOnly() needed in @BeforeClass
8    }
9
10    @IntegrationTest
11    public static void testExternalServiceCallout() {
12        Account a = [SELECT Id FROM Account WHERE Name = 'PaymentTestAccount'];
13
14        // Real HTTP callout — no mock required
15        Http h = new Http();
16        HttpRequest req = new HttpRequest();
17        req.setEndpoint('callout:PaymentGateway/verify/' + a.Id);
18        req.setMethod('GET');
19        HttpResponse res = h.send(req);
20
21        Assert.areEqual(200, res.getStatusCode());
22        Assert.isTrue(res.getBody().contains('verified'));
23    }
24
25    @TearDown
26    public static void tearDown() {
27        delete [SELECT Id FROM Account WHERE Name = 'PaymentTestAccount'];
28    }
29}

Use Test.startTest() and Test.stopTest() with Asynchronous Apex

Asynchronous Apex operations enqueued in integration tests can be dequeued synchronously using the Test.startTest() and Test.stopTest() pattern.

1@IntegrationTest
2public with sharing class AsyncIntegrationTest {
3
4    public with sharing class AccountProcessorQueueable implements Queueable {
5        private Id accountId;
6
7        public AccountProcessorQueueable(Id accountId) {
8            this.accountId = accountId;
9        }
10
11        public void execute(QueueableContext context) {
12            Account a = [SELECT Id, Name FROM Account WHERE Id = :accountId WITH USER_MODE];
13            a.Industry = 'Technology';
14            update as user a;
15        }
16    }
17
18    @IntegrationTest
19    public static void testQueueableExecution() {
20        Account a = new Account(Name = 'Async Test Account');
21        insert as user a;
22        IntegrationTest.commitTestOnly();
23
24        Test.startTest();
25        Id jobId = System.enqueueJob(new AccountProcessorQueueable(a.Id));
26        Test.stopTest();
27
28        Account updated = [SELECT Industry FROM Account WHERE Id = :a.Id WITH USER_MODE];
29        Assert.areEqual('Technology', updated.Industry,
30            'Queueable should have updated the industry');
31
32        AsyncApexJob job = [
33            SELECT Status, NumberOfErrors
34            FROM AsyncApexJob WHERE Id = :jobId
35            WITH USER_MODE
36        ];
37        Assert.areEqual('Completed', job.Status);
38        Assert.areEqual(0, job.NumberOfErrors);
39    }   
40    @TearDown
41    public static void tearDown() {
42        delete as user [SELECT Id FROM Account WHERE Name = 'Async Test Account'];
43    }
44}

Create Multiple Commits Within a Transaction

Use IntegrationTest.commitTestOnly() multiple times with a test method to commit data progressively, which is useful when later operations depend on previously committed records.

1@IntegrationTest
2public with sharing class MultiCommitIntegrationTest {
3
4    @IntegrationTest
5    public static void testMultipleCommits() {
6        Account a = new Account(Name = 'Commit Test Account');
7        insert as user a;
8        IntegrationTest.commitTestOnly();
9
10        a.Website = 'example.com';
11        update as user a;
12        IntegrationTest.commitTestOnly();
13
14        Contact c = new Contact(
15            FirstName = 'Test',
16            LastName = 'Contact',
17            AccountId = a.Id
18        );
19        insert as user c;
20        IntegrationTest.commitTestOnly();
21
22        Assert.areEqual(1,
23            [SELECT COUNT() FROM Account WHERE Name = 'Commit Test Account' WITH USER_MODE]);
24        Assert.areEqual(1,
25            [SELECT COUNT() FROM Contact WHERE AccountId = :a.Id WITH USER_MODE]);
26    }
27
28    @TearDown
29    public static void tearDown() {
30        delete as user [SELECT Id FROM Contact WHERE LastName = 'Contact'];
31        delete as user [SELECT Id FROM Account WHERE Name = 'Commit Test Account'];
32    }
33}

Test Field History Tracking

Standard unit tests roll back all data, so field history records are never created. Integration tests commit data, which makes it possible to assert on field history.

1@IntegrationTest
2public with sharing class FieldHistoryIntegrationTest {
3
4    @IntegrationTest
5    public static void testFieldHistoryIsTracked() {
6        Account a = new Account(Name = 'HistoryTestAccount', Website = 'example.com');
7        insert as user a;
8        IntegrationTest.commitTestOnly();
9
10        a.Website = 'salesforce.com';
11        update as user a;
12        IntegrationTest.commitTestOnly();
13
14        List<AccountHistory> history = [
15            SELECT Id FROM AccountHistory WHERE AccountId = :a.Id
16            WITH USER_MODE
17        ];
18        Assert.isTrue(history.size() > 0, 'Expected field history to be tracked');
19    }
20
21    @TearDown
22    public static void tearDown() {
23        delete as user [SELECT Id FROM Account WHERE Name = 'HistoryTestAccount'];
24    }
25}

Run an Integration Test

The @BeforeClass and @TearDown methods commit their transactions automatically when they complete successfully. Data created during the test persists unless explicitly deleted in the teardown.

Integration tests are considered a distinct “IntegrationTest” category that’s separate from Apex unit tests, flow tests, and Agentforce tests. You can discover and run them asynchronously by using the Salesforce CLI or the Tooling API. You can run only 1 integration test class per org at a time.

Synchronous testing isn’t available for integration tests. Integration tests are excluded from RunAllTests during metadata deployments.

Integration Testing Best Practices

  • Always implement @TearDown methods. Because integration tests commit data to the database, test data that is not torn down after the test run can impact subsequent runs. Delete records in the correct order in @TearDown. When you create related records, delete child records before parent records to avoid foreign key constraint issues.
  • Use commitTestOnly() before callouts to services that must access data from your test transaction. These services operate on separate threads that can’t see uncommitted data in your test’s pending transaction.
  • Keep integration tests focused on integration concerns. Test isolated business logic with standard @IsTest unit tests. Reserve integration tests for scenarios that require real service interactions.
  • Be aware of concurrency limits. Only one integration test can run per org at a time. Design your test suite accordingly, and prefer shorter, more focused tests over long monolithic ones. Integration tests have access to and can commit data to your org (seeAllData=true). Be mindful of other requests that may be trying to write to the same data rows in your org if you use existing data from your org.
  • Create setup data and avoid reusing org data to avoid row lock contention.
  • Plan for the 10-minute runtime limit. If a test involves multiple slow service calls, consider splitting it into separate test methods.
  • When you register a HttpCalloutMock, the mock takes precedence over a real HTTP callout.

Integration Testing Limitations and Considerations

  • During developer preview, integration testing can only run in scratch orgs. Integration tests aren’t available in production orgs, sandboxes, or during metadata deployments.
  • Integration tests don’t count toward code coverage requirements.
  • There is no @TestVisible access. Unlike @IsTest classes, @IntegrationTest classes can’t access private members annotated with @TestVisible in the test classes.
  • When using the @BeforeClass annotation, be aware that changed test data persists between test method executions and isn’t rolled back at the end of the test.
  • The@BeforeClass method runs only once per test class, but the @TearDown method transaction runs after each test method. Deleting @BeforeClass data from the @TearDown method leaves subsequent test methods without the required test data.
  • Only asynchronous execution is supported. Only one test runs per org at a time.
  • Asynchronous Apex governor limits (for SOQL, DML, CPU and heap limits) apply to integration testing. See Execution Governors and Limits.
  • Integration tests share the same 24-hour limit on asynchronous Apex test runs with flow tests and unit tests.