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);
}
}
@isTest— marks the test class.private class TransactionTriggerTest {— the test class.@isTest— marks the test method.static void updatesBalanceForBulkDeposits() {— states what's verified.Bank_Account__c acc = new Bank_Account__c(Name = 'Test', Balance__c = 0, Status__c = 'Active');— test data, starting at zero.insert acc;— saves it.- (blank line)
List<Transaction__c> deposits = new List<Transaction__c>();— a list to build in bulk.for (Integer i = 0; i < 200; i++) {— Module 31's "always test at bulk scale" rule — the maximum trigger batch size.deposits.add(new Transaction__c(Bank_Account__c = acc.Id, Type__c = 'Deposit', Amount__c = 10));— 200 identical deposits.}— closes theforloop.- (blank line)
Test.startTest();— the governor-limit boundary.insert deposits;— one bulk DML statement, firing the trigger once with all 200 records.Test.stopTest();— closes the boundary.- (blank line)
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.}— closes the test method.}— 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();
}
@isTest— marks the test method.static void rejectsOverdraftWithdrawal() {— states the exact scenario under test.Bank_Account__c acc = new Bank_Account__c(Name = 'Test', Balance__c = 100, Status__c = 'Active');— a modest starting balance.insert acc;— saves it.- (blank line)
Transaction__c overdraft = new Transaction__c(Bank_Account__c = acc.Id, Type__c = 'Withdrawal', Amount__c = 500);— a withdrawal larger than the balance.- (blank line)
Test.startTest();— the boundary.try {— Module 31's exception-testing shape.insert overdraft;— expected to throw, given the validation trigger.Assert.fail('Expected an overdraft DmlException');— fails the test if no exception was thrown.} catch (DmlException e) {—addError()surfaces as aDmlExceptionto the calling code.Assert.isTrue(e.getMessage().contains('Insufficient funds'));— confirms the specific rejection reason.}— closes thetry/catch.Test.stopTest();— closes the boundary.}— 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
}
@isTest— marks the test method.static void reportsToLedgerAsynchronously() {— states what's verified.Test.setMock(HttpCalloutMock.class, new LedgerMockSuccess());— Module 36's mocking, swapping in a fabricated response.Bank_Account__c acc = new Bank_Account__c(Name = 'Test', Balance__c = 0, Status__c = 'Active');— test data.insert acc;— saves it.- (blank line)
Test.startTest();— the boundary.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.Test.stopTest(); // forces the enqueued Queueable job to run synchronously here— Module 31's boundary, forcing the async job to completion.- (blank line)
// assert on a field the mock's callout would have set, or that no exception occurred— a placeholder for the real assertion.}— 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.
Write a Full Test Suite Quiz
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.