@salesforce/afv-skills 1.5.0 → 1.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -415
- package/package.json +5 -3
- package/skills/building-ui-bundle-app/SKILL.md +325 -0
- package/skills/building-ui-bundle-frontend/SKILL.md +122 -0
- package/skills/{building-webapp-react-components → building-ui-bundle-frontend}/implementation/component.md +1 -1
- package/skills/creating-b2b-commerce-store/SKILL.md +169 -0
- package/skills/creating-b2b-commerce-store/references/store-vs-storefront.md +169 -0
- package/skills/deploying-ui-bundle/SKILL.md +77 -0
- package/skills/generating-apex/CREDITS.md +30 -0
- package/skills/generating-apex/SKILL.md +399 -0
- package/skills/generating-apex/assets/abstract.cls +132 -0
- package/skills/generating-apex/assets/batch.cls +125 -0
- package/skills/generating-apex/assets/domain.cls +102 -0
- package/skills/generating-apex/assets/dto.cls +108 -0
- package/skills/generating-apex/assets/exception.cls +51 -0
- package/skills/generating-apex/assets/interface.cls +25 -0
- package/skills/generating-apex/assets/invocable.cls +115 -0
- package/skills/generating-apex/assets/queueable.cls +92 -0
- package/skills/generating-apex/assets/rest-resource.cls +300 -0
- package/skills/generating-apex/assets/schedulable.cls +75 -0
- package/skills/generating-apex/assets/selector.cls +92 -0
- package/skills/generating-apex/assets/service.cls +69 -0
- package/skills/generating-apex/assets/trigger.cls +45 -0
- package/skills/generating-apex/assets/utility.cls +97 -0
- package/skills/generating-apex/references/AccountDeduplicationBatch.cls +148 -0
- package/skills/generating-apex/references/AccountSelector.cls +193 -0
- package/skills/generating-apex/references/AccountService.cls +201 -0
- package/skills/generating-apex-test/CREDITS.md +30 -0
- package/skills/generating-apex-test/SKILL.md +199 -0
- package/skills/generating-apex-test/assets/test-class-template.cls +93 -0
- package/skills/generating-apex-test/assets/test-data-factory-template.cls +111 -0
- package/skills/generating-apex-test/references/assertion-patterns.md +108 -0
- package/skills/generating-apex-test/references/async-testing.md +193 -0
- package/skills/generating-apex-test/references/mocking-patterns.md +220 -0
- package/skills/generating-apex-test/references/test-data-factory.md +75 -0
- package/skills/generating-experience-react-site/SKILL.md +20 -9
- package/skills/generating-experience-react-site/docs/configure-metadata-digital-experience.md +1 -1
- package/skills/generating-flexipage/SKILL.md +58 -60
- package/skills/generating-ui-bundle-features/SKILL.md +45 -0
- package/skills/generating-ui-bundle-metadata/SKILL.md +106 -0
- package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/SKILL.md +5 -5
- package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/constraints.md +2 -2
- package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/examples.md +1 -1
- package/skills/{implementing-webapp-file-upload → implementing-ui-bundle-file-upload}/SKILL.md +11 -11
- package/skills/searching-media/SKILL.md +342 -0
- package/skills/{using-webapp-salesforce-data → using-ui-bundle-salesforce-data}/SKILL.md +52 -25
- package/skills/using-ui-bundle-salesforce-data/references/mutation-query-generation.md +140 -0
- package/skills/using-ui-bundle-salesforce-data/references/query-testing.md +78 -0
- package/skills/using-ui-bundle-salesforce-data/references/read-query-generation.md +307 -0
- package/skills/using-ui-bundle-salesforce-data/references/schema-introspection.md +53 -0
- package/skills/using-ui-bundle-salesforce-data/references/ui-bundle-integration.md +221 -0
- package/skills/{using-webapp-salesforce-data → using-ui-bundle-salesforce-data/scripts}/graphql-search.sh +75 -23
- package/skills/building-webapp-data-visualization/SKILL.md +0 -72
- package/skills/building-webapp-data-visualization/implementation/bar-line-chart.md +0 -316
- package/skills/building-webapp-data-visualization/implementation/dashboard-layout.md +0 -189
- package/skills/building-webapp-data-visualization/implementation/donut-chart.md +0 -181
- package/skills/building-webapp-data-visualization/implementation/stat-card.md +0 -150
- package/skills/building-webapp-react-components/SKILL.md +0 -96
- package/skills/configuring-webapp-csp-trusted-sites/SKILL.md +0 -90
- package/skills/configuring-webapp-metadata/SKILL.md +0 -158
- package/skills/creating-webapp/SKILL.md +0 -140
- package/skills/deploying-webapp-to-salesforce/SKILL.md +0 -226
- package/skills/installing-webapp-features/SKILL.md +0 -210
- /package/skills/{building-webapp-react-components → building-ui-bundle-frontend}/implementation/header-footer.md +0 -0
- /package/skills/{building-webapp-react-components → building-ui-bundle-frontend}/implementation/page.md +0 -0
- /package/skills/{configuring-webapp-csp-trusted-sites/implementation/metadata-format.md → generating-ui-bundle-metadata/implementation/csp-metadata-format.md} +0 -0
- /package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/style-tokens.md +0 -0
- /package/skills/{managing-webapp-agentforce-conversation-client → implementing-ui-bundle-agentforce-conversation-client}/references/troubleshooting.md +0 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: generating-apex-test
|
|
3
|
+
description: Generate and validate Apex test classes with TestDataFactory patterns, bulk testing (251+ records), mocking strategies, assertion best practices, and disciplined test-fix loops. Use this skill when creating new Apex test classes, improving test coverage, debugging and fixing failing Apex tests, running test execution and coverage analysis, or implementing testing patterns for triggers, services, controllers, batch jobs, queueables, and integrations. Triggers on *Test.cls, *_Test.cls files, sf apex run test workflows, coverage reports, test-fix loops. Do NOT trigger for production Apex code (use generating-apex) or Jest/LWC tests.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Generating Apex Tests
|
|
7
|
+
|
|
8
|
+
Generate production-ready Apex test classes and run disciplined test-fix loops with coverage analysis.
|
|
9
|
+
|
|
10
|
+
## Core Principles
|
|
11
|
+
|
|
12
|
+
1. **One behavior per method** — each test method validates a single scenario. Separate positive, negative, and bulk tests. NEVER combine related-but-distinct inputs (e.g., null and empty) in one method — create `_NullInput_` and `_EmptyInput_` as separate test methods
|
|
13
|
+
2. **Bulkify tests** — test with 251+ records to cross the 200-record trigger batch boundary. **Batch Apex exception:** in test context only one `execute()` invocation runs, so set `batchSize >= testRecordCount`. See [references/async-testing.md](references/async-testing.md)
|
|
14
|
+
3. **Isolate test data** — every `@TestSetup` must delegate record creation to a `TestDataFactory` class. If none exists, create one first. Never build record lists inline in `@TestSetup`. Never rely on org data (`SeeAllData=false`) or hardcoded IDs. For duplicate rule handling, see [references/test-data-factory.md](references/test-data-factory.md)
|
|
15
|
+
4. **Assert meaningfully** — use exact expected values computed from test data setup. NEVER use range assertions or approximate counts when the value is deterministic. Always include failure messages. See [references/assertion-patterns.md](references/assertion-patterns.md)
|
|
16
|
+
5. **Use `Assert` class only** — `Assert.areEqual`, `Assert.isTrue`, `Assert.fail`, etc. Never use legacy `System.assert`, `System.assertEquals`, or `System.assertNotEquals`
|
|
17
|
+
6. **Mock external boundaries** — use `HttpCalloutMock` for callouts, `Test.setFixedSearchResults` for SOSL, DML mock classes for database isolation. Design for testability via constructor injection. See [references/mocking-patterns.md](references/mocking-patterns.md)
|
|
18
|
+
7. **Test negative paths** — validate error handling and exception scenarios, not just happy paths
|
|
19
|
+
8. **Wrap with start/stop** — pair `Test.startTest()` with `Test.stopTest()` to reset governor limits and force async execution
|
|
20
|
+
|
|
21
|
+
## Test.startTest() / Test.stopTest()
|
|
22
|
+
|
|
23
|
+
Always wrap the code under test in `Test.startTest()` / `Test.stopTest()`:
|
|
24
|
+
|
|
25
|
+
- Resets governor limits so the test measures only the code under test
|
|
26
|
+
- Executes async operations synchronously (queueables, batch, future methods)
|
|
27
|
+
- Fires scheduled jobs immediately
|
|
28
|
+
|
|
29
|
+
## Test Code Anti-Patterns
|
|
30
|
+
|
|
31
|
+
| Anti-Pattern | Fix |
|
|
32
|
+
|---|---|
|
|
33
|
+
| SOQL/DML inside loops | Query once before the loop; use `Map<Id, SObject>` for lookups |
|
|
34
|
+
| Magic numbers in assertions | Derive expected values from setup constants |
|
|
35
|
+
| God test class (>500 lines) | Split into multiple test classes by behavior area |
|
|
36
|
+
| Long test methods (>30 lines) | Extract Given/When/Then into helper methods |
|
|
37
|
+
| Generic `Exception` catch | Catch the specific expected type (e.g., `DmlException`) |
|
|
38
|
+
|
|
39
|
+
## Workflow
|
|
40
|
+
|
|
41
|
+
### Step 1 — Gather Context
|
|
42
|
+
|
|
43
|
+
Before generating or fixing tests, identify:
|
|
44
|
+
|
|
45
|
+
- the target production class(es) under test
|
|
46
|
+
- existing test classes, test data factories, and setup helpers
|
|
47
|
+
- desired test scope (single class, specific methods, suite, or local tests)
|
|
48
|
+
- coverage threshold (75% minimum for deploy, 90%+ recommended)
|
|
49
|
+
- org alias when running tests against an org
|
|
50
|
+
|
|
51
|
+
### Step 2 — Generate the Test Class
|
|
52
|
+
|
|
53
|
+
Apply the structure, naming conventions, and patterns from the asset templates and reference docs.
|
|
54
|
+
|
|
55
|
+
**MANDATORY — File Deliverables:** For every test class, create BOTH files:
|
|
56
|
+
1. `{ClassName}Test.cls` — the test class (use [assets/test-class-template.cls](assets/test-class-template.cls) as starting point)
|
|
57
|
+
2. `{ClassName}Test.cls-meta.xml` — the metadata file:
|
|
58
|
+
|
|
59
|
+
```xml
|
|
60
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
61
|
+
<ApexClass xmlns="http://soap.sforce.com/2006/04/metadata">
|
|
62
|
+
<apiVersion>66.0</apiVersion>
|
|
63
|
+
<status>Active</status>
|
|
64
|
+
</ApexClass>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
If no `TestDataFactory` exists in the project, create `TestDataFactory.cls` + `TestDataFactory.cls-meta.xml` using [assets/test-data-factory-template.cls](assets/test-data-factory-template.cls).
|
|
68
|
+
|
|
69
|
+
#### @TestSetup Example
|
|
70
|
+
|
|
71
|
+
```apex
|
|
72
|
+
@TestSetup
|
|
73
|
+
static void setupTestData() {
|
|
74
|
+
List<Account> accounts = TestDataFactory.createAccounts(251, true);
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
#### Test Method Structure
|
|
79
|
+
|
|
80
|
+
Use Given/When/Then:
|
|
81
|
+
|
|
82
|
+
```apex
|
|
83
|
+
@isTest
|
|
84
|
+
static void shouldUpdateStatus_WhenValidInput() {
|
|
85
|
+
// Given
|
|
86
|
+
List<Account> accounts = [SELECT Id FROM Account];
|
|
87
|
+
|
|
88
|
+
// When
|
|
89
|
+
Test.startTest();
|
|
90
|
+
MyService.processAccounts(accounts);
|
|
91
|
+
Test.stopTest();
|
|
92
|
+
|
|
93
|
+
// Then
|
|
94
|
+
List<Account> updated = [SELECT Id, Status__c FROM Account];
|
|
95
|
+
Assert.areEqual(251, updated.size(), 'All accounts should be processed');
|
|
96
|
+
}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
#### Negative Test — Exception Pattern
|
|
100
|
+
|
|
101
|
+
Use try/catch with `Assert.fail` to verify expected exceptions:
|
|
102
|
+
|
|
103
|
+
```apex
|
|
104
|
+
@isTest
|
|
105
|
+
static void shouldThrowException_WhenInvalidInput() {
|
|
106
|
+
// Given
|
|
107
|
+
List<Account> emptyList = new List<Account>();
|
|
108
|
+
|
|
109
|
+
// When/Then
|
|
110
|
+
Test.startTest();
|
|
111
|
+
try {
|
|
112
|
+
MyService.processAccounts(emptyList);
|
|
113
|
+
Assert.fail('Expected MyCustomException to be thrown');
|
|
114
|
+
} catch (MyCustomException e) {
|
|
115
|
+
Assert.isTrue(e.getMessage().contains('cannot be empty'),
|
|
116
|
+
'Exception message should indicate empty input');
|
|
117
|
+
}
|
|
118
|
+
Test.stopTest();
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
#### Naming Convention
|
|
123
|
+
|
|
124
|
+
- `should[ExpectedResult]_When[Scenario]`: `shouldSendNotification_WhenOpportunityClosedWon`
|
|
125
|
+
- `[SubjectOrAction]_[Scenario]_[ExpectedResult]`: `AccountUpdate_ChangeName_Success`
|
|
126
|
+
|
|
127
|
+
### Step 3 — Run Tests
|
|
128
|
+
|
|
129
|
+
Start narrow when debugging; widen after the fix is stable.
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
# Single test class
|
|
133
|
+
sf apex run test --class-names MyServiceTest --result-format human --code-coverage --target-org <alias>
|
|
134
|
+
|
|
135
|
+
# Specific test methods
|
|
136
|
+
sf apex run test --tests MyServiceTest.shouldUpdateStatus_WhenValidInput --result-format human --target-org <alias>
|
|
137
|
+
|
|
138
|
+
# All local tests
|
|
139
|
+
sf apex run test --test-level RunLocalTests --result-format human --code-coverage --target-org <alias>
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
### Step 4 — Analyze Results
|
|
143
|
+
|
|
144
|
+
Focus on:
|
|
145
|
+
|
|
146
|
+
- failing methods — exception types and stack traces
|
|
147
|
+
- uncovered lines and weak coverage areas
|
|
148
|
+
- whether failures indicate bad test data, brittle assertions, or broken production logic
|
|
149
|
+
|
|
150
|
+
### Step 5 — Fix Loop
|
|
151
|
+
|
|
152
|
+
When tests fail, run a disciplined fix loop (max 3 iterations — stop and surface root cause if still failing):
|
|
153
|
+
|
|
154
|
+
1. Read the failing test class and the class under test
|
|
155
|
+
2. Identify root cause from error messages and stack traces
|
|
156
|
+
3. Apply fix — adjust test data or assertions for test-side issues; delegate production code issues to the `generating-apex` skill
|
|
157
|
+
4. Rerun the focused test before broader regression
|
|
158
|
+
5. Repeat until all tests pass, iteration limit reached, or root cause requires design change
|
|
159
|
+
|
|
160
|
+
### Step 6 — Validate Coverage
|
|
161
|
+
|
|
162
|
+
| Level | Coverage | Purpose |
|
|
163
|
+
|-------|----------|---------|
|
|
164
|
+
| Production deploy | 75% minimum | Required by Salesforce |
|
|
165
|
+
| Recommended | 90%+ | Best practice target |
|
|
166
|
+
| Critical paths | 100% | Business-critical code |
|
|
167
|
+
|
|
168
|
+
Cover all paths: positive, negative/exception, bulk (251+ records), callout/async.
|
|
169
|
+
|
|
170
|
+
## What to Test by Component
|
|
171
|
+
|
|
172
|
+
| Component | Key Test Scenarios |
|
|
173
|
+
|-----------|-------------------|
|
|
174
|
+
| Trigger | Bulk insert/update/delete, recursion guard, field change detection |
|
|
175
|
+
| Service | Valid/invalid inputs, bulk operations, exception handling |
|
|
176
|
+
| Controller | Page load, action methods, view state |
|
|
177
|
+
| Batch | start/execute/finish, scope matching (batch size >= record count), `Database.Stateful` tracking, error handling, chaining (separate methods — `finish()` calling `Database.executeBatch()` throws `UnexpectedException`) |
|
|
178
|
+
| Queueable | Chaining (only first job runs in tests), bulkification, error handling, callout mocks before `Test.startTest()` |
|
|
179
|
+
| Callout | Success response, error response, timeout |
|
|
180
|
+
| Selector | Valid/null/empty inputs, bulk (251+), field population, sort order, `WITH USER_MODE` via `System.runAs` |
|
|
181
|
+
| Scheduled | Direct execution via `execute(null)`, CRON registration via `CronTrigger` query |
|
|
182
|
+
| Platform Event | `Test.enableChangeDataCapture()`, `Test.getEventBus().deliver()`, verify subscriber side effects |
|
|
183
|
+
|
|
184
|
+
## Output Expectations
|
|
185
|
+
|
|
186
|
+
Deliverables per test class:
|
|
187
|
+
- `{ClassName}Test.cls` + `{ClassName}Test.cls-meta.xml` (match API version of class under test; default `66.0`)
|
|
188
|
+
- `TestDataFactory.cls` + `TestDataFactory.cls-meta.xml` (if not already present)
|
|
189
|
+
|
|
190
|
+
## Reference Files
|
|
191
|
+
|
|
192
|
+
Load on demand for detailed patterns:
|
|
193
|
+
|
|
194
|
+
| Reference | When to use |
|
|
195
|
+
|-----------|-------------|
|
|
196
|
+
| [references/test-data-factory.md](references/test-data-factory.md) | TestDataFactory patterns, field overrides, duplicate rule handling |
|
|
197
|
+
| [references/assertion-patterns.md](references/assertion-patterns.md) | Assertion best practices, anti-patterns, common pitfalls |
|
|
198
|
+
| [references/mocking-patterns.md](references/mocking-patterns.md) | HttpCalloutMock, DML mocking, StubProvider, SOSL, Email, Platform Events |
|
|
199
|
+
| [references/async-testing.md](references/async-testing.md) | Batch, Queueable, Future, Scheduled job testing |
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Test class for {ClassUnderTest}.
|
|
3
|
+
* Tests bulk operations (251+ records), positive/negative paths,
|
|
4
|
+
* and exception handling.
|
|
5
|
+
*/
|
|
6
|
+
@isTest
|
|
7
|
+
private class {ClassUnderTest}Test {
|
|
8
|
+
|
|
9
|
+
@TestSetup
|
|
10
|
+
static void setupTestData() {
|
|
11
|
+
List<Account> accounts = TestDataFactory.createAccounts(251, true);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// ─── Positive Tests ───────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
@isTest
|
|
17
|
+
static void shouldPerformExpectedBehavior_WhenValidInput() {
|
|
18
|
+
// Given
|
|
19
|
+
List<Account> accounts = [SELECT Id, Name FROM Account];
|
|
20
|
+
|
|
21
|
+
// When
|
|
22
|
+
Test.startTest();
|
|
23
|
+
// {ClassUnderTest}.methodUnderTest(params);
|
|
24
|
+
Test.stopTest();
|
|
25
|
+
|
|
26
|
+
// Then
|
|
27
|
+
// Assert.areEqual(expected, actual, 'Descriptive failure message');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@isTest
|
|
31
|
+
static void shouldHandleBulkRecords_WhenProcessing251() {
|
|
32
|
+
// Given
|
|
33
|
+
List<Account> accounts = [SELECT Id FROM Account];
|
|
34
|
+
Assert.areEqual(251, accounts.size(), 'Should have 251 test records');
|
|
35
|
+
|
|
36
|
+
// When
|
|
37
|
+
Test.startTest();
|
|
38
|
+
// {ClassUnderTest}.bulkMethod(accounts);
|
|
39
|
+
Test.stopTest();
|
|
40
|
+
|
|
41
|
+
// Then
|
|
42
|
+
// List<Account> results = [SELECT Id, Status__c FROM Account];
|
|
43
|
+
// for (Account acc : results) {
|
|
44
|
+
// Assert.areEqual('Processed', acc.Status__c, 'All records should be processed');
|
|
45
|
+
// }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Negative Tests ───────────────────────────────────────────────────
|
|
49
|
+
|
|
50
|
+
@isTest
|
|
51
|
+
static void shouldThrowException_WhenNullInput() {
|
|
52
|
+
Test.startTest();
|
|
53
|
+
try {
|
|
54
|
+
// {ClassUnderTest}.methodUnderTest(null);
|
|
55
|
+
Assert.fail('Expected exception for null input');
|
|
56
|
+
} catch (MyCustomException e) {
|
|
57
|
+
Assert.isTrue(e.getMessage().contains('cannot be null'),
|
|
58
|
+
'Exception message should mention null input');
|
|
59
|
+
}
|
|
60
|
+
Test.stopTest();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@isTest
|
|
64
|
+
static void shouldReturnEmpty_WhenEmptyInput() {
|
|
65
|
+
Test.startTest();
|
|
66
|
+
// List<SObject> results = {ClassUnderTest}.methodUnderTest(new List<Id>());
|
|
67
|
+
Test.stopTest();
|
|
68
|
+
|
|
69
|
+
// Assert.isTrue(results.isEmpty(), 'Should return empty list for empty input');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Edge Case Tests ──────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
@isTest
|
|
75
|
+
static void shouldHandleMixedRecords_WhenSomeQualify() {
|
|
76
|
+
// Given
|
|
77
|
+
List<Account> accounts = [SELECT Id, Status__c FROM Account];
|
|
78
|
+
Integer half = accounts.size() / 2;
|
|
79
|
+
// for (Integer i = 0; i < half; i++) {
|
|
80
|
+
// accounts[i].Status__c = 'Qualifying';
|
|
81
|
+
// }
|
|
82
|
+
// update accounts;
|
|
83
|
+
|
|
84
|
+
// When
|
|
85
|
+
Test.startTest();
|
|
86
|
+
// {ClassUnderTest}.conditionalMethod(accounts);
|
|
87
|
+
Test.stopTest();
|
|
88
|
+
|
|
89
|
+
// Then
|
|
90
|
+
// List<Account> qualifying = [SELECT Id FROM Account WHERE Processed__c = true];
|
|
91
|
+
// Assert.areEqual(half, qualifying.size(), 'Only qualifying records should be processed');
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @description Centralized factory for creating test data with sensible defaults.
|
|
3
|
+
* All methods accept a doInsert flag for flexibility.
|
|
4
|
+
* Bulk methods create multiple records; single-record methods delegate to bulk.
|
|
5
|
+
*/
|
|
6
|
+
@isTest
|
|
7
|
+
public class TestDataFactory {
|
|
8
|
+
|
|
9
|
+
// ─── Accounts ─────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
public static List<Account> createAccounts(Integer count, Boolean doInsert) {
|
|
12
|
+
List<Account> accounts = new List<Account>();
|
|
13
|
+
for (Integer i = 0; i < count; i++) {
|
|
14
|
+
accounts.add(new Account(
|
|
15
|
+
Name = 'Test Account ' + i,
|
|
16
|
+
BillingCity = 'San Francisco',
|
|
17
|
+
BillingState = 'CA',
|
|
18
|
+
BillingCountry = 'USA',
|
|
19
|
+
Industry = 'Technology',
|
|
20
|
+
Type = 'Customer'
|
|
21
|
+
));
|
|
22
|
+
}
|
|
23
|
+
if (doInsert) insert accounts;
|
|
24
|
+
return accounts;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public static Account createAccount(Boolean doInsert) {
|
|
28
|
+
return createAccounts(1, doInsert)[0];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Contacts ─────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
public static List<Contact> createContacts(List<Account> accounts, Integer countPerAccount, Boolean doInsert) {
|
|
34
|
+
List<Contact> contacts = new List<Contact>();
|
|
35
|
+
Integer idx = 0;
|
|
36
|
+
for (Account acc : accounts) {
|
|
37
|
+
for (Integer i = 0; i < countPerAccount; i++) {
|
|
38
|
+
contacts.add(new Contact(
|
|
39
|
+
FirstName = 'Test',
|
|
40
|
+
LastName = 'Contact ' + idx,
|
|
41
|
+
Email = 'test.contact' + idx + '@example.com',
|
|
42
|
+
AccountId = acc.Id
|
|
43
|
+
));
|
|
44
|
+
idx++;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
if (doInsert) insert contacts;
|
|
48
|
+
return contacts;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Opportunities ────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
public static List<Opportunity> createOpportunities(List<Account> accounts, Integer countPerAccount, Boolean doInsert) {
|
|
54
|
+
List<Opportunity> opps = new List<Opportunity>();
|
|
55
|
+
Integer idx = 0;
|
|
56
|
+
for (Account acc : accounts) {
|
|
57
|
+
for (Integer i = 0; i < countPerAccount; i++) {
|
|
58
|
+
opps.add(new Opportunity(
|
|
59
|
+
Name = 'Test Opportunity ' + idx,
|
|
60
|
+
AccountId = acc.Id,
|
|
61
|
+
StageName = 'Prospecting',
|
|
62
|
+
CloseDate = Date.today().addDays(30),
|
|
63
|
+
Amount = 10000 + (idx * 1000)
|
|
64
|
+
));
|
|
65
|
+
idx++;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (doInsert) insert opps;
|
|
69
|
+
return opps;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// ─── Users ────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
public static User createUser(String profileName, Boolean doInsert) {
|
|
75
|
+
Profile p = [SELECT Id FROM Profile WHERE Name = :profileName LIMIT 1];
|
|
76
|
+
String uniqueKey = String.valueOf(DateTime.now().getTime());
|
|
77
|
+
|
|
78
|
+
User u = new User(
|
|
79
|
+
FirstName = 'Test',
|
|
80
|
+
LastName = 'User ' + uniqueKey,
|
|
81
|
+
Email = 'testuser' + uniqueKey + '@example.com',
|
|
82
|
+
Username = 'testuser' + uniqueKey + '@example.com.test',
|
|
83
|
+
Alias = 'tuser',
|
|
84
|
+
TimeZoneSidKey = 'America/Los_Angeles',
|
|
85
|
+
LocaleSidKey = 'en_US',
|
|
86
|
+
EmailEncodingKey = 'UTF-8',
|
|
87
|
+
LanguageLocaleKey = 'en_US',
|
|
88
|
+
ProfileId = p.Id
|
|
89
|
+
);
|
|
90
|
+
if (doInsert) insert u;
|
|
91
|
+
return u;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── Field Override Pattern ────────────────────────────────────────────
|
|
95
|
+
|
|
96
|
+
public static Account createAccount(Map<String, Object> fieldOverrides, Boolean doInsert) {
|
|
97
|
+
Account acc = new Account(
|
|
98
|
+
Name = 'Test Account',
|
|
99
|
+
Industry = 'Technology'
|
|
100
|
+
);
|
|
101
|
+
for (String fieldName : fieldOverrides.keySet()) {
|
|
102
|
+
acc.put(fieldName, fieldOverrides.get(fieldName));
|
|
103
|
+
}
|
|
104
|
+
if (doInsert) insert acc;
|
|
105
|
+
return acc;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ─── Custom Objects ───────────────────────────────────────────────────
|
|
109
|
+
// Add methods for your custom objects following the same pattern:
|
|
110
|
+
// public static List<MyObject__c> createMyObjects(Integer count, Boolean doInsert) { ... }
|
|
111
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Assertion Patterns
|
|
2
|
+
|
|
3
|
+
## Assertion Methods
|
|
4
|
+
|
|
5
|
+
| Method | Use Case |
|
|
6
|
+
|--------|----------|
|
|
7
|
+
| `Assert.areEqual(expected, actual, msg)` | Exact equality |
|
|
8
|
+
| `Assert.areNotEqual(expected, actual, msg)` | Value should differ |
|
|
9
|
+
| `Assert.isTrue(condition, msg)` | Boolean condition |
|
|
10
|
+
| `Assert.isFalse(condition, msg)` | Negated boolean condition |
|
|
11
|
+
| `Assert.fail(msg)` | Force failure (e.g., expected exception not thrown) |
|
|
12
|
+
| `Assert.isNotNull(value, msg)` | Non-null check |
|
|
13
|
+
| `Assert.isNull(value, msg)` | Null check |
|
|
14
|
+
|
|
15
|
+
**Always include the message parameter** — makes test failures actionable.
|
|
16
|
+
|
|
17
|
+
## Good vs Bad Assertions
|
|
18
|
+
|
|
19
|
+
### Bad: No message, tests coverage not behavior
|
|
20
|
+
|
|
21
|
+
```apex
|
|
22
|
+
Assert.isTrue(result); // no message
|
|
23
|
+
Assert.isTrue(accounts.size() > 0); // vague — use areEqual with exact count
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Good: Descriptive message, tests specific behavior
|
|
27
|
+
|
|
28
|
+
```apex
|
|
29
|
+
Assert.areEqual(true, result, 'Service should return true for valid input');
|
|
30
|
+
Assert.areEqual(200, accounts.size(), 'All 200 accounts should be processed');
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Common Assertion Patterns
|
|
34
|
+
|
|
35
|
+
### Collection Size
|
|
36
|
+
|
|
37
|
+
```apex
|
|
38
|
+
Assert.areEqual(200, results.size(), 'Should process all 200 records');
|
|
39
|
+
Assert.isTrue(results.isEmpty(), 'No results expected for invalid input');
|
|
40
|
+
Assert.isFalse(results.isEmpty(), 'Results should not be empty');
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Field Values
|
|
44
|
+
|
|
45
|
+
```apex
|
|
46
|
+
Assert.areEqual('Processed', acc.Status__c, 'Account status should be updated to Processed');
|
|
47
|
+
|
|
48
|
+
for (Account acc : updatedAccounts) {
|
|
49
|
+
Assert.areEqual('Active', acc.Status__c,
|
|
50
|
+
'Account ' + acc.Name + ' should have Active status');
|
|
51
|
+
}
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Exception Testing
|
|
55
|
+
|
|
56
|
+
```apex
|
|
57
|
+
@isTest
|
|
58
|
+
static void shouldThrowException_WhenInputInvalid() {
|
|
59
|
+
Test.startTest();
|
|
60
|
+
try {
|
|
61
|
+
MyService.process(null);
|
|
62
|
+
Assert.fail('Expected MyCustomException for null input');
|
|
63
|
+
} catch (MyCustomException e) {
|
|
64
|
+
Assert.isTrue(e.getMessage().contains('cannot be null'),
|
|
65
|
+
'Exception message should mention null input');
|
|
66
|
+
}
|
|
67
|
+
Test.stopTest();
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### DML Results
|
|
72
|
+
|
|
73
|
+
```apex
|
|
74
|
+
// Insert success
|
|
75
|
+
Database.SaveResult[] results = Database.insert(accounts, false);
|
|
76
|
+
for (Database.SaveResult sr : results) {
|
|
77
|
+
Assert.isTrue(sr.isSuccess(), 'Insert should succeed: ' + sr.getErrors());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
Database.SaveResult sr = Database.insert(invalidAccount, false);
|
|
81
|
+
Assert.isFalse(sr.isSuccess(), 'Insert should fail for invalid data');
|
|
82
|
+
Assert.isTrue(sr.getErrors()[0].getMessage().contains('REQUIRED_FIELD_MISSING'),
|
|
83
|
+
'Error should indicate missing required field');
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Null Checks
|
|
87
|
+
|
|
88
|
+
```apex
|
|
89
|
+
Assert.isNull(result.ErrorMessage__c, 'No error expected for valid input');
|
|
90
|
+
Assert.isNotNull(result.Id, 'Record should have been inserted');
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Date/DateTime
|
|
94
|
+
|
|
95
|
+
```apex
|
|
96
|
+
Assert.areEqual(Date.today(), record.CreatedDate__c, 'Should be created today');
|
|
97
|
+
Assert.isTrue(record.DueDate__c >= Date.today(), 'Due date should be in the future');
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## Anti-Patterns
|
|
101
|
+
|
|
102
|
+
| Anti-Pattern | Fix |
|
|
103
|
+
|---|---|
|
|
104
|
+
| `Assert.isTrue(results.size() > 0)` | Use `Assert.areEqual(expectedCount, results.size(), ...)` |
|
|
105
|
+
| `Assert.isTrue(results.size() >= expected)` | Compute exact expected count, use `Assert.areEqual` |
|
|
106
|
+
| Testing implementation not behavior | Assert on observable outcomes (field values, record counts) |
|
|
107
|
+
| Missing negative test assertions | Verify the actual outcome, not just that no exception occurred |
|
|
108
|
+
| `Assert.isTrue(count != 0)` | Use `Assert.areEqual` with deterministic value from test data |
|