Testing, Debugging, and Deployment · Topic 1 of 4
Testing Triggers, Classes, Controllers and Flows
Apex testing for Platform Developer: test classes, test data, TestSetup, startTest and stopTest, runAs, callout mocks, coverage rules and flow tests.
- 4 min
- Production deploys need 75% overall coverage, coverage on every trigger, and all tests passing.
- Tests do not see org data by default; create it, ideally in
@TestSetupor a factory. startTest()gives fresh limits;stopTest()runs queued async work.
What it is
Apex unit tests are methods that run your code and assert that it behaves as expected. Salesforce requires tests to deploy Apex to production: at least 75% overall code coverage, every trigger with some coverage, and every test passing.
Tests run in isolation. They create their own data, and everything they write is rolled back when the test ends.
How it works
A test class
@IsTest
private class ContactTriggerTest {
@TestSetup
static void makeData() {
insert new Account(Name = 'Acme');
}
@IsTest
static void setsDescriptionInBulk() {
Account a = [SELECT Id FROM Account LIMIT 1];
List<Contact> cs = new List<Contact>();
for (Integer i = 0; i < 200; i++) {
cs.add(new Contact(LastName = 'C' + i, AccountId = a.Id));
}
Test.startTest();
insert cs;
Test.stopTest();
for (Contact c : [SELECT Description FROM Contact]) {
Assert.areEqual('Acme', c.Description);
}
}
}
Key features
| Feature | What it does |
|---|---|
@IsTest | Marks a test class or method; test code does not count toward the org's Apex size limit |
@TestSetup | Creates data once for all test methods in the class; each method sees a fresh copy |
Test.startTest() / Test.stopTest() | Code between them gets a fresh set of governor limits; at stopTest() asynchronous work (future, queueable, batch) runs synchronously |
System.runAs(user) | Runs code as a given user to test sharing and permissions; also avoids mixed DML errors in tests |
Assert.areEqual, Assert.isTrue, System.assertEquals | Check results |
Test.setMock(HttpCalloutMock.class, mock) | Return fake HTTP responses; tests cannot make real callouts |
Test.loadData | Load test records from a CSV static resource |
@IsTest(SeeAllData=true) | Lets the test see org data; avoid it, because results depend on the org |
By default, tests cannot see existing org data (except some setup data such as users and profiles). A query for Accounts in a test with no setup returns nothing.
Testing different code
| Code | Approach |
|---|---|
| Triggers | Insert, update or delete records; test 1 record, 200 records and invalid data |
| Controllers and extensions | Test.setCurrentPage(Page.MyPage), set parameters with ApexPages.currentPage().getParameters().put(...), construct the controller and call its actions |
| Batch and queueable | Start them between startTest() and stopTest(), then assert |
| Callouts | A class implementing HttpCalloutMock; branch on req.getEndpoint() to mock several endpoints |
| Permission-sensitive code | System.runAs() with a user who lacks access |
| Flows | Flow tests in Flow Builder for record-triggered flows, or Apex tests that perform the DML that triggers the flow |
| Lightning Web Components | Jest tests in the local project; not part of Apex coverage |
Test data factory
A public @IsTest utility class with static methods such as createAccounts(Integer n) gives every test consistent, valid data and keeps setup in one place.
Coverage arithmetic
Coverage counts executable lines only; comments and blank lines are ignored. If a class has 70 executable lines and tests run 56 of them, coverage is 80%.
A worked example
A trigger rejects contacts without Email using addError(). A good test inserts 200 contacts with Database.insert(contacts, false), three without Email. It checks that the three SaveResult entries failed with the trigger's message, and that 197 contacts exist afterwards. That one test proves bulk behaviour, the rejection path and the message, which coverage alone never shows.
Exam answers that "increase coverage" without assertions, or use SeeAllData=true to find data, are almost always wrong. Create test data and assert results.
How the exam asks about this
- "Async code must finish before the assertions." Wrap it in
Test.startTest()andTest.stopTest(). - "Test that a user without access sees no records."
System.runAs(). - "Code makes a callout."
Test.setMockwith anHttpCalloutMock.
What to remember for the exam
- Production deploys need 75% overall coverage, coverage on every trigger, and all tests passing.
- Tests do not see org data by default; create it, ideally in
@TestSetupor a factory. startTest()gives fresh limits;stopTest()runs queued async work.System.runAs()tests user access and avoids mixed DML errors.- Mock callouts with
HttpCalloutMock; real callouts are not allowed in tests.
A test method with no assertions can reach 100% coverage and still let a broken trigger reach production. Every test needs at least one meaningful assertion.
Common mistakes
Relying on existing records. Tests without SeeAllData see no org data, and results would vary between orgs anyway.
Testing with one record only. Bulk bugs appear only with 200 records.
Asserting after an async call without stopTest(). The job has not run yet.
Check yourself
What does
Test.stopTest()do to a queueable job enqueued afterstartTest()?It runs the job synchronously, so the test can assert its results.
What does
[SELECT COUNT() FROM Account]return in a test with no setup data and no SeeAllData?Zero, because tests do not see org data by default.
How do you test code that makes an HTTP callout?
Implement
HttpCalloutMockand register it withTest.setMockbefore calling the code.
Topic 1 of 4 in Testing, Debugging, and Deployment
Test yourself on Testing, Debugging, and Deployment
The full Salesforce Platform Developer question bank has 645 questions, with timed practice exams and an explanation for every answer.
This study guide is independent and is not affiliated with, endorsed by, or sponsored by Salesforce. Vendor names, certification names and exam codes are trademarks of their respective owners and are used for identification only.