Testing, Debugging & Deployment PD1 · Section 4 · 22% of exam

Overview

This section is worth ~22% of PD1 and is the single most commonly under-prepared area. Master four things: writing isolated test data, testing async & callouts, reading logs/exceptions, and the deployment rules.

75%Org-wide coverage to deploy
>0%Every trigger covered
AllTests must pass
AtomicDeploy all-or-nothing

Exam checklist

  • @isTest · SeeAllData=false · Test Data Factory · @testSetup
  • Assertions (prefer assertEquals) · Positive + Negative + Bulk (200) tests
  • Test.startTest() / Test.stopTest() · Governor-limit awareness
  • Mocks for callouts · System.runAs() for sharing · 75% coverage rule
Top traps75% is the whole org, not every class · startTest resets limits, stopTest runs async · LimitException can't be caught · a single-row SOQL with 0 rows throws QueryException · deployment moves metadata, not data.

Part 1 — Testing Fundamentals

Why testing exists: all Apex runs in a shared multitenant environment, so tests verify correctness, edge cases, and that nothing breaks — and they run automatically during deployment.

Deployment requirements & the 75% rule

  • Production needs ≥75% org-wide Apex coverage, every trigger with some coverage, and all tests passing.
  • 75% is ORG coverage — not every class. Class A 100% + B 60% + C 40% can still average ≥75% → deploy succeeds.
  • Coverage counts executable lines only; comments, blank lines, and pure declarations are ignored. Formula: executed ÷ executable × 100.

@isTest & test methods

  • Marks class/method as test code — doesn't count against code size, can't be called by users, not exposed via API.
  • A test method takes no parameters, returns void, is marked @isTest.
@isTest
private class AccountTest {
    @isTest
    static void testInsertAccount() {
        // ...
    }
}

Test isolation & SeeAllData

  • Tests must never depend on existing org data — always create your own.
  • Default @isTest = SeeAllData=false (good): isolated, repeatable, portable, deploy-safe.
  • SeeAllData=true exposes real records — avoid it. Rare exceptions: standard Price Book, some setup/legacy data.

Test Data Factory PD1 loves this

Centralize record creation so tests stay reusable, clean, and maintainable.

@isTest
public class TestDataFactory {
    public static Account createAccount() {
        Account a = new Account(Name = 'Acme');
        insert a;
        return a;
    }
}
// Usage:  Account a = TestDataFactory.createAccount();

@testSetup

  • Runs once per class; Salesforce clones the setup data fresh for each test method.
  • Changing records in one method does not affect others. Less DML, faster, cleaner, fewer governor hits.
@testSetup
static void setup() {
    insert new Account(Name = 'Acme');
}
Trap"@testSetup runs before every method" is almost right — it runs once, then the data is cloned per method.

Assertions

  • Prefer System.assertEquals(expected, actual) — best error messages, Salesforce-recommended.
  • Also: assert(), assertNotEquals(), assertNotNull().
System.assertEquals(1, accounts.size());
System.assertNotEquals(null, acc.Id);
System.assertNotNull(contact);

Positive · Negative · Bulk

  • Positive: valid input → expect success.
  • Negative: invalid input → expect exception.
  • Bulk (200): triggers always receive lists — never test just one record. Exam favourite.
// Negative test
try {
    insert new Account();      // missing required Name
    System.assert(false);
} catch (Exception e) {
    System.assert(true);
}

Test.startTest() / Test.stopTest() High-yield

  • startTest() → fresh governor limits (e.g. 99 SOQL used → 0 after). Put setup before, code under test between.
  • stopTest() → forces Future / Queueable / Batch / Scheduled jobs to run synchronously so you can assert. Without it, async code never executes in the test.
Test.startTest();
System.enqueueJob(new MyQueueableJob());
Test.stopTest();               // queueable executes here
System.assertEquals(...);
TrapTest.startTest() does not execute async Apex — Test.stopTest() does.

Part 2 — Advanced Testing

Testing async Apex, callouts, permissions, and the test utility toolbox — the highest-density exam area of this section.

The async testing flow

