Salesforce Interview Prep

Module 6 — Testing & Test Classes

Interview weight: 8–12% (coverage rule, test isolation, mocking, async-testing patterns, negative tests — the "how do you test this?" follow-up appears after almost every Apex answer) · Estimated time: 4–6 sessions (~90 min each) Target: By the end, you can design a behavior-test suite (not a coverage game), explain test isolation and @TestSetup, mock callouts (HttpCalloutMock) and SOAP (WebServiceMock), test async code correctly (startTest/stopTest semantics), test security contexts (runAs), and write negative tests that assert errors instead of swallowing them — and you can say the 75% coverage rule accurately. Testing is where senior candidates show how they think about their own code: the interviewer's "how would you test that?" is a design question, not a trivia question.


M0 — THE MAP (read this first, 5–10 min)

The one idea everything hangs on: A TEST IS A QUESTION YOU ASK YOUR OWN CODE — AND THE ANSWER MUST BE AN ASSERTION, NOT A GREEN RUN

Every concept in this module — isolation, coverage, mocks, startTest/stopTest, runAs, negative tests — is a consequence of one realization:

Salesforce tests run in a pristine, isolated world: every test method gets a fresh database (no real data, no leftovers), the platform enforces a deployment gate (75% coverage for code going to production), and async code doesn't behave like it does in production unless you wrap it in Test.startTest/stopTest. A test that doesn't assert is a rumor. A suite that hits 75% by testing the wrong things is a false passport. The 3–4yr candidate knows the semantics of the test engine — isolation, timing, mocking, context — and designs tests like an engineer, not like a coverage farmer.

Think of it as an inspector's workshop:

  • The clean room = test isolation: each test method runs against a fresh, empty database — you create the data the test needs, and it vanishes after (rollback). No real data, no shared state (seeAllData defaults to false since API 29). Tests that "depend on data" are broken tests.
  • The prep bench = @TestSetup: runs once per class (before the first test method), shared by all methods in the class, rolled back between methods — the efficient way to build common record fixtures.
  • The time machine = Test.startTest() / Test.stopTest(): the first resets governor limits; the second flushes async work (queueables, batches, futures enqueued inside the window) and runs it synchronously — the ONLY way to reliably test async behavior.
  • The dummy vendor = HttpCalloutMock / WebServiceMock: your code never calls the real internet in tests — the mock stands in for the external system, including failure responses (this is how you test the retry paths from Module 3).
  • The undercover user = System.runAs: execute code as a specific user to test sharing/permission behavior — the security tests from Module 5, made executable.
  • The quota board = coverage: 75% aggregate (org-wide) is the deployment gate for code going to production — per-class, per-trigger, and org-wide averages all matter; and coverage is a floor, not a goal: behavior tests are the goal, coverage is the byproduct.
  • The verdict sheet = assertions: System.assertEquals, assertNotEquals, assert, and @isTest(expectedExceptions=...) — the test's whole job is a verifiable claim about behavior. A test with no assertion asserts nothing.

Why this map matters (the bridge): The interviewer's "how would you test that?" is asked after almost every Apex/trigger/integration answer. The senior answer has the shape: name the behavior → name the isolation/setup → name the timing (startTest/stopTest for async) → name the mock (for external) → name the assertion. Every incident in this module is a real org where a test suite looked healthy and wasn't:

  1. Coverage is a gate, not a goal — behavior tests are the goal.
  2. Tests run isolated — data, static state, and async behavior all have rules.
  3. Mocks are mandatory for external systems — including failure paths.
  4. startTest/stopTest is the async-testing key.
  5. runAs is the security-testing key.
  6. Negative tests assert errors — they never swallow them.

By the end of this module, "knowing it" looks like this: given any one of the 9 problems below, you can (a) name the mechanism that failed, (b) explain the fix on a whiteboard, (c) write the corrected test from memory, and (d) say which interview question it maps to.

#IncidentThe villain mechanism
1The Green Build That Deployed a BugCoverage game — behavior untested
2The Test That Failed on FridaysShared static state + date-dependent data
3The Callout Test That Called the InternetReal callouts in tests — no mock
4The 40-Minute Test That Deployed NothingBulk data + no startTest budget discipline
5The Async Test That Tested NothingAsync code tested outside startTest/stopTest
6The runAs Test That Ran as the Wrong UserSecurity context untested / misused runAs
7The Scheduled Job That Never FiredSchedulable testing gaps
8The Exception Test That Swallowed the BugNegative tests that don't assert
9🏆 Capstone — The Suite That Deployed the OrgThe multi-symptom test-failure postmortem

Protocol reminder (from file 00): attempt in writing FIRST (≥2 hypotheses + 2 solution attempts), hard 45-min cap, hint ladder, then reveal, then REDO, then retrieval drill. The sealed answer sheet lives in 06b_Topic06_Testing_TestClasses_Answer_Sheet.md. You are expected to fail. The failure is the task.


INCIDENT 1 — THE GREEN BUILD THAT DEPLOYED A BUG

STAKES

The org's coverage dashboard shows 87% — well above the 75% requirement. The release goes to production on Friday. Monday: the new Order trigger's discount-cascade logic is broken in production — discounts didn't cascade to line items for the exact scenario the business specified. The test class exists; it covers the trigger 100%; the build was green. The dev's defense: "We have 87% coverage — how could this ship a bug?" The answer: the test class asserts nothing. It inserts orders, calls the trigger path, and... ends. No System.assertEquals. The "coverage" is real; the testing never happened.

THE INCIDENT

// The "test class" — 100% coverage, zero assertions:
@isTest
private class OrderTriggerTest {
    @isTest
    static void testCascade() {
        Account a = new Account(Name = 'Acme'); insert a;
        Order o = new Order(AccountId = a.Id, Status = 'Draft'); insert o;
        // insert line items, apply a discount... and then nothing.
        // No assertions. The method ends. Coverage counted. Nothing tested.
    }
}

THE PROBLEM

