Testing Fundamentals
The older System.assert, System.assertEquals, and System.assertNotEquals still work; Assert (Winter '23+) is clearer and recommended.
22.1 Why and how much
Deploying Apex to production requires at least 75% overall code coverage across all Apex classes, every trigger to have some coverage, and all tests to pass.
Coverage is a minimum, not a goal: aim for meaningful assertions on positive, negative, bulk (200+ records), and permission scenarios.
Test code does not count toward the 6 MB code-size limit, and test data is rolled back automatically.
22.2 Test class structure
@isTest
private class OpportunityServiceTest {
@testSetup
static void setup() {
// runs once; data is rolled back after each test method but re-available to each
Account acc = new Account(Name = 'Test Co');
insert acc;
List<Opportunity> opps = new List<Opportunity>();
for (Integer i = 0; i < 200; i++) {
opps.add(new Opportunity(Name = 'Opp ' + i, AccountId = acc.Id,
StageName = 'Prospecting', CloseDate = Date.today().addDays(30), Amount = 1000));
}
insert opps;
}
@isTest
static void closingWonCreatesTasks() {
List<Opportunity> opps = [SELECT Id FROM Opportunity];
for (Opportunity o : opps) o.StageName = 'Closed Won';
Test.startTest(); // fresh set of governor limits
update opps;
Test.stopTest(); // async work enqueued inside runs synchronously here
Integer taskCount = [SELECT COUNT() FROM Task WHERE Subject = 'Send welcome kit'];
Assert.areEqual(200, taskCount, 'One task per closed-won opportunity');
}
@isTest
static void negativeAmountIsRejected() {
Opportunity o = [
…22.3 Test annotations and utilities
Marks test classes and methods (replaces the old testMethod keyword) Creates common data once per class
Lets tests see org data — avoid; tests become fragile Allows parallel execution (default behavior depends on org settings) Exposes private members to test classes Test.startTest() / Test.stopTest()
Fresh limits; executes queued async work at stopTest Branch behavior in tests (use sparingly)
Run as a specific user (sharing, permissions; not FLS by itself) Mock callouts (§26)
Test.loadData(Account.SObjectType, 'StaticResourceName') Load CSV test data from a static resource Define SOSL results Get the standard price book in tests Test.setCreatedDate(id, datetime) Back-date test records
Deliver platform events immediately (§25)
22.4 The Assert class
The older System.assert, System.assertEquals, and System.assertNotEquals still work; Assert (Winter '23+) is clearer and recommended.
Assert.areEqual(expected, actual, 'message');
Assert.areNotEqual(a, b);
Assert.isTrue(condition, 'message');
Assert.isFalse(condition);
Assert.isNull(value);
Assert.isNotNull(value);
Assert.isInstanceOfType(obj, Account.class);
Assert.isNotInstanceOfType(obj, Contact.class);
Assert.fail('Should not reach here');22.5 Test data factory
Testing as a restricted user:
@isTest
public class TestDataFactory {
public static List<Account> createAccounts(Integer count, Boolean doInsert) {
List<Account> accs = new List<Account>();
for (Integer i = 0; i < count; i++) {
accs.add(new Account(Name = 'Test Account ' + i, Industry = 'Technology'));
}
if (doInsert) insert accs;
return accs;
}
public static User createUser(String profileName, String permSetName) {
Profile p = [SELECT Id FROM Profile WHERE Name = :profileName LIMIT 1];
String uniq = String.valueOf(Crypto.getRandomInteger()).replace('-', '');
User u = new User(
Alias = 'tuser', Email = 'tuser' + uniq + '@example.com',
EmailEncodingKey = 'UTF-8', LastName = 'Tester', LanguageLocaleKey = 'en_US',
LocaleSidKey = 'en_US', ProfileId = p.Id, TimeZoneSidKey = 'America/Los_Angeles',
Username = 'tuser' + uniq + '@example.com.test'
);
insert u;
if (permSetName != null) {
PermissionSet ps = [SELECT Id FROM PermissionSet WHERE Name = :permSetName LIMIT 1];
insert new PermissionSetAssignment(AssigneeId = u.Id, PermissionSetId
…@isTest
static void standardUserSeesOnlyOwnRecords() {
User u = TestDataFactory.createUser('Standard User', null);
System.runAs(u) {
List<Account> visible = AccountController.getMyAccounts();
Assert.areEqual(0, visible.size());
}
}22.6 Running tests
Also: VS Code Testing panel, Developer Console (*Test → New Run*), and *Setup → Apex Test Execution*.
sf apex run test --test-level RunLocalTests --code-coverage --result-format human --wait 20
sf apex run test --class-names OpportunityServiceTest --tests OpportunityServiceTest.closingWonCreatesTasks
sf apex get test --test-run-id 707xx0000000001 --code-coverage🧪 Part II Exercises
Build a ShoppingCart class with an inner CartItem class, a computed total property, method chaining, and a static counter of carts created in the transaction.
Create an abstract class Shape with area() and perimeter(), and Circle, Rectangle, Triangle subclasses; sort a list of shapes by area using Comparable.
Write three classes demonstrating with sharing, without sharing, and inherited sharing; call them from Anonymous Apex as a user who owns no Accounts and compare results.
Write a SOQL query that returns Accounts with at least one open Opportunity and no Cases, including the opportunities in a subquery.
Write an aggregate query that returns total closed-won amount per owner per calendar month for this year.
Build a safe dynamic search method that accepts an object name, a list of fields, and a sort field, validating all identifiers against describe information and running in user mode.
Insert 10 Contacts with Database.insert(list, false) where three are invalid; log each failure's row index, status code, and message.
Implement a bulk-safe trigger on Contact that keeps a Number_of_Contacts__c field on Account up to date for insert, update (account change), delete, and undelete.
Refactor exercise 8 to use the TriggerHandler base class from §19 with a custom-metadata kill switch and recursion protection.
Write a test class for exercise 9 that uses @testSetup, a TestDataFactory, a 250-record bulk test, a negative test, and System.runAs.