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)
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.