Test the Feature
By the end of this lesson, you'll be able to:
- Write a basic @isTest class covering the happy path and the validation-blocking scenario
- Use Execute Anonymous as a quick sanity check while actively building, distinct from the real test
- Recognize a passing @isTest class as this feature's actual definition of done
Prerequisites: "Add Service Classes"
A quick manual sanity check first
While actively building a feature, running a realistic scenario through Execute Anonymous (Module 1) is still a fast, useful way to sanity-check as you go:
Account acc = new Account(Name = 'Test Account');
insert acc;
Opportunity opp = new Opportunity(
Name = 'Test Deal', AccountId = acc.Id, StageName = 'Prospecting',
CloseDate = Date.today(), Amount = 50000
);
insert opp;
opp.StageName = 'Closed Won';
update opp;
Account updated = [SELECT Total_Won_Revenue__c FROM Account WHERE Id = :acc.Id];
System.debug('Expected 50000, got: ' + updated.Total_Won_Revenue__c);
Account acc = new Account(Name = 'Test Account');— a test account.insert acc;— saved.- (blank line)
Opportunity opp = new Opportunity(— a related opportunity, starting unwon.Name = 'Test Deal', AccountId = acc.Id, StageName = 'Prospecting',— links it to the account.CloseDate = Date.today(), Amount = 50000— a valid amount.);— closes the constructor call.insert opp;— saved.- (blank line)
opp.StageName = 'Closed Won';— the transition being tested.update opp;— fires the trigger.- (blank line)
Account updated = [SELECT Total_Won_Revenue__c FROM Account WHERE Id = :acc.Id];— re-queries the account.System.debug('Expected 50000, got: ' + updated.Total_Won_Revenue__c);— this is a good first check while writing the code — but per the previous module's lesson, it's not where verification should stop. Nothing about this check survives once the window is closed.
The real test: the happy path
@isTest
private class OpportunityRollupTest {
@isTest
static void closingAnOpportunityRollsUpRevenueToTheAccount() {
Account acc = new Account(Name = 'Test Account');
insert acc;
Opportunity opp = new Opportunity(
Name = 'Test Deal', AccountId = acc.Id, StageName = 'Prospecting',
CloseDate = Date.today(), Amount = 50000
);
insert opp;
Test.startTest();
opp.StageName = 'Closed Won';
update opp;
Test.stopTest();
Account updated = [SELECT Total_Won_Revenue__c FROM Account WHERE Id = :acc.Id];
System.assertEquals(50000, updated.Total_Won_Revenue__c, 'Expected the won Opportunity amount to roll up to the Account');
}
}
@isTest— marks the class as test-only.private class OpportunityRollupTest {— the test class.@isTest— marks the test method.static void closingAnOpportunityRollsUpRevenueToTheAccount() {— a clearly-named test.Account acc = new Account(Name = 'Test Account');— the same setup as the manual check.insert acc;— saved.- (blank line)
Opportunity opp = new Opportunity(— the related opportunity.Name = 'Test Deal', AccountId = acc.Id, StageName = 'Prospecting',— starting unwon.CloseDate = Date.today(), Amount = 50000— a valid amount.);— closes the constructor call.insert opp;— saved.- (blank line)
Test.startTest();— marks the start of the action being measured/tested.opp.StageName = 'Closed Won';— the transition.update opp;— fires the trigger.Test.stopTest();— marks the end.- (blank line)
Account updated = [SELECT Total_Won_Revenue__c FROM Account WHERE Id = :acc.Id];— re-queries.System.assertEquals(50000, updated.Total_Won_Revenue__c, 'Expected the won Opportunity amount to roll up to the Account');— wrapped in@isTestwith a real assertion, this version stays in the codebase and re-runs automatically forever, catching a regression the instant someone's later change breaks the rollup.}— closes the test method.}— closes the class.
The real test: the validation-blocking scenario
@isTest
private class OpportunityRollupTest {
// ... closingAnOpportunityRollsUpRevenueToTheAccount() from above ...
@isTest
static void closingAnOpportunityWithNoAmountIsBlocked() {
Account acc = new Account(Name = 'Test Account');
insert acc;
Opportunity badOpp = new Opportunity(
Name = 'Bad Deal', AccountId = acc.Id, StageName = 'Prospecting',
CloseDate = Date.today() // no Amount set
);
insert badOpp;
badOpp.StageName = 'Closed Won';
try {
update badOpp;
System.assert(false, 'Expected this update to be blocked by validation');
} catch (DmlException e) {
System.assert(e.getMessage().contains('Amount'), 'Expected the error to mention the missing Amount');
}
}
}
@isTest— marks the class.private class OpportunityRollupTest {— the same test class as before.// ... closingAnOpportunityRollsUpRevenueToTheAccount() from above ...— the first test method still lives here too.- (blank line)
@isTest— marks a second test method.static void closingAnOpportunityWithNoAmountIsBlocked() {— a clearly-named test for the validation scenario.Account acc = new Account(Name = 'Test Account');— a test account.insert acc;— saved.- (blank line)
Opportunity badOpp = new Opportunity(— an opportunity built to violate the rule.Name = 'Bad Deal', AccountId = acc.Id, StageName = 'Prospecting',— links it to the account.CloseDate = Date.today() // no Amount set— deliberately missing the required field.);— closes the constructor call.insert badOpp;— saved, still valid at this point (not yet Closed Won).- (blank line)
badOpp.StageName = 'Closed Won';— sets up the violation.- (blank line)
try {— this is Module 19'stry/catcharound aDmlException.update badOpp;— expected to be blocked by the validation.System.assert(false, 'Expected this update to be blocked by validation');— if this line is ever reached, the test fails — the update should never succeed.} catch (DmlException e) {— catches the expected failure.System.assert(e.getMessage().contains('Amount'), 'Expected the error to mention the missing Amount');— the "it was correctly blocked" outcome is asserted, not just read off a debug log by eye.}— closes thecatch.}— closes the test method.}— closes the class.
Exercise
Add a third test method, revenueDecreasesWhenAnOpportunityIsUnwon, that closes an Opportunity as Won, then moves it back to Prospecting, and asserts the Account's Total_Won_Revenue__c decreased accordingly.
Show hint
Insert the Account and won Opportunity first, then change StageName back to Prospecting inside Test.startTest()/stopTest(), then re-query the Account and assert.
Test the Feature 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
The previous module's "Writing Your First Automated Test" lesson established the habit — this lesson applies it for real, with a basic @isTest class that verifies the rollup actually works and actually blocks bad input.