Setup dataTest.startTest()Fresh limitsCall async codeTest.stopTest()Async runsAssert
Async typestopTest() required?
FutureYes
QueueableYes
BatchYes
ScheduledYes

Future methods

  • Must be static, void, @future. Cannot return a value or be called from another future method.
  • Parameters: primitives / collections of primitives / IDs only — no sObjects.
  • Callouts need @future(callout=true).
@future(callout=true)
public static void sendEmail(Id accountId) { }

Queueable Preferred

  • Handles complex objects, supports chaining, returns a Job ID, better monitoring.
  • Newer than Future and the recommended choice for new development.
System.enqueueJob(new MyQueueable());

Future vs Queueable

FutureQueueable
Primitive parameters onlyComplex objects allowed
No chainingChaining supported
No Job IDReturns Job ID
OlderRecommended

Batch Apex

  • Database.Batchable<SObject>start(), execute(), finish().
  • Default scope 200, max 2000. execute() runs once per batch scope (1000 records ÷ 200 = 5 executes).
Test.startTest();
Database.executeBatch(new AccountBatch(), 200);
Test.stopTest();

Scheduled Apex

  • Implements Schedulable with execute(); scheduled via System.schedule().
  • Cron: Seconds Minutes Hours Day Month Day-of-week [Year].
Test.startTest();
System.schedule('Job', '0 0 12 * * ?', new MyScheduler());
Test.stopTest();       // runs at stopTest; '0 0 12 * * ?' = daily 12 PM

Testing callouts High-yield

Real HTTP is not allowed in tests — you must register a mock before the callout.

global class MockCallout implements HttpCalloutMock {
    global HttpResponse respond(HttpRequest req) {
        HttpResponse res = new HttpResponse();
        res.setStatusCode(200);
        res.setBody('Success');
        return res;
    }
}
// Register (before the callout runs):
Test.setMock(HttpCalloutMock.class, new MockCallout());
MockUse for
HttpCalloutMockREST callouts
WebServiceMockSOAP callouts
StaticResourceCalloutMockResponse stored in a Static Resource (large JSON)
MultiStaticResourceCalloutMockMultiple endpoints / different responses (know purpose)

System.runAs()

  • Runs a block as another user — tests only. Used to test sharing/visibility and to avoid Mixed DML.
  • Changes: current user, sharing rules, role hierarchy, ownership, record access.
  • Does NOT change: governor limits, license limits, CRUD/FLS or profile permissions automatically.
System.runAs(user2) {
    insert account;   // runs as user2
}

Test utilities to memorize

  • Test.startTest() — reset limits
  • Test.stopTest() — run async
  • Test.setMock() — register callout mock
  • Test.loadData(Account.class,'CSV') — load CSV static resource
  • Test.isRunningTest() — true in test context (use sparingly; prefer DI)
  • Test.getStandardPricebookId() — only way to get the Standard Price Book
Trap bustersQueueable can chain · Future can't return values · HTTP callouts need mocks · runAs does not change limits · Batch execute() runs once per scope · startTest doesn't run async · Future takes no sObjects · Queueable is newer than Future.

Part 3 — Debugging

Debug logs are the primary tool. Know log levels/categories, the Developer Console, exception types, and which errors you can and can't catch.

Debug logs & Trace Flags

  • Logs record triggers, SOQL, DML, flows, validation, CPU/heap, exceptions.
  • Detailed logs need an active Trace Flag (User + Duration + Log Levels). Without it you may not get detailed logs. Exam favourite.

Log levels (low → high verbosity)

NONEERRORWARNINFODEBUGFINEFINERFINEST

Log categories

  • Apex Code — method calls, variables, System.debug()
  • Database — SOQL, SOSL, DML
  • Workflow — workflow rules, Flow, Process Builder
  • Validation — validation rules
  • Callout — HTTP requests/responses
  • Also: Apex Profiling, System, Visualforce, NBA, Lightning

System.debug()

  • Default level is DEBUG; logs only — never changes execution.
System.debug('Hello');
System.debug(LoggingLevel.ERROR, 'Failure');