Define coverage accurately (what the 75% number is, where it applies, how it's calculated), explain why this suite shipped a bug (the three failure modes of a coverage-only suite), and design the behavior test for the discount cascade (the assertion-first pattern: what to assert, on what, with which data).

Write: (1) the coverage definition, (2) the three failure modes, (3) the assertion-first test design.


HINT LADDER

  • Hint 1 (the avenue): (1) Coverage = the % of executable lines executed by test methods — a deployment gate for code going to production (75% org-wide aggregate; per-class/per-trigger averages matter for deployment, with exceptions for managed packages/plugins). It measures execution, not correctness. (2) Failure modes: tests that assert nothing (execute-only); tests that assert the process not the result (assert "insert succeeded" instead of "discounts applied"); tests that never test the business scenario (happy-path insert only). (3) Behavior test: given Order + line items + discount → assert the line-item discount fields after the trigger — the exact values, via a fresh query.
  • Hint 2 (the mechanism): (1) The 75% rule: at least 75% of your Apex code must be covered by tests to deploy to production (each class/trigger ≥75% for deployment, org-wide aggregate also tracked); exceptions: managed packages, plugins, and some system code. Coverage counts lines executed, not behaviors verified. 87% with zero assertions = 87% execution, 0% verification. (2) The three failure modes: (a) no assertions — the test passes if the code merely runs; (b) asserting the processSystem.assert(a.Id != null) proves the insert ran, not that the cascade is correct; (c) no scenario coverage — the business scenario (discount → line items) was never part of the fixture. (3) The fix: assertion-first design — build the fixture (Order + 2 line items + discount on Order), run the trigger path, re-query the line items, assert each expected value with System.assertEquals. If the assertion doesn't exist, the test is decoration.
  • Hint 3 (the skeleton):
@isTest
static void testDiscountCascadesToLineItems() {
    // fixture: Account, Order (Discount__c = 10), 2 line items (Qty, Price)
    insert ...;
    // trigger path already ran on insert
    List<OrderItem__c> items = [SELECT Discount__c FROM OrderItem__c WHERE Order__c = :o.Id];
    System.assertEquals(2, items.size());
    System.assertEquals(10, items[0].Discount__c);   // the assertion IS the test
    System.assertEquals(10, items[1].Discount__c);
}

THE REVEAL — POSTMORTEM

What actually happened (real class of incidents — the coverage-farmer org; the green build that shipped a bug; agent 06's traps: testing as a checkbox):

The coverage definition (say it precisely): Coverage is the percentage of executable lines of Apex run by test methods. It's a deployment gate: code going to production needs ≥75% coverage (the aggregate is org-wide; per-class and per-trigger coverage matter for deployment; managed packages and certain system/plugin code are exempt). It measures execution, not correctness — 87% means "87% of lines ran during tests," nothing more. The dev's defense ("87% — how could it ship a bug?") misunderstands the metric: coverage is a floor, not a goal; behavior tests are the goal, and coverage is the byproduct.

The three failure modes of a coverage-only suite (memorize — the "why did the green build fail" answer):

  1. No assertions (execute-only tests): the test runs the code and ends — the suite is green if the code merely runs. A broken discount cascade still executes lines; the trigger "covered" 100% while doing the wrong math.
  2. Asserting the process, not the result: System.assert(order.Id != null) proves the insert happened — not that the discount applied to line items. Process assertions verify the plumbing, not the behavior.
  3. No scenario coverage: the fixture never matched the business scenario (discount on Order → cascade to line items); happy-path inserts exercise the code without exercising the rule.

The assertion-first test design (the senior pattern): (1) Name the behavior first ("discount on the Order cascades to every line item"); (2) build the fixture that makes the behavior visible (Order with Discount__c = 10, 2 line items with prices); (3) let the trigger path run (inserts fire the trigger); (4) re-query the result (SELECT Discount__c FROM OrderItem__c WHERE Order__c = :o.Id — never trust in-memory objects the trigger may not have touched); (5) assert the exact expected values (System.assertEquals(10, items[0].Discount__c)). The rule: "A test without an assertion is a rumor." If a line of code can change behavior and every test still passes, that behavior is untested.

Why the "obvious fixes" failed (the contrast):

  • "Add more test methods to push coverage higher" → coverage isn't the disease; assertion-less tests are. More coverage farming makes the suite heavier and more misleading.
  • "Assert that no exceptions were thrown" → an exception-free run isn't a correct run; assert the values.
  • "The code reviewer can check the trigger" → review catches what a reviewer reads; the assertion catches what the code does — and catches it forever (regression).

KNOWLEDGE EXTRACTION (interview-ready)

  • "The 75% coverage rule?" → ≥75% of Apex lines must be covered by tests to deploy to production (org-wide aggregate; per-class/trigger matter for deployment; managed packages/plugins exempt). It's a gate, not a goal.
  • "What does coverage measure?" → Lines executed by tests — not correctness. Behavior tests are the goal; coverage is the byproduct.
  • "How do you test a trigger?" → Fixture → insert/update through the trigger path → re-query the result → assert exact expected values (never assert only "no exception" or "Id != null").
  • "Assert on what?" → The result: the field values, record counts, and side effects the behavior specifies — re-queried, not in-memory.

THE REDO

From memory: the coverage definition (3 clauses), the three failure modes, and the assertion-first pattern (5 steps).

RETRIEVAL DRILL

  1. The 75% rule — exactly what it gates, and two exemptions.
  2. What does coverage actually measure?
  3. The three failure modes of a coverage-only suite.
  4. Why re-query instead of trusting in-memory objects?
  5. The one-line rule about assertions.

INTERVIEW MAPPING

The "how do you test your triggers?" follow-up — asked after every trigger answer. The coverage-vs-behavior distinction is the senior opening; "assert the result, not the process" is the pattern.


INCIDENT 2 — THE TEST THAT FAILED ON FRIDAYS

STAKES

A test class passes Monday through Thursday. Friday it fails. The failure is a date assertion: System.assertEquals(30, daysOpen). The test "works" because it uses Date.today() to build its fixture — a 30-day-old record is created as Date.today() - 30... except the test ALSO relies on a static map populated by a helper and read by every test method — and on Fridays the calendar arithmetic plus the shared static state collide: the first test method to run mutates the static map, the second method sees a different map, and the date drift (weekend boundary) flips the assertion. The suite has become order-dependent and calendar-dependent — the two classic flaky-test diseases.

THE INCIDENT

@isTest
private class AgingTest {
    private static Map<Id, Integer> agingCache = new Map<Id, Integer>();  // shared static state

    @isTest static void testAging30Days() {
        Account a = new Account(Name = 'Acme' + Datetime.now().getTime());  // unique-ish
        insert a;
        Opportunity opp = new Opportunity(AccountId = a.Id, CloseDate = Date.today().addDays(-30), ...);
        insert opp;
        agingCache.put(a.Id, compute(opp));        // test 1 mutates the cache
        System.assertEquals(30, agingCache.get(a.Id));   // passes Mon–Thu...
    }
    @isTest static void testAgingWeekend() {
        // reads agingCache from test 1 (order-dependent!) + Date.today() arithmetic
        // ...fails when Date.today() - 30 lands on a weekend vs the CloseDate math
    }
}

THE PROBLEM

Name the two flaky-test diseases in this class (calendar dependence + shared static state), explain test isolation precisely (what resets between methods — data, static state, limits — and what @TestSetup does and doesn't share), and redesign the class: deterministic dates, per-method state, and the correct @TestSetup usage.

Write: (1) the two diseases, (2) the isolation rules, (3) the redesigned class.


HINT LADDER

  • Hint 1 (the avenue): (1) Diseases: (a) calendar-dependent fixturesDate.today() arithmetic breaks on weekend/date-boundary flips; (b) shared static state between test methods — static variables persist across test methods in a class (only database changes roll back), so method order matters. (2) Isolation: each test method runs in its own transaction — DML rolls back between methods, but static variables do NOT reset between methods — so shared statics = order-dependence. @TestSetup runs once per class before tests, its data is rolled back between methods, and it's shared by all methods. (3) Redesign: fixed dates (Date.newInstance(2026, 1, 15) — not today()), per-method state (no shared static cache — compute in each test), @TestSetup for the shared Account fixture (records, not statics).
  • Hint 2 (the mechanism): (1) Date.today() in fixtures = the test's truth is the calendar, which changes every day; a 30-day window crossing a weekend/month boundary flips System.assertEquals(30, ...). Deterministic dates (Date.newInstance(...)) make the test's truth fixed. (2) Salesforce test isolation: DML is rolled back between test methods, BUT static variables persist for the lifetime of the test class run — a static cache populated by test 1 is visible to test 2; run order (A–Z, not file order) decides the outcome → order-dependent suite. Also: no real data access by default (seeAllData=false since API 29), so fixtures must be created. (3) @TestSetup: runs once per class; the data it creates is rolled back between methods but available to all; use it for records, never for static state.
  • Hint 3 (the skeleton):
@isTest
private class AgingTest {
    @TestSetup static void setup() {
        insert new Account(Name = 'Acme');   // shared record fixture (rolled back between methods)
    }
    @isTest static void testAging30Days() {
        Account a = [SELECT Id FROM Account LIMIT 1];
        Date fixed = Date.newInstance(2026, 1, 15);            // deterministic, not today()
        insert new Opportunity(AccountId = a.Id, CloseDate = fixed.addDays(-30), ...);
        Integer days = compute([SELECT Id FROM Opportunity LIMIT 1]);   // no shared cache
        System.assertEquals(30, days);
    }
}

THE REVEAL — POSTMORTEM

What actually happened (real class of incidents — the Friday-failing suite; the calendar-dependent test; the order-dependent static cache):

Disease #1 — calendar dependence: the fixture used Date.today() and addDays(-30); the assertion expected exactly 30. The test's truth is the calendar — and calendar arithmetic near weekend/month boundaries (or DST/weekend drift in how "days open" is computed) flips the result on specific days. A test whose fixture depends on today() is a test whose truth changes daily. The fix: deterministic dates (Date.newInstance(2026, 1, 15)), so the fixture, the computation, and the assertion all agree forever.

Disease #2 — shared static state: Salesforce test isolation rolls back database changes between test methods — but static variables persist for the class's run. The static agingCache populated by test 1 was read by test 2; the A–Z method execution order decided which cache the second test saw → order-dependent. The fix: per-method state — compute within each test; never share statics between test methods. (This is the test-side twin of Module 1 Incident 2's static-flag recursion lesson: static state is the most dangerous thing in Apex, including inside tests.)

The isolation rules (say them precisely — the interview answer): (1) each test method runs in its own transaction; DML rolls back between methods; (2) no real data by default (seeAllData=false since API 29) — fixtures must be created; (3) static variables do NOT reset between methods — shared statics create order-dependence; (4) @TestSetup runs once per class, its data is rolled back between methods and shared by all — use it for record fixtures, never for statics; (5) Test.startTest/stopTest reset governor limits and flush async (Incident 5).

Why the "obvious fixes" failed (the contrast):

  • "Run the tests again — it'll pass" → the disease is deterministic (calendar + order); re-running just moves the failure day.
  • "Clear the static cache in each test" → clearing is a patch; not sharing state is the design (compute per method).
  • "Use @TestSetup for the cache"@TestSetup shares records; static state is still per-class-run state — same disease, different location.

KNOWLEDGE EXTRACTION (interview-ready)

  • "Test isolation — what resets between methods?" → Database changes roll back; static variables persist for the class run; no real data by default (seeAllData=false); limits reset at startTest.
  • "What does @TestSetup do?" → Runs once per class before the test methods; its records are available to all methods and rolled back between them; counts once against limits. For record fixtures — never for static state.
  • "Why do tests fail on specific days?" → Calendar-dependent fixtures (Date.today() arithmetic); use deterministic dates.
  • "Order-dependent tests?" → Static state shared between methods (execution is A–Z, not file order) — compute per method.

THE REDO

From memory: the two diseases + fixes, the five isolation rules, and the corrected class skeleton.

RETRIEVAL DRILL

  1. The two flaky-test diseases in the class.
  2. What rolls back between test methods — and what doesn't?
  3. What does @TestSetup share — and what must it never hold?
  4. Why is Date.today() in a fixture dangerous?
  5. seeAllData — default and meaning.

INTERVIEW MAPPING

The "why do tests fail intermittently / how do you write deterministic tests?" question — a favorite 3–4yr probe because it separates people who ran tests from people who designed them.


INCIDENT 3 — THE CALLOUT TEST THAT CALLED THE INTERNET

STAKES

A developer writes a test for the billing-sync class (Module 3's pattern). The test fails with a real CalloutException — because the code calls Http.send() and there's no mock — and, worse, on one occasion the test actually hit the real billing endpoint in a sandbox (an HttpCalloutMock was missing AND the environment allowed the call — 100+ real records were marked "synced" against the staging API). The class "has 90% coverage" (the sync method is covered); the test "fails"; the team's answer: "the internet is down." The actual answer: tests must never make real callouts — mocks are mandatory, and the failure path (timeouts, 500s) is exactly what mocks exist to test.

THE INCIDENT

@isTest
private class BillingSyncTest {
    @isTest static void testSyncSuccess() {
        // NO HttpCalloutMock. The sync method calls Http.send() for real:
        BillingSync.run();   // → CalloutException (no mock) — or, worse, a REAL call
        // Coverage counted on the exception path only.
    }
}

THE PROBLEM

State the mocking rule (why real callouts are forbidden in tests), name the mock classes (HttpCalloutMock + its siblings for REST, WebServiceMock for SOAP, Test.setMock), design the success-path test with the mock, and design the FAILURE-path test (timeout/500) — the one the Module 3 retry logic depends on.

Write: (1) the mocking rule + class names, (2) the success-path test, (3) the failure-path test.


HINT LADDER

  • Hint 1 (the avenue): (1) Rule: tests must never perform real callouts — Test.setMock(HttpCalloutMock.class, mock) replaces the HTTP layer; without a mock, Http.send throws CalloutException (or worse, calls the real endpoint). (2) Classes: HttpCalloutMock (interface → respond(HTTPRequest) returns HTTPResponse), StaticResourceCalloutMock (returns a static resource body), MultiStaticResourceCalloutMock (multiple endpoints), WebServiceMock (SOAP); Test.setMock registers. (3) Success path: build the mock response (200 + body) → run sync → assert records marked synced + the request's method/endpoint. Failure path: mock returns 500 (or a mock whose respond throws CalloutException for timeout simulation) → assert the retry/dead-letter behavior from Module 3 Incident 8.
  • Hint 2 (the mechanism): (1) When a class makes an HTTP call and no mock is set, the test fails with a callout error — the platform wants you to mock; Test.setMock is the registration point. The mock implements HttpCalloutMock.respond(HTTPRequest req) and returns the HTTPResponse — the test controls the whole conversation. (2) Success test: Test.setMock(HttpCalloutMock.class, new SuccessMock()) where SuccessMock.respond returns 200 with {'status':'ok'}BillingSync.run()System.assertEquals('Synced', [SELECT ...].Status__c) and assert the request details (endpoint, method, headers) via captured request. (3) Failure test: new FailureMock() returns 500 (or a mock that throws CalloutException in respond — the timeout simulation) → assert the retry counter incremented / dead-letter record created / the retry queueable enqueued within startTest/stopTest (Incident 5). The failure path is the test that would have caught the Module 3 blackout.
  • Hint 3 (the skeleton):
@isTest
static void testSyncSuccess() {
    Test.setMock(HttpCalloutMock.class, new SuccessMock());
    Test.startTest(); BillingSync.run(); Test.stopTest();
    System.assertEquals('Synced', [SELECT Status__c FROM Invoice__c].Status__c);
}
@isTest
static void testSyncFailureRetries() {
    Test.setMock(HttpCalloutMock.class, new TimeoutMock());  // throws CalloutException
    Test.startTest(); BillingSync.run(); Test.stopTest();
    // assert: retry counter = 1 (or dead-letter record on 3rd attempt)
}

THE REVEAL — POSTMORTEM

What actually happened (real class of incidents — the unmocked-callout test; the sandbox that "synced" 100 real records; the platform's mock-mandate):

The mocking rule (state it as a law): Tests must never perform real callouts. Salesforce's test engine forbids real HTTP in tests — Http.send without a registered mock throws CalloutException (the platform's way of enforcing the rule), and the "worse" case happens when the environment allows the call: the test actually hits the real endpoint, mutating real external state. Test.setMock(HttpCalloutMock.class, mock) is the mandatory registration — the test owns the entire conversation (request AND response), including the failure paths production will eventually hit.

The mock classes (know all four): (1) HttpCalloutMock — the interface: HTTPResponse respond(HTTPRequest request); the test implements it and returns whatever response the scenario needs (200, 500, empty body). (2) StaticResourceCalloutMock — returns the body of a static resource (for canned large payloads). (3) MultiStaticResourceCalloutMock — different static-resource bodies per endpoint. (4) WebServiceMock — for SOAP callouts (Test.setMock(WebServiceMock.class, ...)). All registered via Test.setMock.

The success-path test (design): set the mock (200 + body) → run the sync (inside startTest/stopTest if it enqueues async) → assert the result: the invoice's Status__c = 'Synced', and — the senior detail — capture the request in the mock and assert the endpoint, method, and headers the code actually sent (the test verifies the conversation, not just the outcome).

The failure-path test (the one that matters): the mock returns 500 (provider error) or throws CalloutException in respond (the timeout simulation — exactly Module 3 Incident 8's 3:00 AM blip). Assert the retry behavior: the retry counter incremented, the retry queueable enqueued (flushed by stopTest), and — after max attempts — the dead-letter record + alert. The failure test is the test that would have caught the Module 3 blackout before it happened — and saying so in an interview is the point.

Why the "obvious fixes" failed (the contrast):

  • "The internet is down" → the internet is never the test's problem; the missing mock is.
  • "Add a try/catch around the callout in the test" → the test then "passes" by swallowing the failure — the behavior is still untested (Incident 8's disease).
  • "Mock only the success path" → coverage passes; the retry/dead-letter logic — the whole Module 3 resilience story — remains untested until production.

KNOWLEDGE EXTRACTION (interview-ready)

  • "How do you test a callout?"Test.setMock(HttpCalloutMock.class, mock); the mock implements respond(HTTPRequest) returning HTTPResponse; never real calls. WebServiceMock for SOAP, StaticResourceCalloutMock for canned bodies.
  • "Test the failure path?" → Mock returns 500 or throws CalloutException in respond (timeout) → assert retry counter/dead-letter/alert. The failure test is the resilience test.
  • "What does Test.setMock do?" → Registers the mock for the test's duration; the HTTP layer is replaced by the mock's responses.
  • "Coverage with mocks?" → The mock covers the success AND failure branches — the failure branches are where coverage farming dies and real testing begins.

THE REDO

From memory: the mocking rule, the four mock classes, the success-path test (with request assertions), and the failure-path test (timeout → retry → dead-letter).

RETRIEVAL DRILL

  1. Why are real callouts forbidden in tests — and what does the engine do?
  2. The four mock classes + their registration method.
  3. What should the success test assert beyond "no exception"?
  4. How do you simulate a timeout with a mock?
  5. Which Module 3 incident does the failure-path test directly protect?

INTERVIEW MAPPING

"Your class calls an external API — how do you test it?" — the integration testing question. The mock-mandate + failure-path discipline is the complete answer; "Test.setMock" alone is the junior version.


INCIDENT 4 — THE 40-MINUTE TEST THAT DEPLOYED NOTHING

STAKES

A batch-class test "proves" the nightly rollup works. It inserts 10,000 parent records with 5 children each (50,000 DML statements in the fixture), runs Database.executeBatch WITHOUT startTest/stopTest — the batch runs in its own asynchronous context, the test waits 40 minutes, and then... the assertions are on records the test inserted itself (the rollup "works" because the fixture pre-set the values). The deploy times out (test execution ceiling), the team's answer is "the tests are too slow," and the real answer is a fixture-design + async-timing problem: the test creates a mountain of data to "look bulk," never uses startTest/stopTest, and asserts on the fixture instead of the batch's effect.

THE INCIDENT

@isTest
private class RollupBatchTest {
    @isTest static void testRollup() {
        // "bulk fixture": 10,000 parents × 5 children — 50,000 DML calls
        // ... 40 minutes of inserts ...
        Database.executeBatch(new RollupBatch());   // NO startTest/stopTest
        // ... assert against the fixture's pre-set values (not the batch's work) ...
    }
}

THE PROBLEM

Name the three design failures (fixture volume, missing startTest/stopTest, asserting on the fixture), state what Test.startTest/stopTest actually do (governor reset + async flush), design the correct bulk test (right-sized fixture, startTest window, assertions on the batch's effect), and answer the follow-up: what's the right way to "test bulk" without a 50,000-row fixture?

Write: (1) the three failures, (2) the startTest/stopTest semantics, (3) the corrected test.


HINT LADDER

  • Hint 1 (the avenue): (1) Failures: (a) fixture volume — 50,000 DML calls to "look bulk" instead of testing bulk semantics; (b) no startTest/stopTest — batch enqueued outside the window runs truly async (test can't reliably assert it; deploy-time risk); (c) asserting the fixture — the assertion passes because the test set the values itself, not because the batch computed them. (2) Semantics: Test.startTest() resets governor limits and marks the async window; Test.stopTest() executes all async work enqueued inside the window synchronously (batch, queueable, future) — so the batch runs, finishes, and its finish() runs — inside the test. (3) Correct design: right-sized fixture (a few parents × a few children — enough to exercise chunking semantics), executeBatch inside startTest/stopTest, assert on re-queried child values (the batch's computed rollup vs a manual SUM).
  • Hint 2 (the mechanism): (1) (a) Bulk semantics (chunking, QueryLocator, governor limits inside execute) are testable with a small fixture — the test exercises the batch's code paths, not its data volume; 50,000 rows make the test slow AND flaky (limits in fixture phase). (b) Test.startTest() resets the governor counters and opens the async window; Test.stopTest() flushes: every System.enqueueJob/executeBatch/@future inside the window executes synchronously before stopTest returns — batch finish() included. Without the window, the batch runs after the test ends (unreliable) or the test times out. (c) The fixture pre-setting the rollup values = asserting the fixture; the correct assertion compares the batch's result to an independently computed SUM. (3) Correct: insert 3 parents × 4 childrenTest.startTest(); Database.executeBatch(batch); Test.stopTest();System.assertEquals(manualSum, [SELECT ... FROM Parent].Rollup__c). Bulk-at-scale testing happens via the batch's own limit assertions (Limits.getDMLRows() style) in a separate small-volume test.
  • Hint 3 (the skeleton):
@isTest
static void testRollupComputes() {
    Account p = new Account(Name='P'); insert p;
    insert new List<Child__c>{ new Child__c(Parent__c=p.Id, Amount__c=100),
                               new Child__c(Parent__c=p.Id, Amount__c=200) };
    Test.startTest();
    Database.executeBatch(new RollupBatch(), 200);
    Test.stopTest();                          // batch + finish() run synchronously
    Account result = [SELECT Rollup__c FROM Account WHERE Id = :p.Id];
    System.assertEquals(300, result.Rollup__c);   // batch's effect, not the fixture
}

THE REVEAL — POSTMORTEM

What actually happened (real class of incidents — the overweight fixture; the batch test that "proved" nothing in 40 minutes; the deploy-timeout suite):

The three design failures (name them like a verdict):

  1. Fixture volume as a substitute for semantics: 50,000 DML calls don't test bulk behavior — they test the platform's patience. Bulk semantics (QueryLocator iteration, chunking, governor limits inside execute) are exercised by a small fixture; volume only adds runtime and flakiness. "Testing bulk" means testing the bulk code paths, not shipping a mountain of rows.
  2. Missing startTest/stopTest: the batch enqueued outside the window runs in its own async context — the test can't reliably assert it, the suite times out (deploy ceiling), or the test exits before the batch finishes ("passed" by exiting early).
  3. Asserting the fixture, not the effect: the rollup values were pre-set by the fixture — the assertion validated the fixture. The test's job is to compare the batch's computed result with an independent expectation (a manual SUM).

The startTest/stopTest semantics (memorize — the async-testing key): Test.startTest() resets governor limits and opens the async window; Test.stopTest() executes all async work enqueued inside the window synchronously — futures, queueables, batches (including finish()), and scheduled jobs scheduled inside the window. This is why the pattern is: enqueue inside the window, assert after stopTest. Without the window: async code runs after the test ends — untested, or the suite waits forever.

The corrected bulk test (the pattern): right-sized fixture (3 parents × 4 children — enough to exercise chunking), executeBatch inside startTest/stopTest, and assertions on re-queried records: the batch's rollup vs an independently computed SUM (System.assertEquals(300, result.Rollup__c)). For genuine "does it handle 200-record scopes": a second small test asserts the batch's limit behavior (Limits.getDMLRows() within execute), not 50,000 rows.

Why the "obvious fixes" failed (the contrast):

  • "Reduce the deploy timeout" → the timeout is a symptom; the fixture design is the disease.
  • "Run the batch synchronously with a flag" → production code shouldn't bend for tests; startTest/stopTest IS the mechanism.
  • "Assert the children exist" → process assertion again (Incident 1's disease) — assert the computed value.

KNOWLEDGE EXTRACTION (interview-ready)

  • "Test.startTest()/stopTest()?" → startTest resets governor limits and opens the async window; stopTest synchronously executes async work enqueued inside (futures, queueables, batches + finish, scheduled). The async-testing key.
  • "How do you test a batch?" → Small fixture → executeBatch inside startTest/stopTest → assert on re-queried computed values vs an independent expectation.
  • "How do you test bulk without huge fixtures?" → Test the bulk code paths (chunking, limits in execute) with small fixtures; assert limit behavior, not row volume.
  • "Why does the batch test time out?" → Async work outside the window runs after the test — wrap it in startTest/stopTest and it runs synchronously.

THE REDO

From memory: the three failures, the startTest/stopTest semantics, and the corrected batch test skeleton.

RETRIEVAL DRILL

  1. The three design failures of the overweight fixture.
  2. What does startTest do — and what does stopTest flush?
  3. Does the batch's finish() run in the test? (When?)
  4. "Testing bulk" = testing what, exactly?
  5. The assertion in the corrected test compares what vs what?

INTERVIEW MAPPING

"How do you test async/batch code?" — the standard follow-up after any batch answer (Module 1 Incident 4). The startTest/stopTest semantics + right-sized fixture is the complete answer.


INCIDENT 5 — THE ASYNC TEST THAT TESTED NOTHING

STAKES

The queueable from Module 3 (the invoice sync) needs a test. The developer writes: BillingSync.enqueue(...);outside startTest/stopTest — then immediately asserts System.assertEquals(0, [SELECT COUNT() FROM Invoice__c WHERE Synced__c = true]). The test passes — because the queueable hasn't run yet (it executes AFTER the test ends). The suite is green, "covers" the queueable (the enqueue line executed) — but the behavior (records actually synced) has never run in a test. The team celebrates 92% coverage. The invoice sync is broken in production (Module 3 Incident 8's blackout, again).

THE INCIDENT

@isTest
private class BillingSyncTest {
    @isTest static void testSync() {
        // enqueue OUTSIDE the startTest window:
        BillingSync.enqueue(invoiceIds);
        System.assertEquals(0, [SELECT COUNT() FROM Invoice__c WHERE Synced__c = true]);
        // "passes" — the queueable hasn't run yet (runs after the test). Nothing verified.
    }
}

THE PROBLEM

Explain exactly when enqueued async work executes relative to the test's end (the two failure modes: executes-after-test vs never-observable), state the rule for testing any async construct (future/queueable/batch/scheduled), and write the corrected test — plus the two extra things the corrected test needs from Module 3: the callout mock and the retry/dead-letter assertions.

Write: (1) the async-execution timing rules, (2) the corrected test, (3) the Module 3 tie-ins (mock + retry assertions).


HINT LADDER

  • Hint 1 (the avenue): (1) Async work enqueued OUTSIDE the window executes after the test method completes — the test can't observe it (asserts pre-execution state, or the work runs in a context the test never sees). Enqueued INSIDE startTest/stopTest → executes synchronously at stopTest → observable. (2) The rule: every async construct is tested inside the startTest/stopTest window, with assertions AFTER stopTest. (3) Module 3 tie-ins: the queueable calls out → Test.setMock(HttpCalloutMock.class, ...) (Incident 3); the retry path → a failure mock → assert retry counter/dead-letter.
  • Hint 2 (the mechanism): (1) Timing: async work enqueued outside the window runs after the test ends (the test asserts a world where the work hasn't happened — the green "0 synced" assertion is the tell). Enqueued inside the window, stopTest flushes it synchronously — the queueable's execute (and everything it chains) runs before the next line. (2) The rule: enqueue inside startTest; assert after stopTest. Same for @future (invoke inside the window), Database.executeBatch (Incident 4), System.schedule (Incident 7), platform-event-triggered flows (invoke the flow path inside the window). (3) The corrected test: Test.setMock(HttpCalloutMock.class, new SuccessMock()); Test.startTest(); BillingSync.enqueue(ids); Test.stopTest(); System.assertEquals(2, [SELECT COUNT() ... Synced__c = true]); — and the failure variant (TimeoutMock) asserting the retry counter = 1 and, on the 3rd attempt, the dead-letter Error_Log__c row.
  • Hint 3 (the skeleton):
@isTest
static void testSyncSuccess() {
    Test.setMock(HttpCalloutMock.class, new SuccessMock());
    Test.startTest();
    BillingSync.enqueue(new List<Id>{ inv.Id, inv2.Id });
    Test.stopTest();                            // queueable executes NOW
    System.assertEquals(2, [SELECT COUNT() FROM Invoice__c WHERE Synced__c = true]);
}
@isTest
static void testSyncTimeoutRetries() {
    Test.setMock(HttpCalloutMock.class, new TimeoutMock());   // throws CalloutException
    Test.startTest(); BillingSync.enqueue(ids); Test.stopTest();
    System.assertEquals(1, [SELECT Retry_Count__c FROM Invoice__c WHERE Id = :inv.Id].Retry_Count__c);
}

THE REVEAL — POSTMORTEM

What actually happened (real class of incidents — the green async test that verified nothing; the "92% coverage" org with a broken sync):

The async timing rules (memorize — the #1 async-testing question): Async work enqueued outside the startTest/stopTest window executes after the test method completes — the test can only observe the world before the work ran (the tell: the "0 synced" assertion passing). Enqueued inside the window, Test.stopTest() flushes it synchronously: the queueable's execute runs before stopTest returns, and everything it chains (including nested enqueues up to the depth limit) runs too. The rule: enqueue inside startTest; assert after stopTest. This applies to every async construct: @future (invoke inside the window), Database.executeBatch (+ finish() — Incident 4), System.enqueueJob, System.schedule (Incident 7), and platform-event-triggered flows.

The two failure modes (name them): (1) executes-after-test — the queueable runs post-test; the test asserted pre-execution state and "passed" while verifying nothing (the org's 92% was real coverage of the enqueue line, not the sync behavior); (2) never-observable — the work's effects land where no assertion can see them, and the next production incident is the test's first run. Both are cured by the same window discipline.

The corrected test + Module 3 tie-ins: (1) the mock — the queueable calls out, so Test.setMock(HttpCalloutMock.class, ...) is mandatory (Incident 3's rule); (2) the assertions after stopTest — the invoice rows actually marked Synced__c = true (re-queried, exact counts); (3) the failure test — a TimeoutMock (throws CalloutException) → assert Retry_Count__c = 1 after the flush; and on max attempts → the dead-letter Error_Log__c row (Module 3 Incident 8's design, now verified). The suite that would have caught the 3:00 AM blackout: success path + timeout path + dead-letter path — each inside the window, each with assertions after.

Why the "obvious fixes" failed (the contrast):

  • "Add more coverage" → the enqueue line was already covered; coverage was never the gap.
  • "Sleep in the test" → sleeping doesn't flush async; stopTest does (and sleep isn't allowed in tests).
  • "Test the sync method directly instead of the queueable" → the direct call tests the logic but not the enqueue→execute path — the window pattern tests both.

KNOWLEDGE EXTRACTION (interview-ready)

  • "How do you test a queueable/future/batch?" → Enqueue/invoke inside Test.startTest(), assert after Test.stopTest() — the window flushes async work synchronously.
  • "What happens if you enqueue outside the window?" → The work executes after the test ends — unobservable; the test can pass while verifying nothing.
  • "Async + callouts?" → Same window + Test.setMock(HttpCalloutMock.class, ...); the failure mock (500/timeout) verifies the retry/dead-letter behavior.
  • "Does stopTest flush nested chaining?" → Yes — everything enqueued inside the window, including chained queueables, executes synchronously at stopTest.

THE REDO

From memory: the two failure modes, the window rule, and the corrected success + failure tests (with mock and retry assertions).

RETRIEVAL DRILL

  1. When does async work enqueued outside the window execute?
  2. The one-line rule for testing async code.
  3. What does the "0 synced" assertion reveal about the broken test?
  4. The Module 3 tie-ins in the corrected test (name both).
  5. Does stopTest flush chained queueables?

INTERVIEW MAPPING

The async-testing follow-up — asked after every queueable/batch/future answer (Modules 1 and 3). The window pattern + the "green but untested" diagnosis is the senior answer.


INCIDENT 6 — THE runAs TEST THAT RAN AS THE WRONG USER

STAKES

A security-critical class (Module 5's search) gets its test. The developer writes: query as a rep... but forgets System.runAs — the test runs as the test-running user (a system/admin context), so the sharing behavior is never actually exercised. The test asserts System.assertEquals(1, results.size()) in a world where the running user is the admin — green, and meaningless. Production shows the rep seeing 40 accounts (Module 5 Incident 1 — again). The fix: System.runAs(user) — create a rep user in the test, run the query under that user, assert the rep's view.

THE INCIDENT

@isTest
private class SearchTest {
    @isTest static void testRepSeesOnlyOwned() {
        // NO runAs — runs as the test-running user (admin context):
        List<Account> results = AccountSearch.find('Acme');
        System.assertEquals(1, results.size());   // passes under admin; meaningless for sharing
    }
}

THE PROBLEM

Explain who the test runs as by default and why security tests REQUIRE System.runAs (with the constraints: what a runAs user needs — Profile/PermissionSet assignment — and what runAs does NOT change), design the corrected security test (rep user creation, runAs block, sharing assertion), and add the second security-test pattern from Module 5: the FLS assertion (what the class returns vs what the profile allows).

Write: (1) the default-context trap, (2) the corrected sharing test, (3) the FLS assertion pattern.


HINT LADDER

  • Hint 1 (the avenue): (1) Tests run as the test-running user (system context — full access) unless wrapped in System.runAs(user); sharing/FLS behavior is invisible under that context. runAs runs a block of code as the given user (needs a real user record with a Profile). (2) Corrected: create a rep (User with a standard profile), insert records owned by rep A and rep B, run the search inside System.runAs(repA), assert rep A sees only A's records. (3) FLS pattern: query fields via the class inside runAs(repA) with Security.stripInaccessible — assert the sensitive fields are absent.
  • Hint 2 (the mechanism): (1) Default: tests execute in system context — sharing and FLS are not enforced for the test-running user (they see everything). A sharing test without runAs asserts the admin's view, not the rep's — green and meaningless. System.runAs(someUser) { ... } runs the block under that user's context — sharing rules, FLS, and permissions apply. Constraints: the user must be created in the test (or fetched) and must exist when runAs is called; the user needs a Profile (a standard profile, not system admin) and may need PermissionSets for the objects. (2) Corrected: User repA = new User(ProfileId = standardProfile, ...); insert repA;Account a = new Account(Name='Acme', OwnerId = repA.Id); insert a; plus an account owned by another user → System.runAs(repA) { List<Account> r = AccountSearch.find('Acme'); System.assertEquals(1, r.size()); } — the sharing behavior, actually exercised. (3) FLS: inside runAs(repA), call the class and assert sensitive fields are stripped (null) — the Module 5 Incident 2 enforcement, now as a test.
  • Hint 3 (the skeleton):
@isTest
static void testRepSeesOnlyOwned() {
    User repA = TestUtil.createRep();            // standard profile user
    Account mine = new Account(Name='Acme-1', OwnerId = repA.Id); insert mine;
    Account others = new Account(Name='Acme-2', OwnerId = otherUser.Id); insert others;
    System.runAs(repA) {
        List<Account> r = AccountSearch.find('Acme');
        System.assertEquals(1, r.size());            // sharing actually exercised
        System.assertEquals('Acme-1', r[0].Name);
    }
}

THE REVEAL — POSTMORTEM

What actually happened (real class of incidents — the system-context security test; Module 5 Incident 1's test-side twin):

The default-context trap: tests execute as the test-running user — in practice a system/admin context where sharing and FLS are not enforced. A security test without runAs asserts the admin's view: green, and meaningless for the security behavior. Module 5's lesson ("Apex bypasses filters unless declared") has a test-side twin: the test context bypasses filters unless you declare the user. System.runAs(user) { ... } runs the block under the given user's context — sharing rules, FLS, and permissions apply exactly as they would for that user in production.

The constraints (know them): the runAs user must be a real user record created in the test (or fetched); give it a standard profile (not system admin) and any needed PermissionSets; then insert records with OwnerId = user.Id to control ownership. runAs does NOT change: the user's license limits (some system operations still run system-side), and it doesn't bypass test isolation (real data still isn't visible). And remember: runAs requires the user to exist — create it first.

The corrected security test (the pattern): (1) create the rep user (standard profile); (2) create records with controlled ownership (one owned by the rep, one by another user); (3) wrap the class invocation in System.runAs(repA); (4) assert the rep's view: System.assertEquals(1, results.size()) and the specific record — the sharing behavior is now actually exercised. The same pattern tests: with sharing vs without sharing classes, sharing rules, and the Module 5 two-layer fix (keyword + FLS).

The FLS assertion pattern (Module 5 Incident 2, as a test): inside runAs(repA), call the class and assert the sensitive fields are stripped (the Security.stripInaccessible behavior): System.assertEquals(null, result.SSN__c) — or, with WITH USER_MODE, assert the query returns the FLS-filtered shape. Security tests assert the user's result, not the code's result.

Why the "obvious fixes" failed (the contrast):

  • "The test passes, the class must be fine" → the test asserted the admin's view; the rep's view was never computed.
  • "Add with sharing to the class" → necessary, but the test still doesn't verify it — runAs is the verification.
  • "Test the sharing manually in Setup" → manual checks don't run in CI; the runAs test runs forever.

KNOWLEDGE EXTRACTION (interview-ready)

  • "Who runs your tests?" → The test-running user (system context) unless System.runAs(user) wraps the code — runAs enforces the given user's sharing/FLS.
  • "How do you test sharing behavior?" → Create a standard-profile user + controlled-ownership records → System.runAs(user) { ... } → assert the user's view (counts + specific records).
  • "Test FLS?" → Inside runAs, assert sensitive fields are stripped by Security.stripInaccessible / WITH USER_MODE.
  • "runAs constraints?" → The user must exist (create in test); standard profile + needed PermissionSets; doesn't bypass isolation.

THE REDO

From memory: the default-context trap, the runAs constraints, the corrected sharing test skeleton, and the FLS assertion pattern.

RETRIEVAL DRILL

  1. Who executes test code by default — and what does that hide?
  2. What must a runAs user have (3 things)?
  3. What does runAs NOT change?
  4. The 4-step corrected security test.
  5. How do you assert FLS behavior in a test?

INTERVIEW MAPPING

The "how do you test security?" follow-up after any sharing/CRUD-FLS answer (Module 5). The runAs pattern + "assert the user's view" is the senior answer.


INCIDENT 7 — THE SCHEDULED JOB THAT NEVER FIRED

STAKES

The nightly invoice reminder (a Schedulable class, Module 1/3 pattern) needs a test. The developer writes: System.schedule('Reminder', cron, new ReminderJob()); then asserts System.assertEquals(1, [SELECT COUNT() FROM EmailMessage ...]) — the assert runs immediately, before the scheduled job fires (scheduled work doesn't run in tests unless scheduled inside the startTest/stopTest window). The test passes (0 emails — asserted 1? No: the developer asserts the cron trigger exists: System.assert(Test.getCronTrigger('Reminder') != null) — which proves the schedule was created, not that the job runs). Coverage: the execute method of the schedulable is never executed — so in production the job's logic (the actual email logic) is untested, and when the schedule fires on night 1, the email logic fails. "The schedule exists" ≠ "the job works."

THE INCIDENT

@isTest
private class ReminderJobTest {
    @isTest static void testSchedules() {
        System.schedule('Reminder', '0 0 2 * * ?', new ReminderJob());
        System.assert(Test.getCronTrigger('Reminder') != null);
        // Proves the schedule was CREATED — the job's execute() never ran.
        // The email logic is untested. In production it fails on night 1.
    }
}

THE PROBLEM

Explain what the assertion actually proves (cron creation vs job execution), the correct pattern to test a schedulable (schedule inside the window + Test.getCronTrigger + assert the job's EFFECT after stopTest), and the two-leg design: a test for scheduling mechanics and a test for the job's logic — with the mock requirement if the job calls out (Module 3 tie-in).

Write: (1) the false-proof diagnosis, (2) the correct schedulable-test pattern, (3) the two-leg design.


HINT LADDER

  • Hint 1 (the avenue): (1) Test.getCronTrigger proves the schedule row exists, NOT that execute ran. Scheduled jobs execute during stopTest ONLY when scheduled inside the window — and then you assert the job's effect. (2) Pattern: Test.startTest(); String jobId = System.schedule('Reminder', cron, new ReminderJob()); Test.stopTest(); System.assert(Test.getCronTrigger(jobId) != null); System.assertEquals(1, [SELECT COUNT() FROM EmailMessage ...]); — schedule inside, assert after. (3) Two-leg design: (a) scheduling-mechanics test (cron exists, no overlap), (b) logic test — call new ReminderJob().execute(null) directly OR let the window flush it, then assert the effect; mock if it calls out.
  • Hint 2 (the mechanism): (1) System.schedule in a test creates the cron trigger row immediately — Test.getCronTrigger finds it — but the scheduled execution happens at the scheduled time, NOT during the test (unless inside the window). The assertion "cron exists" is true and worthless for the job's behavior. (2) Inside the window: Test.startTest(); System.schedule(...); Test.stopTest();stopTest executes the scheduled job synchronously; the job's execute runs; assertions after observe its effect. This mirrors Incident 4/5's flush semantics — scheduled work is async work. (3) Two legs: leg 1 = mechanics (Test.getCronTrigger(jobId) != null + schedule-time assertions); leg 2 = behavior — the job's execute runs (via the window flush or a direct execute(null) call) and the effect is asserted (emails/records). If the job calls out (Module 3), Test.setMock (Incident 3) applies inside leg 2.
  • Hint 3 (the skeleton):
@isTest
static void testReminderSchedulesAndFires() {
    Test.startTest();
    String jobId = System.schedule('Reminder', '0 0 2 * * ?', new ReminderJob());
    Test.stopTest();                       // scheduled job executes NOW
    System.assert(Test.getCronTrigger(jobId) != null);   // mechanics
    System.assertEquals(1, [SELECT COUNT() FROM EmailMessage ...]);  // behavior — the job's effect
}

THE REVEAL — POSTMORTEM

What actually happened (real class of incidents — the schedule-creation test; the job that failed on night 1 because its logic never ran in tests):

The false-proof diagnosis: Test.getCronTrigger('Reminder') != null proves the schedule row was created — nothing about the job's execute logic. System.schedule in a test creates the cron entry immediately; the execution happens at the scheduled time, outside the test — unless scheduled inside the startTest/stopTest window, where stopTest flushes it synchronously (same rule as Incidents 4 and 5: scheduled work is async work). The org's coverage dashboard showed the schedulable covered (the schedule call executed) while the job's execute — and its email logic — ran zero times in tests. Production night 1 was the test's first run.

The correct pattern (memorize): (1) schedule inside the window: Test.startTest(); String jobId = System.schedule(...); Test.stopTest();stopTest executes the scheduled job synchronously; (2) assert mechanics after: System.assert(Test.getCronTrigger(jobId) != null); (3) assert behavior after: the job's effect — the emails/records the job was supposed to produce. The jobId from System.schedule feeds Test.getCronTrigger(jobId) — assert on the returned ID, not the name (names can collide).

The two-leg design (say it like a checklist): Leg 1 — scheduling mechanics: the cron trigger exists, is scheduled at the right time, and schedule returned an ID (repeated schedules of the same class are prevented by the platform — assert no overlap). Leg 2 — job behavior: the execute logic actually runs and produces its effect — either via the window flush or a direct new ReminderJob().execute(null) call — and the effect is asserted (emails sent, records updated). If the job calls out (Module 3's nightly sync), leg 2 needs Test.setMock(HttpCalloutMock.class, ...) — the mock requirement travels everywhere (Incident 3).

Why the "obvious fixes" failed (the contrast):

  • "The cron trigger exists — it works" → existence ≠ execution; the schedule is the appointment, not the work.
  • "Test the email logic separately" → correct instinct; but the scheduling→execute path (the window flush) is what connects them — test both legs.
  • "Just wait for it to run" → tests don't wait for real time; the window IS the time machine.

KNOWLEDGE EXTRACTION (interview-ready)

  • "How do you test a schedulable?"Test.startTest(); String jobId = System.schedule(...); Test.stopTest(); — stopTest executes the job; assert Test.getCronTrigger(jobId) (mechanics) + the job's effect (behavior).
  • "What does Test.getCronTrigger prove?" → The schedule row exists — not that the job ran. Assert behavior after stopTest.
  • "Scheduled jobs + callouts?" → Same window + Test.setMock inside the job's execute path.
  • "Two-leg design?" → Leg 1: scheduling mechanics. Leg 2: job behavior (execute runs + effect asserted).

THE REDO

From memory: the false-proof diagnosis, the correct window pattern, and the two-leg design.

RETRIEVAL DRILL

  1. What does Test.getCronTrigger actually prove?
  2. When does a scheduled job execute in a test?
  3. Why assert on jobId instead of the name?
  4. The two legs of a schedulable test.
  5. Which Module does the mock requirement tie back to?

INTERVIEW MAPPING

The "how do you test a scheduled job?" follow-up after any schedulable answer (Modules 1/3). The mechanics-vs-behavior split is the senior answer.


INCIDENT 8 — THE EXCEPTION TEST THAT SWALLOWED THE BUG

STAKES

A negative test for the invoice-sync retry logic (Module 3/Incident 5's design): the developer writes a test that calls BillingSync.run() with a failing mock and wraps the call in try/catchcatch (Exception e) { /* expected */ } — and the test passes. The suite is green. But the retry logic is broken in production: the code should catch the CalloutException, increment the retry counter, and enqueue a retry — and it doesn't (the exception propagates and the sync dies). Why did the test pass? Because the try/catch swallowed the exception AND there was no assertion. The test "verified" that an exception was thrown (the code crashed) — which is the OPPOSITE of the behavior the retry logic specifies. The bug: the test asserted nothing about the retry outcome.

THE INCIDENT

@isTest
private class BillingSyncTest {
    @isTest static void testRetryOnFailure() {
        Test.setMock(HttpCalloutMock.class, new TimeoutMock());
        try {
            BillingSync.run();
        } catch (Exception e) {
            // "expected" — swallowed. No assertion.
        }
        // Test passes whether the code retried, crashed, or did nothing.
    }
}

THE PROBLEM

Diagnose why this test is worse than no test (the three reasons: no assertion, exception-swallowing, and asserting the wrong thing), present the two correct negative-test patterns (@isTest(expectedExceptions=...) for code that SHOULD throw, and result-assertions for code that should HANDLE the failure — the retry case), and write the corrected retry test: mock failure → assert retry counter + enqueued retry (in the window) + dead-letter on max attempts.

Write: (1) the diagnosis, (2) the two patterns + when each applies, (3) the corrected retry test.


HINT LADDER

  • Hint 1 (the avenue): (1) The test passes because: no assertion (try/catch ended silently), the exception was swallowed (the test's own catch), and the "expected" comment asserted nothing — the test verifies only "something threw," which the retry logic should never allow (it should catch and retry). (2) Two patterns: (a) @isTest(expectedExceptions = SomeException.class) — for code that SHOULD throw (validation, guard clauses) — the annotation IS the assertion; (b) behavior assertions — for code that should HANDLE failures: run inside the window, assert the retry counter/dead-letter — "the failure was handled" is the assertion. (3) Corrected: TimeoutMock → Test.startTest(); BillingSync.run(); Test.stopTest(); → assert Retry_Count__c = 1 + the retry queueable's effect (or dead-letter Error_Log__c on attempt 3).
  • Hint 2 (the mechanism): (1) Three reasons the test is worse than none: (a) no assertion — the test body ends in the catch; green = "nothing failed the runner," not "behavior correct"; (b) exception-swallowing — the test's own try/catch hides whether the code under test handled the failure — the test passes whether the code crashed, retried, or did nothing; (c) wrong expectation — "an exception was thrown" is the opposite of the retry contract (the code must catch, increment, enqueue). The retry logic was never verified; production was the first run. (2) Pattern choice: expectedExceptions = the code SHOULD throw (input validation, guard conditions) — the annotation fails the test if NO exception is thrown; behavior assertions = the code SHOULD HANDLE (retries, dead-letter, fallback) — assert the state after the window. The retry case is a handling test, not a throwing test. (3) Corrected test: failure mock → window → assert Retry_Count__c = 1 (the counter incremented), the retry queueable executed (flush), and on the 3rd attempt → the dead-letter Error_Log__c row — the complete Module 3 Incident 8 contract, verified.
  • Hint 3 (the skeleton):
@isTest
static void testRetryOnTimeout() {
    Test.setMock(HttpCalloutMock.class, new TimeoutMock());
    Test.startTest();
    BillingSync.run();                       // code should CATCH and retry
    Test.stopTest();
    System.assertEquals(1, [SELECT Retry_Count__c FROM Invoice__c WHERE Id = :inv.Id].Retry_Count__c);
    // max-attempt variant: assert Error_Log__c dead-letter row + alert
}
// Pattern 2 (code SHOULD throw):
@isTest(expectedExceptions = CalloutException.class)
static void testInvalidInputThrows() {
    BillingSync.run(badInput);               // must throw — annotation asserts it
}

THE REVEAL — POSTMORTEM

What actually happened (real class of incidents — the exception-swallowing test; the green suite with a dead retry path; the "test passed, production crashed" paradox):

The diagnosis (three reasons — memorize): (1) No assertion: the test body ended inside the catch; green meant "the test runner saw no failure," not "the behavior is correct." (2) Exception-swallowing: the test's own try/catch intercepted the exception — the runner never saw it, and — the deeper problem — the test cannot distinguish "the code crashed" from "the code handled the failure correctly" because both look identical from inside the catch. (3) Asserting the wrong thing: "an exception was thrown" is the opposite of the retry contract — the code must catch the CalloutException, increment the counter, and enqueue a retry. The test's implicit expectation (the exception escapes) describes exactly the broken behavior. The retry path was never exercised; production was its first run.

The two correct negative-test patterns (know when each applies):

  1. @isTest(expectedExceptions = SomeException.class) — for code that SHOULD throw: validation failures, guard clauses, malformed input. The annotation IS the assertion: the test fails if no exception is thrown. This is the throwing pattern.
  2. Behavior assertions — for code that SHOULD HANDLE a failure: retries, dead-letter, fallbacks. Run the code (in the window if async), then assert the handling state: the retry counter, the enqueued retry, the dead-letter row. This is the handling pattern — and the retry case is a handling test, not a throwing test.

The corrected retry test (the full Module 3 Incident 8 contract): failure mock (TimeoutMock throws CalloutException) → Test.startTest(); BillingSync.run(); Test.stopTest(); → assert Retry_Count__c = 1 (the counter incremented) and the retry queueable's effect (flushed by stopTest); then the max-attempt variant: three failures → assert the dead-letter Error_Log__c row and the alert path. Every clause of the resilience design is now a verifiable claim.

Why the "obvious fixes" failed (the contrast):

  • "Add a comment saying it's expected" → comments don't assert; the annotation or a result-assertion does.
  • "Re-throw in the catch" → converts a swallowing test into a throwing test — wrong for a handling contract.
  • "Test that it throws" → that's the broken behavior; test that it handles.

KNOWLEDGE EXTRACTION (interview-ready)

  • "Negative tests — the two patterns?"@isTest(expectedExceptions=...) for code that SHOULD throw; behavior assertions (counter/dead-letter/fallback state) for code that should HANDLE failures. Never swallow exceptions in a test.
  • "Why is try/catch in a test dangerous?" → It hides whether the code under test crashed or handled the failure — and without an assertion it verifies nothing.
  • "How do you test the retry logic?" → Failure mock → window → assert retry counter + enqueued retry + dead-letter on max attempts — the handling state IS the assertion.
  • "Test with Database.insert(records, false)? → For DML partial failures: assert the SaveResult errors per record (the handling pattern applied to DML).

THE REDO

From memory: the three-reason diagnosis, the two patterns with when each applies, and the corrected retry test (counter + dead-letter).

RETRIEVAL DRILL

  1. The three reasons the swallowing test is worse than none.
  2. Which pattern for code that SHOULD throw — and what does the annotation do?
  3. Which pattern for code that should HANDLE — what is the assertion?
  4. What does the corrected retry test assert (3 clauses)?
  5. Why is "test that it throws" the wrong expectation for a retry contract?

INTERVIEW MAPPING

The negative-testing question — "how do you test failure paths?" The two-pattern answer + "never swallow" is the complete senior response; try/catch-with-no-assertion is the anti-pattern every interviewer is listening for.


INCIDENT 9 — THE CAPSTONE — THE SUITE THAT DEPLOYED THE ORG

STAKES — the story that binds the module

Your org's deployment is blocked: the test suite is green (passing, 95% coverage), but the security review refused sign-off — and the external audit found the "Testing" page empty: no test strategy, no coverage report, no evidence that the failure paths were ever exercised. Meanwhile, a new hire is about to add a feature that touches: the invoice-sync queueable (Module 3), the account search (Module 5 security), a nightly batch rollup (Module 1), and a scheduled reminder. You are asked to (1) rebuild the testing discipline so the suite actually proves the behaviors, (2) answer the auditor's questions about strategy, coverage, and async/security testing, and (3) mentor the new hire with the Incident 1–8 patterns.

The suite that deploys the org is the suite that tests behavior, not lines; failures, not just success; the user's view, not the system's; and the async paths, inside the window.

THE PROBLEM

Deliver, in order:

  1. The 8-pattern recap (the testing contract): one line per Incident 1–8 naming the pattern.
  2. The audit answers (test strategy): (a) "How do you decide what a test must assert?" (b) "How do you measure testing success?" (c) "What's the minimum for a deployable suite?" (d) "How do you test async and security?"
  3. The new-hire sprint plan: the test-design checklist for the four new components (queueable, search, batch, scheduled job) — mapping each to the Incident patterns (2–3 patterns each, with the mock/window/runAs requirements).
  4. The regression net: the three tests that MUST ship before the feature (the queueable success+timeout, the search sharing/FLS, the batch+schedule window) — one sentence each on what each proves.

HINT LADDER

  • Hint 1 (the avenue): (1) Recap: 1 = assert behavior, not process (re-query + exact values); 2 = no calendar/static-state dependence (fixed date + reset state in setup); 3 = mock all callouts (success + failure paths); 4 = right-sized fixtures + startTest/stopTest for batches; 5 = enqueue inside the window, assert after; 6 = runAs for sharing/FLS; 7 = schedulables: mechanics + behavior legs; 8 = negative tests: expectedExceptions for throwing, behavior assertions for handling — never swallow. (2) Audit: (a) assertions prove behavior: the state change the code promises (re-queried, exact); (b) success = behavior covered, not lines: every branch (success/failure/security) has an assertion; (c) minimum: 75%+ coverage as the floor, behavior tests for every async/security path, no swallowing tests, suite runs in minutes; (d) async = the window; security = runAs + user-view assertions. (3) Queueable → Incidents 5 + 3 + 8 (window, mock, failure/retry); Search → Incident 6 (runAs + FLS, two-layer fix); Batch → Incidents 4 + 1 (window + right-sized fixture, computed-value assertions); Scheduled → Incidents 7 + 3 (mechanics + behavior legs, mock if it calls out). (4) The three: queueable success+timeout (sync works + retry/dead-letter), search runAs (rep sees only owned, FLS stripped), batch+schedule window (rollup computes + job fires).
  • Hint 2 (the mechanism): (1) The contract sentence: "Every test asserts a behavior claim the code promises — measured on re-queried state, under the user's context, inside the async window, with mocks for every callout." 1–8 each name one clause. (2) (a) A test asserts the state change (re-queried) + the conditions (exact values/counts); (b) testing success = % of behavior claims verified (success, failure, security, async), reported per component; (c) deployable = green + behavior-complete + fast + no swallowing; (d) async = startTest/stopTest flush semantics; security = System.runAs + FLS assertions. (3) Map: queueable → [5: window] + [3: mock] + [8: timeout→retry→dead-letter]; search → [6: runAs sharing] + [6: FLS stripped]; batch → [4: right-sized + window] + [1: computed-value assertions]; scheduled → [7: mechanics leg] + [7: behavior leg] + [3: mock if callout]. (4) The three regression tests ship in the PR: queueable (success: synced rows after stopTest; timeout: retry counter + dead-letter), search (rep view = owned only, SSN null), batch+schedule (rollup = manual SUM; job fires in window).
  • Hint 3 (the skeleton): the deliverable is a spoken/written plan — structure it: (1) 8 one-line patterns; (2) 4 audit answers; (3) 4-component checklist with Incident references; (4) 3 regression tests with their proofs.

THE REVEAL — POSTMORTEM

What actually happened (the class of failure): the green suite that deployed a broken org — and the rebuild that made testing a discipline.

1. The 8-pattern recap (the testing contract — memorize as a single sentence): "Every test asserts a behavior claim the code promises — measured on re-queried state, under the user's context, inside the async window, with mocks for every callout, and never a swallowed exception." Clause by clause: Incident 1 = assert behavior, not process (re-query + exact values — coverage is a byproduct, never the goal); Incident 2 = no calendar/static-state dependence (fixed dates, reset state in @TestSetup); Incident 3 = mock every callout — success AND failure (the mock owns the conversation); Incident 4 = right-sized fixtures + the window for batches (bulk = code paths, not rows); Incident 5 = enqueue inside the window, assert after (the flush is the time machine); Incident 6 = runAs for sharing/FLS (assert the user's view); Incident 7 = schedulables have two legs (mechanics + behavior); Incident 8 = negative tests: expectedExceptions for throwing, behavior assertions for handling — never swallow.

2. The audit answers (test strategy — the four questions): (a) "How do you decide what a test must assert?" → the state change the code promises, re-queried and exact: the sync marks the row Synced, the rollup equals the manual SUM, the rep sees only owned records. Process assertions (no exception, record exists) are symptoms, not behavior. (b) "How do you measure testing success?" → behavior coverage, not line coverage: every branch has a claim — success path, failure path, security path, async path — reported per component, plus suite runtime (a 40-minute suite is a design failure). (c) "What's the minimum for a deployable suite?" → 75%+ coverage as the floor, behavior tests for every async/security path, no swallowing tests, green + fast. (d) "How do you test async and security?" → async: the startTest/stopTest window — enqueue inside, assert after, mocks for callouts; security: System.runAs + user-view assertions + FLS-stripped fields.

3. The new-hire sprint plan (the checklist — each component mapped to its patterns): (1) The queueable → [5: enqueue inside window, assert after] + [3: Test.setMock success + timeout mocks] + [8: timeout→retry-counter→dead-letter handling assertions]. (2) The account search → [6: runAs(rep) sharing test — owned vs not-owned] + [6: FLS — sensitive fields stripped for the rep]. (3) The batch rollup → [4: right-sized fixture + window flush] + [1: computed-value assertion vs manual SUM]. (4) The scheduled reminder → [7: mechanics leg — cron exists] + [7: behavior leg — job fires in the window, emails asserted] + [3: mock if it calls out]. Every component's checklist ends with the same question: "Which Incident patterns does this component violate if untested?"

4. The regression net (the three tests that MUST ship before the feature): (1) Queueable success + timeout: proves the sync marks rows Synced after the window flush (5+3), and the timeout path increments the retry counter and dead-letters on max attempts (8) — the Module 3 resilience contract, verified. (2) Search under runAs: proves the rep sees only owned records and FLS strips sensitive fields (6) — the Module 5 security contract, verified. (3) Batch + schedule window: proves the rollup equals the manual SUM (4+1) and the scheduled job fires inside the window with its effect asserted (7) — the automation contracts, verified. These three + the green suite are what the security review signs.

Why the "obvious fixes" failed (the contrast):

  • "Add more tests" → volume without the contract is the old suite's disease; the contract (assert behavior, mock, window, user's view, never swallow) is the cure.
  • "Track coverage %" → a dashboard is reporting, not discipline; behavior coverage is the metric.
  • "Test everything in one big class" → the four-component checklist + regression net is the structure — per-component, per-pattern.

KNOWLEDGE EXTRACTION (interview-ready)

  • "Your testing philosophy?" → One sentence: assert behavior claims — re-queried state, user's context, async window, mocked callouts, no swallowed exceptions.
  • "The 8 patterns?" → Behavior-not-process; no calendar/static dependence; mock all callouts (both paths); right-sized fixtures + window; enqueue inside, assert after; runAs for security; schedulables = mechanics + behavior; negative tests = expectedExceptions or handling assertions.
  • "Test strategy for an org?" → Behavior coverage per component (success/failure/security/async), 75%+ floor, no swallowing tests, fast suite.
  • "The audit's four questions?" → Assertions = promised state changes; success = behavior covered; minimum = green + behavior-complete + fast; async/security = window + runAs.

THE REDO

From memory: the contract sentence, the 8 one-line patterns, the 4 audit answers, and the 4-component checklist (with Incident references).

RETRIEVAL DRILL

  1. Recite the testing contract in one sentence.
  2. Name all 8 patterns in order.
  3. The 4 audit answers (compressed).
  4. Which patterns map to the queueable? The search? The batch? The scheduled job?
  5. The three regression tests and what each proves.

INTERVIEW MAPPING

The capstone answer to "describe your testing strategy" — the contract + the 8 patterns + the audit answers is a complete, structured senior response.


THE KNOWLEDGE SPINE — TESTING & TEST CLASSES

THE 8 SPINE COMMANDS

T1 — THE TEST IS A BEHAVIOR CLAIM. "A test asserts the state change the code promises — re-queried, exact, under the conditions that matter." Each test asks: what did the code promise, and how do I observe the promise kept on re-queried state? (Incident 1)

T2 — COVERAGE IS A BYPRODUCT, NEVER THE GOAL. "The deploy threshold is 75%+, but a 95% suite that asserts process is worthless; a 65% suite that asserts behavior ships." (Incident 1)

T3 — TESTS ARE DETERMINISTIC. "No clock, no shared state, no environment: fixed dates, @TestSetup-reset state, mocks for every callout — the same test, same result, every run, anywhere." (Incident 2)

T4 — THE MOCK OWNS THE CONVERSATION. "Test.setMock replaces the HTTP layer; the mock controls request AND response — including the failures. No mock, no real callout, ever: the platform throws CalloutException to enforce the rule." (Incident 3)

T5 — THE WINDOW IS THE TIME MACHINE. "Test.startTest() resets limits and opens the async window; Test.stopTest() executes everything enqueued inside — futures, queueables, batches + finish, scheduled jobs — synchronously. Enqueue inside; assert after." (Incidents 4, 5, 7)

T6 — BULK = CODE PATHS, NOT ROWS. "A small fixture exercises chunking, QueryLocator, and limit behavior; 50,000 rows only add runtime and flakiness. Right-sized fixtures + window flush + computed-value assertions." (Incident 4)

T7 — SECURITY TESTS ASSERT THE USER'S VIEW. "System.runAs(user) enforces sharing and FLS; without it, tests run in system context and security behavior is invisible. Assert what the user sees — counts, records, stripped fields." (Incident 6)

T8 — NEGATIVE TESTS EITHER EXPECT OR ASSERT. "@isTest(expectedExceptions=...) for code that SHOULD throw; behavior assertions (retry counter, dead-letter, fallback) for code that should HANDLE. Never swallow an exception in a test — that's how suites go green over broken retries." (Incident 8)

THE SPINE QUESTIONS (8)

  1. "What does a test assert — and what does coverage really prove?" → T1 + T2
  2. "Why must tests be deterministic — and the two top violators?" → T3 (calendar + shared state; mocks for callouts)
  3. "How do you test a callout — success and failure?" → T4 (both paths; the mock owns the conversation)
  4. "What do startTest/stopTest actually do — and why is async-testing impossible without them?" → T5 (reset + flush)
  5. "How do you test a batch without a mountain of data?" → T6 (right-sized + window + computed assertions)
  6. "Who runs your tests — and how do you test sharing/FLS?" → T7 (system context trap; runAs)
  7. "How do you test a scheduled job?" → T7/T5 mechanics + behavior legs
  8. "Negative tests: the two patterns?" → T8 (expectedExceptions vs handling assertions)

RAPID-FIRE — 20 QUESTIONS

  1. A test asserts System.assertEquals(1, results.size()) where results came from the test's own fixture. What's wrong? (T1: process, not behavior)
  2. The deployment fails the coverage check at 74%. What's your response? (T2: coverage is the floor; assert the behavior gaps, don't farm lines)
  3. A test fails only when the org's timezone is DST. The cause? (T3: calendar dependence — fixed dates)
  4. What happens if a test makes an HTTP call without Test.setMock? (T4: CalloutException — the platform enforces the mock rule)
  5. WebServiceMock is for which protocol? (T4: SOAP; HttpCalloutMock for REST)
  6. Does Test.stopTest() run the batch's finish()? (T5: yes — everything enqueued inside the window)
  7. Your batch test times out. The first thing you check? (T6: enqueue inside startTest/stopTest — the window)
  8. "Testing bulk" means testing what? (T6: chunking, QueryLocator, limit behavior — not row volume)
  9. Who runs test code by default — and what does that hide? (T7: test-running user, system context — sharing/FLS invisible)
  10. The runAs user must exist where? (T7: created in the test — standard profile + needed PermissionSets)
  11. What does Test.getCronTrigger(jobId) prove — and not prove? (T7/T5: schedule exists; job executed only via the window)
  12. A test with try/catch and no assertion — the verdict? (T8: worse than no test — swallows failures, asserts nothing)
  13. @isTest(expectedExceptions=...) applies to which kind of code? (T8: code that SHOULD throw — validation/guard clauses)
  14. Your retry logic's test asserts Retry_Count__c = 1. What pattern is this? (T8: handling assertion — the failure was handled)
  15. What does Test.startTest() reset? (T5: governor limits — fresh limits for the window)
  16. Mock returns 500 — what's being tested? (T4/T8: the failure path — retry/dead-letter behavior)
  17. The sharing test passes without runAs. Why is it meaningless? (T7: asserted the admin's view, not the user's)
  18. When should the assertion run — before or after stopTest? (T5: after — the flush happens at stopTest)
  19. Coverage at 92% but the sync breaks in production. What was covered? (T1: the enqueue line, not the behavior)
  20. A scheduled job's execute never ran in a test — how to fix? (T7: schedule inside the window + assert the effect; two legs)

INTERLEAVED PRACTICE SET

Practice the module in cross-topic mode. Time-box each problem (senior pace: 5–7 minutes each).

IP-1 (Integration × Testing): The Module 3 sync class is failing in production — retries work but dead-letter records never get created. Write the single test that would have caught it, naming the mock and the assertions. (Answer: TimeoutMock → window → assert Retry_Count = 1 → on 3rd attempt assert Error_Log__c row — Incidents 3+8.)

IP-2 (Security × Testing): The Module 5 search returns 40 accounts for a rep who should see 1. Write the test that proves the fix, and state what the pre-fix test was wrongly asserting. (Answer: runAs(rep) with owned/not-owned records → assert 1 result + FLS stripped fields; pre-fix asserted the admin's view — Incident 6.)

IP-3 (Async × Testing): The nightly batch (Module 1) "works" but its test takes 40 minutes. Diagnose with three findings and rewrite. (Answer: overweight fixture, no window, fixture-assertions — right-sized + window + computed SUM — Incident 4.)

IP-4 (Automation × Testing): A Flow calls a queueable. Where does the flow's testing contract end and the queueable's begin — and what pattern does the queueable's test need? (Answer: flow tests assert the record outcome; the queueable needs the window + mock — Incidents 5+3.)

IP-5 (Order of Execution × Testing): The order-of-execution chain (Module 1) breaks when a trigger is added. Which test pattern protects the chain — and what would a process-assertion test miss? (Answer: behavior tests asserting the final record state after the full chain — re-queried, exact values; process assertions miss the chain's composed outcome — Incident 1.)


SPACED REPETITION — The Six-Window Schedule

Window 1 — NEW (20 minutes after the module)

  1. Write the contract sentence. (Incident 9)
  2. Write the corrected queueable test (success + timeout) from memory. (Incident 5)
  3. The startTest/stopTest semantics — write both halves. (Incident 4)
  4. Write the runAs sharing test skeleton. (Incident 6)
  5. The two negative-test patterns + when each applies. (Incident 8)

Window 2 — 24 HOURS

  1. The 8 one-line patterns (incident order). (Incident 9)
  2. The four mock classes + registration method. (Incident 3)
  3. Why is the exception-swallowing test worse than none? (Three reasons.) (Incident 8)
  4. What does Test.getCronTrigger prove — and not prove? (Incident 7)
  5. The three design failures of the 40-minute test. (Incident 4)

Window 3 — 3 DAYS

  1. The two failure modes of async testing (green-but-untested). (Incident 5)
  2. The runAs constraints (3). (Incident 6)
  3. The three regression tests and what each proves. (Incident 9)
  4. The scheduling test: two legs, what each asserts. (Incident 7)
  5. The corrected retry test — all clauses. (Incident 8)

Window 4 — 1 WEEK

  1. The testing contract sentence — verbatim. (Incident 9)
  2. The four audit answers (compressed). (Incident 9)
  3. The corrected bulk batch test skeleton. (Incident 4)
  4. The FLS assertion pattern. (Incident 6)
  5. Why is "test that it throws" the wrong expectation for a retry contract? (Incident 8)

Window 5 — 2 WEEKS

  1. The 8 patterns from memory — then the contract sentence. (Incident 9)
  2. The success-path callout test — with the request assertions. (Incident 3)
  3. Which patterns map to the queueable/search/batch/scheduled? (Incident 9)
  4. The default-context trap, in one sentence. (Incident 6)
  5. "How do you test a scheduled job?" — full answer. (Incident 7)

Window 6 — 1 MONTH

  1. The full module answer: "Describe your testing strategy." (Incident 9)
  2. The 8 spine commands, verbatim. (The Knowledge Spine)
  3. The window semantics for ALL async constructs. (Incidents 4+5+7)
  4. The mock classes + both-path testing. (Incident 3)
  5. The negative-test verdict: "swallow" vs "expect" vs "assert". (Incident 8)

SOURCES & REFERENCES


End of Topic 6 — Testing & Test Classes. Next: Topic 7 — Deployment / DevOps / SFDX / CI-CD (the 8–12% slice).

This workbook is the property of the Salesforce Interview Prep archive. Every answer you build from these incidents is a deployable claim.

On this page

M0 — THE MAP (read this first, 5–10 min)The one idea everything hangs on: A TEST IS A QUESTION YOU ASK YOUR OWN CODE — AND THE ANSWER MUST BE AN ASSERTION, NOT A GREEN RUNThe incidents (choose your own adventure — recommended order)INCIDENT 1 — THE GREEN BUILD THAT DEPLOYED A BUGSTAKESTHE INCIDENTTHE PROBLEMTHE REVEAL — POSTMORTEMKNOWLEDGE EXTRACTION (interview-ready)THE REDORETRIEVAL DRILLINTERVIEW MAPPINGINCIDENT 2 — THE TEST THAT FAILED ON FRIDAYSSTAKESTHE INCIDENTTHE PROBLEMTHE REVEAL — POSTMORTEMKNOWLEDGE EXTRACTION (interview-ready)THE REDORETRIEVAL DRILLINTERVIEW MAPPINGINCIDENT 3 — THE CALLOUT TEST THAT CALLED THE INTERNETSTAKESTHE INCIDENTTHE PROBLEMTHE REVEAL — POSTMORTEMKNOWLEDGE EXTRACTION (interview-ready)THE REDORETRIEVAL DRILLINTERVIEW MAPPINGINCIDENT 4 — THE 40-MINUTE TEST THAT DEPLOYED NOTHINGSTAKESTHE INCIDENTTHE PROBLEMTHE REVEAL — POSTMORTEMKNOWLEDGE EXTRACTION (interview-ready)THE REDORETRIEVAL DRILLINTERVIEW MAPPINGINCIDENT 5 — THE ASYNC TEST THAT TESTED NOTHINGSTAKESTHE INCIDENTTHE PROBLEMTHE REVEAL — POSTMORTEMKNOWLEDGE EXTRACTION (interview-ready)THE REDORETRIEVAL DRILLINTERVIEW MAPPINGINCIDENT 6 — THE runAs TEST THAT RAN AS THE WRONG USERSTAKESTHE INCIDENTTHE PROBLEMTHE REVEAL — POSTMORTEMKNOWLEDGE EXTRACTION (interview-ready)THE REDORETRIEVAL DRILLINTERVIEW MAPPINGINCIDENT 7 — THE SCHEDULED JOB THAT NEVER FIREDSTAKESTHE INCIDENTTHE PROBLEMTHE REVEAL — POSTMORTEMKNOWLEDGE EXTRACTION (interview-ready)THE REDORETRIEVAL DRILLINTERVIEW MAPPINGINCIDENT 8 — THE EXCEPTION TEST THAT SWALLOWED THE BUGSTAKESTHE INCIDENTTHE PROBLEMTHE REVEAL — POSTMORTEMKNOWLEDGE EXTRACTION (interview-ready)THE REDORETRIEVAL DRILLINTERVIEW MAPPINGINCIDENT 9 — THE CAPSTONE — THE SUITE THAT DEPLOYED THE ORGSTAKES — the story that binds the moduleTHE PROBLEMTHE REVEAL — POSTMORTEMKNOWLEDGE EXTRACTION (interview-ready)THE REDORETRIEVAL DRILLINTERVIEW MAPPINGTHE KNOWLEDGE SPINE — TESTING & TEST CLASSESTHE 8 SPINE COMMANDSTHE SPINE QUESTIONS (8)RAPID-FIRE — 20 QUESTIONSINTERLEAVED PRACTICE SETSPACED REPETITION — The Six-Window ScheduleWindow 1 — NEW (20 minutes after the module)Window 2 — 24 HOURSWindow 3 — 3 DAYSWindow 4 — 1 WEEKWindow 5 — 2 WEEKSWindow 6 — 1 MONTHSOURCES & REFERENCES