Salesforce Interview Questions & Answers Book is now available!

Grab Now

Apex testing course

Learn Apex Testing: Write Test Classes That Prove Your Code Works

Learn Apex testing hands-on: test data, bulk tests, the Assert class, runAs, async and callout mocks. Write Salesforce test classes that prove behaviour, not just 75%.

By Teja, ex-Salesforce engineer · Updated October 6, 2026 · 5 min read

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.

Apex Test Fest40 episodes · 5 hours
  • 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 blocksDiscountAboveThirtyPercentForSalesReps explains 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:

  • @TestSetup builds 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() and Test.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 is with sharing and 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() and Test.stopTest(). The job finishes at stopTest, so your assertions afterwards see its results.
  • Callouts. Write a class that implements HttpCalloutMock, return a canned response, and register it with Test.setMock before 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 try block, call Assert.fail straight after it, and in the catch assert 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

  1. Tests with no assertions. They raise coverage and prove nothing. Every test method should assert something meaningful.
  2. Testing with one record. Bulk bugs only show up with many records. Use 200.
  3. Using @IsTest(SeeAllData=true). Tests that depend on org data break when someone deletes a record. Create what you need.
  4. Hard-coded IDs. Query record types and profiles by developer name or name instead.
  5. Skipping negative paths. Assert that invalid input throws the right exception and that validation blocks what it should.
  6. Never testing as a restricted user. If your code runs with sharing, use System.runAs with a user who has limited access, or you're not testing sharing at all.
  7. Calling async code without Test.stopTest(). The Queueable never runs, so your assertions check nothing.
  8. 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

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 runAs tests 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.

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

Keep me in the loop!

Email me when a new episode is published (promise, I will not spam you)

You're signing up to receive emails from Salesforce Casts