Advanced 40 min read

Write a Full Test Suite

By the end of this lesson, you'll be able to:

  • Write bulk-safe test coverage for both triggers and the batch job
  • Test the overdraft-rejection and frozen-account scenarios explicitly
  • Apply Module 31's testing principles to a system with real financial stakes

Prerequisites: "Secure Account Data"

Testing the happy path in bulk

@isTest
private class TransactionTriggerTest {
    @isTest
    static void updatesBalanceForBulkDeposits() {
        Bank_Account__c acc = new Bank_Account__c(Name = 'Test', Balance__c = 0, Status__c = 'Active');
        insert acc;

        List<Transaction__c> deposits = new List<Transaction__c>();
        for (Integer i = 0; i < 200; i++) {
            deposits.add(new Transaction__c(Bank_Account__c = acc.Id, Type__c = 'Deposit', Amount__c = 10));
        }

        Test.startTest();
        insert deposits;
        Test.stopTest();

        Assert.areEqual(2000, [SELECT Balance__c FROM Bank_Account__c WHERE Id = :acc.Id].Balance__c);
    }
}
  1. @isTest — marks the test class.
  2. private class TransactionTriggerTest { — the test class.
  3. @isTest — marks the test method.
  4. static void updatesBalanceForBulkDeposits() { — states what's verified.
  5. Bank_Account__c acc = new Bank_Account__c(Name = 'Test', Balance__c = 0, Status__c = 'Active'); — test data, starting at zero.
  6. insert acc; — saves it.
  7. (blank line)
  8. List<Transaction__c> deposits = new List<Transaction__c>(); — a list to build in bulk.
  9. for (Integer i = 0; i < 200; i++) { — Module 31's "always test at bulk scale" rule — the maximum trigger batch size.
  10. deposits.add(new Transaction__c(Bank_Account__c = acc.Id, Type__c = 'Deposit', Amount__c = 10)); — 200 identical deposits.
  11. } — closes the for loop.
  12. (blank line)
  13. Test.startTest(); — the governor-limit boundary.
  14. insert deposits; — one bulk DML statement, firing the trigger once with all 200 records.
  15. Test.stopTest(); — closes the boundary.
  16. (blank line)
  17. Assert.areEqual(2000, [SELECT Balance__c FROM Bank_Account__c WHERE Id = :acc.Id].Balance__c); — a specific expected total — 200 deposits of 10 each.
  18. } — closes the test method.
  19. } — closes the test class.

200 deposits of 10 each should net to exactly 2000 — this is Module 31's "always test at bulk scale" rule, catching any hidden per-record assumption Lesson 2's trigger might have.

Testing the rejection paths explicitly

@isTest
static void rejectsOverdraftWithdrawal() {
    Bank_Account__c acc = new Bank_Account__c(Name = 'Test', Balance__c = 100, Status__c = 'Active');
    insert acc;

    Transaction__c overdraft = new Transaction__c(Bank_Account__c = acc.Id, Type__c = 'Withdrawal', Amount__c = 500);

    Test.startTest();
    try {
        insert overdraft;
        Assert.fail('Expected an overdraft DmlException');
    } catch (DmlException e) {
        Assert.isTrue(e.getMessage().contains('Insufficient funds'));
    }
    Test.stopTest();
}
  1. @isTest — marks the test method.
  2. static void rejectsOverdraftWithdrawal() { — states the exact scenario under test.
  3. Bank_Account__c acc = new Bank_Account__c(Name = 'Test', Balance__c = 100, Status__c = 'Active'); — a modest starting balance.
  4. insert acc; — saves it.
  5. (blank line)
  6. Transaction__c overdraft = new Transaction__c(Bank_Account__c = acc.Id, Type__c = 'Withdrawal', Amount__c = 500); — a withdrawal larger than the balance.
  7. (blank line)
  8. Test.startTest(); — the boundary.
  9. try { — Module 31's exception-testing shape.
  10. insert overdraft; — expected to throw, given the validation trigger.
  11. Assert.fail('Expected an overdraft DmlException'); — fails the test if no exception was thrown.
  12. } catch (DmlException e) { — addError() surfaces as a DmlException to the calling code.
  13. Assert.isTrue(e.getMessage().contains('Insufficient funds')); — confirms the specific rejection reason.
  14. } — closes the try/catch.
  15. Test.stopTest(); — closes the boundary.
  16. } — closes the test method.

This is Module 31's negative-path testing principle — confirming the rejection itself works correctly is just as important as confirming a valid deposit updates the balance, since an untested rejection path could silently let an overdraft through.

Testing the async ledger callout with a mock

@isTest
static void reportsToLedgerAsynchronously() {
    Test.setMock(HttpCalloutMock.class, new LedgerMockSuccess());
    Bank_Account__c acc = new Bank_Account__c(Name = 'Test', Balance__c = 0, Status__c = 'Active');
    insert acc;

    Test.startTest();
    insert new Transaction__c(Bank_Account__c = acc.Id, Type__c = 'Deposit', Amount__c = 50);
    Test.stopTest(); // forces the enqueued Queueable job to run synchronously here

    // assert on a field the mock's callout would have set, or that no exception occurred
}
  1. @isTest — marks the test method.
  2. static void reportsToLedgerAsynchronously() { — states what's verified.
  3. Test.setMock(HttpCalloutMock.class, new LedgerMockSuccess()); — Module 36's mocking, swapping in a fabricated response.
  4. Bank_Account__c acc = new Bank_Account__c(Name = 'Test', Balance__c = 0, Status__c = 'Active'); — test data.
  5. insert acc; — saves it.
  6. (blank line)
  7. Test.startTest(); — the boundary.
  8. insert new Transaction__c(Bank_Account__c = acc.Id, Type__c = 'Deposit', Amount__c = 50); — fires both triggers, including the enqueue of the ledger job.
  9. Test.stopTest(); // forces the enqueued Queueable job to run synchronously here — Module 31's boundary, forcing the async job to completion.
  10. (blank line)
  11. // assert on a field the mock's callout would have set, or that no exception occurred — a placeholder for the real assertion.
  12. } — closes the test method.

Test.setMock and Test.stopTest() forcing the enqueued job to run are Module 31's and Module 36's exact async-callout testing patterns, combined here for the ledger-integration job from Lesson 4.

Exercise

As a comment, list the three categories of test this lesson covers, and why all three are needed rather than just the happy-path bulk test.

Show hint

Think about what each test type would catch that the others wouldn't.

APEX

Write a Full Test Suite Quiz

1. Why does rejectsOverdraftWithdrawal wrap the insert in a try/catch and assert inside the catch block?

Log in to submit the quiz and save your score.

My Notes

Log in to keep private notes on this lesson.

Questions about this lesson

No questions yet — be the first to ask.

Log in to ask a question about this lesson.

Summary

This lesson applies Module 31's full testing toolkit — bulk data, negative-path testing, and Test.startTest()/stopTest() for the async ledger callout — to a system where an untested edge case has real financial consequences.