SF
sf-e2e-testing
>-
Install
mkdir -p .claude/skills/sf-e2e-testing && curl -L -o skill.zip "https://agentskills.codes/api/skills/download/17159" && unzip -o skill.zip -d .claude/skills/sf-e2e-testing && rm skill.zipInstalls to .claude/skills/sf-e2e-testing
Activation
This is the description your AI agent reads to decide when to run this skill — the better it matches your request, the more reliably it fires.
Use when writing Salesforce Apex end-to-end integration tests, deployment verification, or bulk scenario validation. Do NOT use for unit tests or TDD.150 chars✓ has a “when” trigger
About this skill
Salesforce E2E Testing
Comprehensive testing patterns for Salesforce applications including Apex integration tests, LWC component tests, and deployment verification.
Reference: @../_reference/TESTING_STANDARDS.md
When to Use
- When writing integration tests that exercise complete business workflows end-to-end
- When verifying full automation chains including triggers, flows, and queueable jobs
- When setting up pre-deployment verification test suites for a sandbox or production org
- When testing bulk scenarios with 200+ records across the full trigger and automation stack
- When building LWC integration tests with Jest that mock Apex wire adapters and calls
Apex Integration Tests
@IsTest
private class OpportunityWorkflowTest {
@TestSetup
static void setup() {
Account acc = new Account(Name = 'E2E Test Account');
insert acc;
Opportunity opp = new Opportunity(
AccountId = acc.Id,
Name = 'E2E Test Opp',
StageName = 'Prospecting',
CloseDate = Date.today().addDays(30),
Amount = 50000
);
insert opp;
}
@IsTest
static void shouldProgressThroughFullSalesCycle() {
Opportunity opp = [SELECT Id, StageName FROM Opportunity LIMIT 1];
Test.startTest();
opp.StageName = 'Qualification';
update opp;
opp.StageName = 'Proposal/Price Quote';
update opp;
opp.StageName = 'Closed Won';
update opp;
Test.stopTest();
Opportunity result = [SELECT StageName, IsClosed, IsWon FROM Opportunity WHERE Id = :opp.Id];
Assert.areEqual('Closed Won', result.StageName);
Assert.isTrue(result.IsClosed, 'Should be closed');
Assert.isTrue(result.IsWon, 'Should be won');
// Verify downstream automation fired
List<Task> followUpTasks = [SELECT Id FROM Task WHERE WhatId = :opp.Id];
Assert.isTrue(!followUpTasks.isEmpty(), 'Should have created follow-up tasks');
}
@IsTest
static void shouldHandleBulkStageChanges() {
List<Opportunity> opps = new List<Opportunity>();
Account acc = [SELECT Id FROM Account LIMIT 1];
for (Integer i = 0; i < 200; i++) {
opps.add(new Opportunity(
AccountId = acc.Id,
Name = 'Bulk Opp ' + i,
StageName = 'Prospecting',
CloseDate = Date.today().addDays(30)
));
}
insert opps;
Test.startTest();
for (Opportunity opp : opps) {
opp.StageName = 'Closed Won';
}
update opps;
Test.stopTest();
Integer closedCount = [SELECT COUNT() FROM Opportunity WHERE StageName = 'Closed Won'];
Assert.isTrue(closedCount >= 200, 'All opportunities should be closed');
}
}
LWC Integration Tests
import { createElement } from 'lwc';
import OpportunityPipeline from 'c/opportunityPipeline';
import getOpportunities from '@salesforce/apex/OpportunityController.getOpportunities';
jest.mock('@salesforce/apex/OpportunityController.getOpportunities', () => ({
default: jest.fn()
}), { virtual: true });
const MOCK_OPPS = [
{ Id: '006xx0001', Name: 'Opp 1', StageName: 'Prospecting', Amount: 10000 },
{ Id: '006xx0002', Name: 'Opp 2', StageName: 'Closed Won', Amount: 50000 },
];
describe('c-opportunity-pipeline (integration)', () => {
afterEach(() => { while (document.body.firstChild) document.body.removeChild(document.body.firstChild); });
it('renders pipeline with correct stage grouping', async () => {
getOpportunities.mockResolvedValue(MOCK_OPPS);
const element = createElement('c-opportunity-pipeline', { is: OpportunityPipeline });
document.body.appendChild(element);
await Promise.resolve();
await Promise.resolve();
const stages = element.shadowRoot.querySelectorAll('.stage-column');
expect(stages.length).toBeGreaterThan(0);
});
});
Deployment Verification
# Full E2E deployment verification
sf project deploy validate --test-level RunLocalTests --target-org staging
sf project deploy start --test-level RunLocalTests --target-org staging
sf apex run test --test-level RunLocalTests --result-format human --target-org staging
Flow Integration Testing
@IsTest
private class CaseEscalationFlowTest {
@IsTest
static void shouldEscalateHighPriorityCases() {
Account acc = new Account(Name = 'Flow E2E Account');
insert acc;
Case c = new Case(
AccountId = acc.Id,
Subject = 'Urgent Issue',
Priority = 'High',
Status = 'New'
);
insert c;
Test.startTest();
c.Status = 'Escalated';
update c;
Test.stopTest();
List<Task> tasks = [SELECT Subject, Priority FROM Task WHERE WhatId = :c.Id AND Subject LIKE '%Escalation%'];
Assert.isTrue(!tasks.isEmpty(), 'Flow should have created escalation task');
Assert.areEqual('High', tasks[0].Priority, 'Task priority should match case priority');
}
}
Platform Event Testing
@IsTest
private class OrderEventTest {
@IsTest
static void shouldPublishEventOnOrderCompletion() {
Order_Complete__e event = new Order_Complete__e(
Order_Id__c = 'ORD-001',
Total_Amount__c = 5000.00
);
Test.startTest();
Database.SaveResult sr = EventBus.publish(event);
Test.stopTest();
Assert.isTrue(sr.isSuccess(), 'Event should publish successfully');
}
@IsTest
static void shouldProcessEventInSubscriber() {
Order_Complete__e event = new Order_Complete__e(
Order_Id__c = 'ORD-002',
Total_Amount__c = 10000.00
);
Test.startTest();
EventBus.publish(event);
Test.getEventBus().deliver(); // Force trigger subscriber to execute
Test.stopTest();
List<Fulfillment__c> fulfillments = [
SELECT Order_Id__c FROM Fulfillment__c WHERE Order_Id__c = 'ORD-002'
];
Assert.areEqual(1, fulfillments.size(), 'Subscriber should have created fulfillment');
}
}
Async Job Verification
@IsTest
private class AsyncWorkflowTest {
@IsTest
static void shouldProcessQueueableChain() {
Account acc = new Account(Name = 'Async E2E');
insert acc;
Test.startTest();
System.enqueueJob(new AccountEnrichmentJob(acc.Id));
Test.stopTest();
Account result = [SELECT Description, Industry FROM Account WHERE Id = :acc.Id];
Assert.isNotNull(result.Description, 'Enrichment job should populate description');
}
}
Multi-User Testing
@IsTest
private class PermissionE2ETest {
@IsTest
static void shouldRestrictAccessForStandardUser() {
// Note: Profile names are locale-dependent. For non-English orgs,
// query by Profile.UserType or use a custom permission set.
Profile stdProfile = [SELECT Id FROM Profile WHERE Name = 'Standard User'];
User testUser = new User(
Alias = 'stdu', Email = '[email protected]',
EmailEncodingKey = 'UTF-8', LastName = 'StdUser',
LanguageLocaleKey = 'en_US', LocaleSidKey = 'en_US',
ProfileId = stdProfile.Id, TimeZoneSidKey = 'America/Los_Angeles',
UserName = 'stduser' + DateTime.now().getTime() + '@test.com'
);
insert testUser;
System.runAs(testUser) {
Test.startTest();
try {
ConfidentialRecord__c rec = new ConfidentialRecord__c(Name = 'Secret');
insert rec;
Assert.fail('Should have thrown insufficient access exception');
} catch (DmlException e) {
Assert.isTrue(e.getMessage().contains('INSUFFICIENT_ACCESS') ||
e.getMessage().contains('access'),
'Should get access denied: ' + e.getMessage());
}
Test.stopTest();
}
}
}
Performance Assertions
@IsTest
private class PerformanceE2ETest {
@IsTest
static void shouldStayWithinGovernorLimits() {
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < 200; i++) {
accounts.add(new Account(Name = 'Perf Test ' + i));
}
insert accounts;
Test.startTest();
Integer queriesBefore = Limits.getQueries();
AccountService.processAccounts(new Map<Id, Account>(accounts).keySet());
Integer queriesUsed = Limits.getQueries() - queriesBefore;
Test.stopTest();
Assert.isTrue(queriesUsed <= 5,
'Should use <= 5 SOQL queries for 200 records, used: ' + queriesUsed);
}
}
Anti-Patterns
| Anti-Pattern | Problem | Fix |
|---|---|---|
| Testing with 1 record only | Misses bulkification bugs | Test with 200+ records |
| No Test.startTest/stopTest | Governor limits from setup count against test | Wrap test logic in startTest/stopTest |
| Using SeeAllData=true | Tests depend on org data, fail unpredictably | Use @TestSetup with test-specific data |
| Hardcoded IDs | Break across orgs and sandboxes | Query for IDs or use TestDataFactory |
| No downstream verification | Trigger fires but automation not verified | Assert on records created by automation |
| Ignoring async results | Queueable/Batch results not checked | Test.stopTest forces execution, then assert |
Best Practices
- Use @TestSetup for shared test data across methods
- Test complete workflows, not just individual methods
- Include bulk tests (200+ records) for all trigger-based automation
- Test both happy path and error scenarios
- Verify downstream automation by checking created records
- Use Test.startTest()/Test.stopTest() to isolate governor limit counti
Content truncated.