Developer Console

  • Execute Anonymous — run Apex immediately (not saved, no coverage).
  • Query Editor — quick SOQL/SOSL.
  • Log Inspector — timeline, SOQL/DML, call stack, execution units, heap, limits.
  • Checkpoints — read-only variable inspection at a line (don't modify).
  • Heap Dump, Test execution.

Exception types

ExceptionWhenCatchable?
DmlExceptioninsert/update/delete/undelete/merge failure (e.g. missing required field)Yes
QueryExceptionSingle-row SOQL returns 0 or >1 rowsYes
NullPointerExceptionNull object reference (most common)Yes
ListExceptionBad list index / out of boundsYes
StringExceptionBad substring / parsingYes
CalloutExceptionHTTP failure, timeout, bad endpointYes
MathException / JSONExceptionDivide by zero / invalid JSONYes
LimitExceptionGovernor limit exceeded (e.g. 101 SOQL)NO — uncatchable
The big oneA single-sObject query with no rows throws QueryException — it does not return null. Query into a List and check isEmpty() instead.
List<Account> accounts = [SELECT Id FROM Account];
if (!accounts.isEmpty()) {
    // safe
}

try / catch / finally

  • finally always runs, even after an exception.
try {
    insert account;
} catch (DmlException e) {
    System.debug(e.getMessage());
} finally {
    System.debug('Done');
}

Common runtime errors

  • Too many SOQL / DML; CPU timeout; heap exceeded
  • Mixed DML — setup-object DML + non-setup DML in one transaction (e.g. insert User + insert Account)
  • Duplicate value / unique field / duplicate rule
  • FIELD_CUSTOM_VALIDATION_EXCEPTION, NullPointer, QueryException
Debug trapsLimitException can't be caught · single-row query on 0 rows → QueryException (not null) · NullPointer = null object, not empty list · System.debug doesn't change execution · Trace Flag required for detailed logs · Checkpoints are read-only.

Part 4 — Deployment

Deployment moves metadata, not business data, between orgs. It is atomic — success deploys everything, failure rolls back everything.

What moves & the pipeline

Dev SandboxIntegrationUAT / FullProduction

Metadata = Apex classes/triggers, LWC, Aura, Visualforce, objects, fields, validation rules, flows, permission sets, profiles, custom metadata. Records do not move via deployment.

Change Sets

  • Built-in; works only between related orgs (e.g. Sandbox → Production). Not between unrelated orgs.
  • Outbound = sending org (sandbox); Inbound = receiving org (production).
  • Steps: create → add components → upload → deploy. Always include dependencies (missing field → deploy fails).

Validation & Quick Deploy Favourite

  • Validation checks a deployment & runs tests without committing — safe, finds errors, no prod changes.
  • Quick Deploy is available only after a successful validation — skips re-running tests, faster to prod.

Test levels

Test levelRunsProduction?
NoTestRunNo testsNo (for Apex deploys)
RunSpecifiedTestsOnly selected testsYes
RunLocalTestsAll local tests (excludes managed-package tests)Yes — most common
RunAllTestsInOrgEvery test incl. managed packagesYes — slowest

Salesforce DX & Scratch Orgs

  • Modern, source-driven: CLI + Git + Scratch Orgs + Packages + CI/CD.
  • Scratch Org = temporary, disposable, created from a config file (source). Sandbox = long-lived, persistent, has org data.

Metadata API & ANT

  • Metadata API deploys/retrieves metadata — used by VS Code, CLI, ANT, CI/CD.
  • ANT Migration Tool = older, uses Metadata API. Know purpose.
  • Deletions via destructiveChanges.xml.

Package types

TypeNamespaceEditable?UpgradesUse
ManagedYesProtectedYesAppExchange
UnmanagedNoYesNoOpen-source / templates
UnlockedOptionalYesVersionedEnterprise / CI-CD

Deployment order & atomicity

  • Order by dependency: Objects → Fields → Validation Rules → Apex → Lightning → Permissions → Profiles.
  • Deployment is atomic: failure rolls back everything. No partial deploys.

Profiles vs Permission Sets & hardcoding

  • Profile = baseline, one per user. Permission Set = additive, many allowed. Salesforce recommends Permission Sets.
  • Never hardcode IDs like '001XXX...' — use SOQL, Custom Metadata, Custom Settings, or Named Credentials.
Deploy trapsChange Sets need related orgs · Quick Deploy needs a validation first · Scratch Orgs are temporary · Production can't use NoTestRun for Apex · deploy is atomic (no partial) · Unmanaged packages don't upgrade · 75% is the whole org.

Part 5 — Last-Minute Revision

Rapid-fire recall across the whole PD1 blueprint — read this in the final 15–20 minutes.

Governor limits

  • SOQL sync 100 / async 200 · DML 150
  • Future 50 · Queueable 50 · Batch default 200
  • Heap 6 MB sync / 12 MB async · CPU 10 s sync / 60 s async

Collections

  • List — ordered, duplicates OK
  • Set — unordered, unique
  • Map — key → value, fast lookup

SOQL vs SOSL

  • SOQL — one object (or related), structured, exact fields
  • SOSL — many objects, text search, FIND

Trigger context

  • Trigger.new — insert/update (not delete)
  • Trigger.old — update/delete (not insert)
  • Before = modify same record · After = related records / IDs

Order of execution (recognize before vs after)

ValidationBefore triggerAfter triggerAssignmentAuto-responseWorkflowProcess BuilderFlowsCommit

Access & keywords

  • private class · protected subclass · public namespace · global everywhere (managed pkg / web services)
  • static shared by all instances · final can't change/override/extend
  • this current object · super parent class

Annotations

  • @AuraEnabled — expose Apex to LWC/Aura
  • @future — async · @InvocableMethod — Flow
  • @isTest, @testSetup — testing · @TestVisible — expose private to tests

DML & partial success

  • insert list; → all-or-none (whole txn fails)
  • Database.insert(list, false)partial success (exam favourite)
  • Methods: insert, update, delete, upsert, merge, undelete

Security

  • OWD = baseline · Role hierarchy = vertical · Sharing rules = horizontal · Permission sets = extra
  • with sharing = record sharing · WITH USER_MODE = CRUD/FLS
  • Sharing ≠ CRUD/FLS

LWC quick hits

  • Decorators: @api public · @track reactive (mostly automatic now) · @wire reactive data
  • Lifecycle: constructor → connectedCallback → renderedCallback → disconnectedCallback → errorCallback
  • Parent→child = property/@api · child→parent = Custom Event
  • @wire = auto/cacheable · imperative = manual (button)
  • LDS — no Apex, uses UI API, respects security, caches

Relationships & automation

  • Lookup = optional · Master-Detail = required + cascade delete + roll-up summary
  • Roll-up (COUNT/SUM/MIN/MAX) works only on Master-Detail, not Lookup
  • Formula calculates; roll-up aggregates
  • Prefer Flow over Apex for simple automation; prefer Queueable for async

Bulkification

Wrong — DML in loop

for (Account a : accounts) {
    insert a;
}

Correct — bulkified

insert accounts;

Never put SOQL or DML in a loop. Bulkify with collections & maps.

Scenario → answer cheat table

Need…Use
Text search across objectsSOSL
Relationship querySOQL
Async (new dev)Queueable
Huge data volumeBatch
Time-based runSchedulable
Test a REST calloutHttpCalloutMock
Run as user contextSystem.runAs()
Reset governor limits (test)Test.startTest()
Execute async in testTest.stopTest()
Partial-success DMLDatabase.insert(list, false)
Roll-up summaryMaster-Detail
Child → parent (LWC)Custom Event
Parent → child (LWC)@api property
2-min cheatList=ordered · Set=unique · Map=key-value · Before=modify / After=IDs · SOQL=structured / SOSL=search · startTest=reset limits / stopTest=run async · Queueable>Future · 75%=whole org · Validation=no changes · Quick Deploy=after validation · Rollback=entire deploy.

Flashcards

Active recall

Answer in your head first, then reveal. Use "Reveal all" in the header to read straight through.