Advanced 45 min read

Write the Test Suite

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

  • Write bulk, negative-path, and async test coverage across the full layered architecture
  • Use dependency injection to unit test service-layer logic without any database records
  • Apply Module 31's complete testing toolkit one final time, at full project scale

Prerequisites: "Secure the Application"

A bulk trigger test

@isTest
private class MilestoneTriggerTest {
    @isTest
    static void allowsValidStatusTransitionsInBulk() {
        Project__c proj = new Project__c(Name = 'Test Project', Status__c = 'Active');
        insert proj;

        List<Milestone__c> milestones = new List<Milestone__c>();
        for (Integer i = 0; i < 200; i++) {
            milestones.add(new Milestone__c(Project__c = proj.Id, Status__c = 'Not Started'));
        }
        insert milestones;

        for (Milestone__c m : milestones) m.Status__c = 'In Progress';

        Test.startTest();
        update milestones;
        Test.stopTest();

        for (Milestone__c m : [SELECT Status__c FROM Milestone__c WHERE Project__c = :proj.Id]) {
            Assert.areEqual('In Progress', m.Status__c);
        }
    }
}
  1. @isTest — marks the test class.
  2. private class MilestoneTriggerTest { — the test class.
  3. @isTest — marks the test method.
  4. static void allowsValidStatusTransitionsInBulk() { — states what's verified.
  5. Project__c proj = new Project__c(Name = 'Test Project', Status__c = 'Active'); — test data.
  6. insert proj; — saves it.
  7. (blank line)
  8. List<Milestone__c> milestones = new List<Milestone__c>(); — a list to build in bulk.
  9. for (Integer i = 0; i < 200; i++) { — Module 31's "always test at bulk scale" rule.
  10. milestones.add(new Milestone__c(Project__c = proj.Id, Status__c = 'Not Started')); — 200 identical Milestones.
  11. } — closes the for loop.
  12. insert milestones; — one bulk DML statement.
  13. (blank line)
  14. for (Milestone__c m : milestones) m.Status__c = 'In Progress'; — a valid transition for every Milestone.
  15. (blank line)
  16. Test.startTest(); — the governor-limit boundary.
  17. update milestones; — one bulk update, firing the trigger once with all 200 records.
  18. Test.stopTest(); — closes the boundary.
  19. (blank line)
  20. for (Milestone__c m : [SELECT Status__c FROM Milestone__c WHERE Project__c = :proj.Id]) { — re-queries every Milestone.
  21. Assert.areEqual('In Progress', m.Status__c); — confirms every single one transitioned correctly.
  22. } — closes the for loop.
  23. } — closes the test method.
  24. } — closes the test class.

200 Milestones updated in one bulk operation — Module 31\'s "always test at bulk scale" rule, still the very first check for any trigger-bearing object.

A dependency-injected service unit test, with zero DML

@isTest
static void marksProjectAtRiskWhenMilestoneOverdue() {
    Id fakeProjectId = TestHelper.fakeId(Project__c.SObjectType);
    Milestone__c overdue = new Milestone__c(
        Id = TestHelper.fakeId(Milestone__c.SObjectType),
        Project__c = fakeProjectId,
        Status__c = 'In Progress',
        Due_Date__c = Date.today().addDays(-3)
    );

    FakeMilestoneSelector fakeSelector = new FakeMilestoneSelector(new List<Milestone__c>{overdue});
    ProjectHealthService service = new ProjectHealthService(fakeSelector);

    Map<Id, String> result = service.calculateHealth(new Set<Id>{fakeProjectId});

    Assert.areEqual('At Risk', result.get(fakeProjectId));
}
  1. @isTest — marks the test method.
  2. static void marksProjectAtRiskWhenMilestoneOverdue() { — states what's verified.
  3. Id fakeProjectId = TestHelper.fakeId(Project__c.SObjectType); — a fake, unsaved Id.
  4. Milestone__c overdue = new Milestone__c( — an in-memory Milestone, never inserted.
  5. Id = TestHelper.fakeId(Milestone__c.SObjectType), — another fake Id.
  6. Project__c = fakeProjectId, — linked to the fake project.
  7. Status__c = 'In Progress', — still open.
  8. Due_Date__c = Date.today().addDays(-3) — three days overdue.
  9. ); — closes the constructor.
  10. (blank line)
  11. FakeMilestoneSelector fakeSelector = new FakeMilestoneSelector(new List<Milestone__c>{overdue}); — a test-only stand-in returning hand-built data.
  12. ProjectHealthService service = new ProjectHealthService(fakeSelector); — Lesson 5's constructor, injecting the fake.
  13. (blank line)
  14. Map<Id, String> result = service.calculateHealth(new Set<Id>{fakeProjectId}); — calls the real service logic against fake data.
  15. (blank line)
  16. Assert.areEqual('At Risk', result.get(fakeProjectId)); — confirms the overdue Milestone correctly marked the project.
  17. } — closes the test method.

This is Lesson 5\'s dependency injection paying off directly in the test suite — ProjectHealthService\'s core business rule is verified without inserting a single real record, running dramatically faster than a DML-based test would.

An async job test

@isTest
static void recalculatesHealthForAllActiveProjects() {
    // ... insert an Active Project with an overdue Milestone

    Test.startTest();
    Database.executeBatch(new RecalculateProjectHealthBatch());
    Test.stopTest(); // forces the batch to actually run

    // assert the Project's Health__c field was updated correctly
}
  1. @isTest — marks the test method.
  2. static void recalculatesHealthForAllActiveProjects() { — states what's verified.
  3. // ... insert an Active Project with an overdue Milestone — test data setup, summarized.
  4. (blank line)
  5. Test.startTest(); — the governor-limit boundary.
  6. Database.executeBatch(new RecalculateProjectHealthBatch()); — starts the batch job.
  7. Test.stopTest(); // forces the batch to actually run — Module 31's and Module 38's boundary, forcing the job to completion.
  8. (blank line)
  9. // assert the Project's Health__c field was updated correctly — the real assertion, summarized.
  10. } — closes the test method.

Test.startTest()/Test.stopTest() forcing the batch to run synchronously within the test — Module 31\'s and Module 38\'s exact async-testing pattern, reused one final time for this project\'s nightly safety net.

Exercise

As a comment, explain why marksProjectAtRiskWhenMilestoneOverdue can use TestHelper.fakeId() instead of actually inserting a Project__c and Milestone__c record.

Show hint

Think about what ProjectHealthService actually depends on, thanks to Lesson 5's design.

APEX

Write the Test Suite Quiz

1. What makes the dependency-injected service test meaningfully faster than a DML-based test would be?

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 complete testing toolkit across every layer of this project — bulk trigger tests, dependency-injected service unit tests, and async job tests — the same rigor from every prior project module, now spanning four objects and several architectural layers at once.