Skip to content

Commit bba4327

Browse files
Add Apex & Unit Test Skills (#105)
* added apex and apex test
1 parent 768aa1e commit bba4327

23 files changed

Lines changed: 2939 additions & 2 deletions

skills/creating-webapp/SKILL.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
---
22
name: creating-webapp
33
description: "Use this skill when creating or setting up a new SFDX React web application. Covers first steps, npm install, skills-first protocol, deployment order, and core web app rules."
4-
paths:
5-
- "**/webapplications/**/*"
64
---
75

86
# First Steps (MUST FOLLOW)
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
---
2+
name: generating-apex-test
3+
description: Apex test class generation with TestDataFactory patterns, bulk testing (200+ records), mocking strategies, and assertion best practices. Use this skill when the user asks to create, write, or improve Apex test classes, add coverage, build mocks, or implement testing patterns for triggers, services, batch jobs, queueables, and integrations.
4+
---
5+
6+
# Apex Test Class Skill
7+
8+
## Core Principles
9+
10+
1. **Bulkify tests** - Always test with 200+ records to catch governor limit issues
11+
2. **Isolate test data** - Use `@TestSetup` and TestDataFactory; never rely on org data
12+
3. **Assert meaningfully** - Test behavior, not just coverage; include failure messages
13+
4. **Mock external dependencies** - Use `HttpCalloutMock`, `Test.setMock()` for integrations
14+
5. **Test negative paths** - Validate error handling, not just happy paths
15+
16+
## Test Class Structure
17+
18+
```apex
19+
@isTest
20+
private class MyServiceTest {
21+
22+
@TestSetup
23+
static void setupTestData() {
24+
// Create shared test data using TestDataFactory
25+
List<Account> accounts = TestDataFactory.createAccounts(200, true);
26+
}
27+
28+
@isTest
29+
static void shouldPerformExpectedBehavior_WhenValidInput() {
30+
// Given: Setup specific test state
31+
List<Account> accounts = [SELECT Id, Name FROM Account];
32+
33+
// When: Execute the code under test
34+
Test.startTest();
35+
MyService.processAccounts(accounts);
36+
Test.stopTest();
37+
38+
// Then: Assert expected outcomes
39+
List<Account> updated = [SELECT Id, Status__c FROM Account];
40+
System.assertEquals(200, updated.size(), 'All accounts should be processed');
41+
for (Account acc : updated) {
42+
System.assertEquals('Processed', acc.Status__c, 'Status should be updated');
43+
}
44+
}
45+
46+
@isTest
47+
static void shouldThrowException_WhenInvalidInput() {
48+
// Given
49+
List<Account> emptyList = new List<Account>();
50+
51+
// When/Then
52+
Test.startTest();
53+
try {
54+
MyService.processAccounts(emptyList);
55+
System.assert(false, 'Expected MyCustomException to be thrown');
56+
} catch (MyCustomException e) {
57+
System.assert(e.getMessage().contains('cannot be empty'),
58+
'Exception message should indicate empty input');
59+
}
60+
Test.stopTest();
61+
}
62+
}
63+
```
64+
65+
## Naming Convention
66+
67+
Use descriptive method names: `should[ExpectedBehavior]_When[Condition]`
68+
69+
Examples:
70+
- `shouldCreateContact_WhenAccountIsActive`
71+
- `shouldThrowException_WhenEmailIsInvalid`
72+
- `shouldSendNotification_WhenOpportunityClosedWon`
73+
- `shouldBypassTrigger_WhenRunningAsBatch`
74+
75+
## Test.startTest() / Test.stopTest()
76+
77+
Always wrap the code under test:
78+
- Resets governor limits for accurate limit testing
79+
- Executes async operations synchronously (queueables, batch, future)
80+
- Fires scheduled jobs immediately
81+
82+
## Asset Templates
83+
84+
Ready-to-use scaffolds for common test patterns:
85+
86+
- **[assets/test-class-template.cls](assets/test-class-template.cls)** - Starter test class with positive, negative, bulk, and governor limit test stubs
87+
- **[assets/test-data-factory-template.cls](assets/test-data-factory-template.cls)** - TestDataFactory with Account, Contact, Opportunity, User factories and field override support
88+
89+
## Reference Files
90+
91+
Detailed patterns for specific scenarios:
92+
93+
- **[references/test-data-factory.md](references/test-data-factory.md)** - TestDataFactory class patterns and field defaults
94+
- **[references/assertion-patterns.md](references/assertion-patterns.md)** - Assertion best practices and common pitfalls
95+
- **[references/mocking-patterns.md](references/mocking-patterns.md)** - HttpCalloutMock, Test.setMock(), stubbing
96+
- **[references/async-testing.md](references/async-testing.md)** - Batch, Queueable, Future, Scheduled job testing
97+
98+
## Quick Reference: What to Test
99+
100+
| Component | Key Test Scenarios |
101+
|-----------|-------------------|
102+
| Trigger | Bulk insert/update/delete, recursion, field changes |
103+
| Service | Valid/invalid inputs, bulk operations, exceptions |
104+
| Controller | Page load, action methods, view state |
105+
| Batch | Start/execute/finish, chunking, error records |
106+
| Queueable | Chaining, bulkification, error handling |
107+
| Callout | Success response, error response, timeout |
108+
| Scheduled | Execution, CRON validation |
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/**
2+
* @description Test class for {ClassUnderTest}.
3+
* Tests bulk operations (200+ records), positive/negative paths,
4+
* and exception handling.
5+
* @author Generated by Apex Test Writer Skill
6+
*/
7+
@isTest
8+
private class {ClassUnderTest}Test {
9+
10+
// ─── Test Setup ───────────────────────────────────────────────────────
11+
12+
@TestSetup
13+
static void setupTestData() {
14+
// Create shared test data using TestDataFactory
15+
// List<Account> accounts = TestDataFactory.createAccounts(200, true);
16+
}
17+
18+
// ─── Positive Tests ───────────────────────────────────────────────────
19+
20+
@isTest
21+
static void shouldPerformExpectedBehavior_WhenValidInput() {
22+
// Given: Setup specific test state
23+
// List<Account> accounts = [SELECT Id, Name FROM Account];
24+
25+
// When: Execute the code under test
26+
Test.startTest();
27+
// {ClassUnderTest}.methodUnderTest(params);
28+
Test.stopTest();
29+
30+
// Then: Assert expected outcomes
31+
// System.assertEquals(expected, actual, 'Descriptive failure message');
32+
}
33+
34+
@isTest
35+
static void shouldHandleBulkRecords_WhenProcessing200() {
36+
// Given: 200+ records to verify bulkification
37+
// List<Account> accounts = [SELECT Id FROM Account];
38+
// System.assertEquals(200, accounts.size(), 'Should have 200 test records');
39+
40+
// When
41+
Test.startTest();
42+
// {ClassUnderTest}.bulkMethod(accounts);
43+
Test.stopTest();
44+
45+
// Then: Verify all records processed
46+
// List<Account> results = [SELECT Id, Status__c FROM Account];
47+
// for (Account acc : results) {
48+
// System.assertEquals('Processed', acc.Status__c, 'All records should be processed');
49+
// }
50+
}
51+
52+
// ─── Negative Tests ───────────────────────────────────────────────────
53+
54+
@isTest
55+
static void shouldThrowException_WhenNullInput() {
56+
Boolean exceptionThrown = false;
57+
String exceptionMessage = '';
58+
59+
Test.startTest();
60+
try {
61+
// {ClassUnderTest}.methodUnderTest(null);
62+
} catch (Exception e) {
63+
exceptionThrown = true;
64+
exceptionMessage = e.getMessage();
65+
}
66+
Test.stopTest();
67+
68+
System.assert(exceptionThrown, 'Exception should be thrown for null input');
69+
System.assert(exceptionMessage.contains('cannot be null'),
70+
'Exception message should mention null input');
71+
}
72+
73+
@isTest
74+
static void shouldReturnEmpty_WhenEmptyInput() {
75+
Test.startTest();
76+
// List<SObject> results = {ClassUnderTest}.methodUnderTest(new List<Id>());
77+
Test.stopTest();
78+
79+
// System.assert(results.isEmpty(), 'Should return empty list for empty input');
80+
}
81+
82+
// ─── Edge Case Tests ──────────────────────────────────────────────────
83+
84+
@isTest
85+
static void shouldHandleMixedRecords_WhenSomeQualify() {
86+
// Given: Mix of qualifying and non-qualifying records
87+
// List<Account> accounts = [SELECT Id, Status__c FROM Account];
88+
// Integer half = accounts.size() / 2;
89+
// for (Integer i = 0; i < half; i++) {
90+
// accounts[i].Status__c = 'Qualifying';
91+
// }
92+
// update accounts;
93+
94+
// When
95+
Test.startTest();
96+
// {ClassUnderTest}.conditionalMethod(accounts);
97+
Test.stopTest();
98+
99+
// Then: Only qualifying records should be affected
100+
// List<Account> qualifying = [SELECT Id FROM Account WHERE Processed__c = true];
101+
// System.assertEquals(half, qualifying.size(), 'Only qualifying records should be processed');
102+
}
103+
104+
// ─── Governor Limit Tests ─────────────────────────────────────────────
105+
106+
@isTest
107+
static void shouldNotExceedGovernorLimits_WhenBulkProcessing() {
108+
// Given
109+
// List<Account> accounts = [SELECT Id FROM Account];
110+
111+
Test.startTest();
112+
// {ClassUnderTest}.heavyMethod(accounts);
113+
Test.stopTest();
114+
115+
System.assert(Limits.getDmlStatements() < Limits.getLimitDmlStatements(),
116+
'Should not exceed DML statement limit');
117+
System.assert(Limits.getQueries() < Limits.getLimitQueries(),
118+
'Should not exceed SOQL query limit');
119+
}
120+
121+
// ─── Helper Methods ───────────────────────────────────────────────────
122+
123+
// Add test-specific helper methods here
124+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
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+
* @author Generated by Apex Test Writer Skill
6+
*/
7+
@isTest
8+
public class TestDataFactory {
9+
10+
// ─── Accounts ─────────────────────────────────────────────────────────
11+
12+
public static List<Account> createAccounts(Integer count, Boolean doInsert) {
13+
List<Account> accounts = new List<Account>();
14+
for (Integer i = 0; i < count; i++) {
15+
accounts.add(new Account(
16+
Name = 'Test Account ' + i,
17+
BillingCity = 'San Francisco',
18+
BillingState = 'CA',
19+
BillingCountry = 'USA',
20+
Industry = 'Technology',
21+
Type = 'Customer'
22+
));
23+
}
24+
if (doInsert) insert accounts;
25+
return accounts;
26+
}
27+
28+
public static Account createAccount(Boolean doInsert) {
29+
return createAccounts(1, doInsert)[0];
30+
}
31+
32+
// ─── Contacts ─────────────────────────────────────────────────────────
33+
34+
public static List<Contact> createContacts(List<Account> accounts, Integer countPerAccount, Boolean doInsert) {
35+
List<Contact> contacts = new List<Contact>();
36+
Integer idx = 0;
37+
for (Account acc : accounts) {
38+
for (Integer i = 0; i < countPerAccount; i++) {
39+
contacts.add(new Contact(
40+
FirstName = 'Test',
41+
LastName = 'Contact ' + idx,
42+
Email = 'test.contact' + idx + '@example.com',
43+
AccountId = acc.Id
44+
));
45+
idx++;
46+
}
47+
}
48+
if (doInsert) insert contacts;
49+
return contacts;
50+
}
51+
52+
// ─── Opportunities ────────────────────────────────────────────────────
53+
54+
public static List<Opportunity> createOpportunities(List<Account> accounts, Integer countPerAccount, Boolean doInsert) {
55+
List<Opportunity> opps = new List<Opportunity>();
56+
Integer idx = 0;
57+
for (Account acc : accounts) {
58+
for (Integer i = 0; i < countPerAccount; i++) {
59+
opps.add(new Opportunity(
60+
Name = 'Test Opportunity ' + idx,
61+
AccountId = acc.Id,
62+
StageName = 'Prospecting',
63+
CloseDate = Date.today().addDays(30),
64+
Amount = 10000 + (idx * 1000)
65+
));
66+
idx++;
67+
}
68+
}
69+
if (doInsert) insert opps;
70+
return opps;
71+
}
72+
73+
// ─── Users ────────────────────────────────────────────────────────────
74+
75+
public static User createUser(String profileName, Boolean doInsert) {
76+
Profile p = [SELECT Id FROM Profile WHERE Name = :profileName LIMIT 1];
77+
String uniqueKey = String.valueOf(DateTime.now().getTime());
78+
79+
User u = new User(
80+
FirstName = 'Test',
81+
LastName = 'User ' + uniqueKey,
82+
Email = 'testuser' + uniqueKey + '@example.com',
83+
Username = 'testuser' + uniqueKey + '@example.com.test',
84+
Alias = 'tuser',
85+
TimeZoneSidKey = 'America/Los_Angeles',
86+
LocaleSidKey = 'en_US',
87+
EmailEncodingKey = 'UTF-8',
88+
LanguageLocaleKey = 'en_US',
89+
ProfileId = p.Id
90+
);
91+
if (doInsert) insert u;
92+
return u;
93+
}
94+
95+
// ─── Field Override Pattern ────────────────────────────────────────────
96+
97+
public static Account createAccount(Map<String, Object> fieldOverrides, Boolean doInsert) {
98+
Account acc = new Account(
99+
Name = 'Test Account',
100+
Industry = 'Technology'
101+
);
102+
for (String fieldName : fieldOverrides.keySet()) {
103+
acc.put(fieldName, fieldOverrides.get(fieldName));
104+
}
105+
if (doInsert) insert acc;
106+
return acc;
107+
}
108+
109+
// ─── Custom Objects ───────────────────────────────────────────────────
110+
// Add methods for your custom objects following the same pattern:
111+
// public static List<MyObject__c> createMyObjects(Integer count, Boolean doInsert) { ... }
112+
}

0 commit comments

Comments
 (0)