Intermediate 30 min read

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);
  1. Account acc = new Account(Name = 'Test Account'); — a test account.
  2. insert acc; — saved.
  3. (blank line)
  4. Opportunity opp = new Opportunity( — a related opportunity, starting unwon.
  5. Name = 'Test Deal', AccountId = acc.Id, StageName = 'Prospecting', — links it to the account.
  6. CloseDate = Date.today(), Amount = 50000 — a valid amount.
  7. ); — closes the constructor call.
  8. insert opp; — saved.
  9. (blank line)
  10. opp.StageName = 'Closed Won'; — the transition being tested.
  11. update opp; — fires the trigger.
  12. (blank line)
  13. Account updated = [SELECT Total_Won_Revenue__c FROM Account WHERE Id = :acc.Id]; — re-queries the account.
  14. 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');
    }
}
  1. @isTest — marks the class as test-only.
  2. private class OpportunityRollupTest { — the test class.
  3. @isTest — marks the test method.
  4. static void closingAnOpportunityRollsUpRevenueToTheAccount() { — a clearly-named test.
  5. Account acc = new Account(Name = 'Test Account'); — the same setup as the manual check.
  6. insert acc; — saved.
  7. (blank line)
  8. Opportunity opp = new Opportunity( — the related opportunity.
  9. Name = 'Test Deal', AccountId = acc.Id, StageName = 'Prospecting', — starting unwon.
  10. CloseDate = Date.today(), Amount = 50000 — a valid amount.
  11. ); — closes the constructor call.
  12. insert opp; — saved.
  13. (blank line)
  14. Test.startTest(); — marks the start of the action being measured/tested.
  15. opp.StageName = 'Closed Won'; — the transition.
  16. update opp; — fires the trigger.
  17. Test.stopTest(); — marks the end.
  18. (blank line)
  19. Account updated = [SELECT Total_Won_Revenue__c FROM Account WHERE Id = :acc.Id]; — re-queries.
  20. System.assertEquals(50000, updated.Total_Won_Revenue__c, 'Expected the won Opportunity amount to roll up to the Account'); — wrapped in @isTest with 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.
  21. } — closes the test method.
  22. } — 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');
        }
    }
}
  1. @isTest — marks the class.
  2. private class OpportunityRollupTest { — the same test class as before.
  3. // ... closingAnOpportunityRollsUpRevenueToTheAccount() from above ... — the first test method still lives here too.
  4. (blank line)
  5. @isTest — marks a second test method.
  6. static void closingAnOpportunityWithNoAmountIsBlocked() { — a clearly-named test for the validation scenario.
  7. Account acc = new Account(Name = 'Test Account'); — a test account.
  8. insert acc; — saved.
  9. (blank line)
  10. Opportunity badOpp = new Opportunity( — an opportunity built to violate the rule.
  11. Name = 'Bad Deal', AccountId = acc.Id, StageName = 'Prospecting', — links it to the account.
  12. CloseDate = Date.today() // no Amount set — deliberately missing the required field.
  13. ); — closes the constructor call.
  14. insert badOpp; — saved, still valid at this point (not yet Closed Won).
  15. (blank line)
  16. badOpp.StageName = 'Closed Won'; — sets up the violation.
  17. (blank line)
  18. try { — this is Module 19's try/catch around a DmlException.
  19. update badOpp; — expected to be blocked by the validation.
  20. 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.
  21. } catch (DmlException e) { — catches the expected failure.
  22. 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.
  23. } — closes the catch.
  24. } — closes the test method.
  25. } — 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.

APEX

Test the Feature Quiz

1. Why does this lesson write a real @isTest class instead of stopping at the Execute Anonymous check?

2. What does Test.startTest()/Test.stopTest() wrap in the happy-path test?

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

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.