TL;DR
- Apex tests are classes annotated with @IsTest that create their own data, run your code and assert the result. Salesforce requires 75% org-wide coverage to deploy Apex to production.
- Coverage only proves lines ran. Good tests prove behaviour: bulk inputs, negative paths, different users, async jobs and mocked callouts.
- Apex Test Fest is dedicated to exactly those scenarios. The testing module in the core development course gives you the fundamentals first.
What the courses cover
Pulled live from the course catalogue — module by module.
- Introduction
- Doing the code coverage when we hit Exceptions
- Code Coverage for Asynchronous Apex
- Doing the code coverage by respecting the User context
- Assert Class and Assert Statements
- Doing the code coverage for Apex Classes making REST API Callout
- Doing the code coverage for REST API Apex Classes
- Doing the code coverage for SOAP API Classes
- Others
- Visualforce
- SOQL
- SOSL
- Data Manipulation Language [DML]
- Collection Framework
- Advanced Apex
- Asynchronous Apex
- Apex Test Framework
To learn Apex testing, learn to write test classes that create their own data, exercise your code with bulk and edge-case inputs, and assert the outcome. That's what Apex Test Fest teaches: covering exceptions, async Apex, different users with System.runAs, the Assert class, and REST and SOAP callouts, all built live against real code. Reaching 75% coverage is a deployment rule. Proving your code works is the actual job.
What Apex testing is and why it matters on real projects
An Apex test is a class marked @IsTest. Its methods run in an isolated context: they create records, call your code and roll everything back afterwards, so nothing persists in the org.
Salesforce enforces two rules at deployment: at least 75% of your Apex must be covered across the org, and every trigger must have some coverage. Those rules exist because Salesforce runs your tests before major releases to catch regressions in customer code.
On a real team, tests do more than unlock deployments:
- They're your regression net. When someone changes a validation rule or adds a flow, a failing test tells you before your users do.
- They document intent. A test named
blocksDiscountAboveThirtyPercentForSalesRepsexplains the business rule better than a comment. - They make refactoring safe. You can't clean up a trigger framework confidently without them.
Prerequisites
- Working Apex. You should be able to write a trigger handler and an Apex class. If not, start with the Apex guide.
- SOQL and DML basics, since tests build data and query results.
- An understanding of governor limits, because many tests exist to prove you stay within them.
What you'll be able to test afterwards
- Triggers and handlers with 200-record bulk inserts and updates
- Exception paths, including asserting the exact error your code throws
- Queueable, Batch, future and scheduled Apex
- Code that behaves differently per user, profile or permission set, using
System.runAs - Outbound REST and SOAP callouts with mocks, and your own Apex REST services
- Test data factories that keep setup in one place
A hands-on example: testing a trigger in bulk
This test covers the account trigger from the Apex guide, which copies an account's billing city onto its contacts.
@IsTest
private class AccountTriggerHandlerTest {
@TestSetup
static void makeData() {
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < 200; i++) {
accounts.add(new Account(Name = 'Test ' + i, BillingCity = 'Pune'));
}
insert accounts;
List<Contact> contacts = new List<Contact>();
for (Account acc : accounts) {
contacts.add(new Contact(LastName = 'Contact ' + acc.Name, AccountId = acc.Id));
}
insert contacts;
}
@IsTest
static void updatesMailingCityWhenBillingCityChanges() {
List<Account> accounts = [SELECT Id FROM Account];
for (Account acc : accounts) {
acc.BillingCity = 'Hyderabad';
}
Test.startTest();
update accounts;
Test.stopTest();
Integer synced = [SELECT COUNT() FROM Contact WHERE MailingCity = 'Hyderabad'];
Assert.areEqual(200, synced, 'Every contact should follow its account billing city');
}
@IsTest
static void ignoresUpdatesThatDoNotChangeBillingCity() {
List<Account> accounts = [SELECT Id FROM Account];
for (Account acc : accounts) {
acc.Description = 'Unrelated change';
}
Test.startTest();
update accounts;
Test.stopTest();
Integer touched = [SELECT COUNT() FROM Contact WHERE MailingCity != null];
Assert.areEqual(0, touched, 'Contacts should not change when billing city is unchanged');
}
}
What makes this a real test, not a coverage test:
@TestSetupbuilds data once for every method in the class, and each method gets its own fresh copy of it.- It uses 200 records. That's the trigger chunk size, so if the handler had a query inside a loop, this test would hit the SOQL limit and fail.
Test.startTest()andTest.stopTest()wrap only the code under test, which gets its own governor limits, separate from the setup.- It asserts outcomes. It checks that all 200 contacts actually changed, with a message that explains the failure.
- It tests the negative path. The second method proves the handler does nothing when it shouldn't, which is exactly where filter bugs hide.
Beyond triggers: users, async jobs and callouts
A trigger test is the easy case. The harder scenarios are where most real test suites have gaps, and where Apex Test Fest spends most of its time.
- Different users. Create a user with a restricted profile or permission set inside the test, then wrap the call in
System.runAs. If your class iswith sharingand uses user-mode queries, this is the only way to prove a sales rep can't see another team's records. - Async Apex. Enqueue the Queueable or start the batch between
Test.startTest()andTest.stopTest(). The job finishes atstopTest, so your assertions afterwards see its results. - Callouts. Write a class that implements
HttpCalloutMock, return a canned response, and register it withTest.setMockbefore calling your code. Write one mock for success and one for failure, because error handling is usually the untested half of an integration. - Exceptions. Call the method inside a
tryblock, callAssert.failstraight after it, and in thecatchassert the exception type and message. If no exception is thrown, the test fails, which is what you want.
Once you've written a few of these, move the record creation into a test data factory class so every test builds valid accounts, contacts and users the same way.
Common mistakes beginners make
- Tests with no assertions. They raise coverage and prove nothing. Every test method should assert something meaningful.
- Testing with one record. Bulk bugs only show up with many records. Use 200.
- Using
@IsTest(SeeAllData=true). Tests that depend on org data break when someone deletes a record. Create what you need. - Hard-coded IDs. Query record types and profiles by developer name or name instead.
- Skipping negative paths. Assert that invalid input throws the right exception and that validation blocks what it should.
- Never testing as a restricted user. If your code runs
with sharing, useSystem.runAswith a user who has limited access, or you're not testing sharing at all. - Calling async code without
Test.stopTest(). The Queueable never runs, so your assertions check nothing. - Using
Test.isRunningTest()to bypass logic. Branching production code for tests means the real path is never tested. Use mocks and dependency injection instead.
The longer version, with examples, is in Apex test classes beyond 75% coverage.
How testing connects to the other topics
- Everything you write in Apex needs tests, and interviewers will ask about them: see Apex interview questions and async Apex questions.
- Callout mocks and REST service tests are essential for Salesforce integration work.
- Deployments run your tests. The Salesforce DX and VS Code guide shows how to run them from the command line, including in CI.
- On the Salesforce developer roadmap, testing sits right after triggers and before integrations, which is deliberate.
Who should skip this
- Skip it if you don't write Apex yet. Learn Apex first. Testing code you don't understand is frustrating.
- Skip the basics if you already write bulk, negative-path and
runAstests by habit. You might still find the callout and SOAP mocking sections useful. - Don't skip it if your tests mostly exist to reach 75%. That's the most common gap I see in developer interviews.
Apex Test Fest
40 episodes · 5 hours · 9 modules
Salesforce Development (Visualforce & Apex)
47 episodes · 5.6 hours · 8 modules
Learn it by building it
Every course, every code repository, the discussion group and certificates of completion — one subscription.
Frequently asked questions
What is the minimum code coverage needed to deploy Apex?
Salesforce requires at least 75% of your Apex code to be covered by tests when you deploy to production, and every trigger needs some coverage. Treat 75% as the floor, not the goal.
Why are my Apex tests failing in production but passing in a sandbox?
Usually because they depend on existing org data, hard-coded IDs or configuration that differs between orgs. Tests should create every record they need, which is the default since tests can't see org data unless you use SeeAllData.
What does Test.startTest and Test.stopTest do?
Code between them gets a fresh set of governor limits, and any asynchronous work queued inside, such as Queueable or future methods, runs synchronously when Test.stopTest is called so you can assert its results.
Should I use System.assertEquals or the Assert class?
Use the Assert class, such as Assert.areEqual and Assert.isTrue, for new code. It reads more clearly and includes Assert.fail for paths that should never be reached. The older System methods still work.
How do I test an Apex callout?
Tests can't make real callouts. Implement HttpCalloutMock (or WebServiceMock for SOAP), register it with Test.setMock, and assert how your code handles both successful and failing responses.
Keep going
Topic guide
Learn Apex: A Hands-On Salesforce Apex Course for Developers
Blog
Apex Test Classes: Stop Chasing 75% Coverage
Interview questions
Apex Interview Questions and Answers (2026)
Career roadmap
Salesforce Developer Roadmap 2026
Topic guide
Learn Salesforce Integration: REST, OAuth, Named Credentials, Platform Events and Bulk API
Blog
Apex Bulkification Explained: From 'Too Many SOQL Queries: 101' to Bulk-Safe Code