The Apex Testing Handbook: From Unit Tests to Full Test Coverage
By Kodelens Team
•October 30, 2025

For many Salesforce developers, Apex tests are seen as a chore—a bureaucratic hurdle you have to clear to meet the 75% code coverage requirement for deployment. But this view misses the entire point.
Apex tests are not a tax you pay to Salesforce. They are your single best insurance policy against future bugs. They are the safety net that lets you refactor old code with confidence. They are the living, breathing documentation of how your application is supposed to behave.
The problem is, many developers write tests that only aim to cover lines of code, not to actually validate functionality. This leads to a false sense of security and brittle tests that break with every minor change.
Moving from "coverage-focused" testing to "behavior-focused" testing is the hallmark of a professional developer. This handbook will guide you through the principles and patterns needed to write tests you can actually trust.
The Pillars of a Great Apex Test
Before writing a single line of code, understand the principles that separate a great test from a mediocre one.
- Pillar 1: It Must Be Isolated. A test should create its own data from scratch and never rely on data that already exists in your sandbox. This ensures your test is repeatable and won't fail just because someone changed a record. The
@isTest(SeeAllData=false)annotation is your best friend and the default for a reason. - Pillar 2: It Must Be Assertive. A test method without a
System.assertEquals(),System.assertNotEquals(), orSystem.assert()call is not a test; it's just code that runs for the sake of coverage. The assertion is the moment of truth—it's where you prove your code behaved exactly as you expected. - Pillar 3: It Must Test a Single "Thing". Each test method should have a clear, singular purpose. Avoid giant test methods that try to validate ten different outcomes. Instead, create multiple, descriptively named methods like
test_calculateDiscount_forVIPCustomer()ortest_throwException_forNegativeQuantity().
The Test Data Factory: Your Most Important Utility
This is the single most important pattern for writing clean, scalable, and maintainable tests.
The Problem: If you create test data (new Account(...), insert acc;) manually at the top of every single test method, your code becomes incredibly repetitive and hard to maintain. What happens when you add a new required field to the Account object? You have to go and fix 50 different test methods.
The Solution: The Test Data Factory Pattern.
Create a single, dedicated @isTest utility class (e.g., TestDataFactory.cls) with static methods for creating common records.
@isTest
public class TestDataFactory {
public static Account createAccount(String name, Boolean doInsert) {
Account acc = new Account(Name = name, Industry = 'Technology');
if (doInsert) {
insert acc;
}
return acc;
}
public static Contact createContact(Id accountId, Boolean doInsert) {
Contact c = new Contact(FirstName = 'Test', LastName = 'Contact', AccountId = accountId);
if (doInsert) {
insert c;
}
return c;
}
}
Now, your test methods become clean, readable, and focused on the logic, not the setup. Account acc = TestDataFactory.createAccount('Test Corp', true);
The Anatomy of a Test Method: Arrange, Act, Assert
Every good test method follows a simple, three-part structure.
- Arrange: Set up your test data and context. Create all the records and conditions needed for your specific test scenario, using your Test Data Factory.
- Act: Execute the single piece of code you want to test. This is usually one line that calls your method or fires your trigger. It's a best practice to wrap this action in
Test.startTest()andTest.stopTest()to get a fresh set of governor limits. - Assert: Verify the outcome. This is the most important part. Use
System.assertEquals()to check that field values are correct. Query the database to confirm that records were created, updated, or deleted as expected.
Here is a complete example:
@isTest
static void test_createPrimaryContact_onAccountInsert() {
// 1. ARRANGE: Set up the data needed for the test.
Account acc = TestDataFactory.createAccount('New Corp', false); // Don't insert yet.
// 2. ACT: Execute the code being tested.
Test.startTest();
insert acc; // This will fire the Account trigger.
Test.stopTest();
// 3. ASSERT: Verify the result was what we expected.
Contact primaryContact = [SELECT Id FROM Contact WHERE AccountId = :acc.Id AND isPrimary__c = true];
System.assertNotEquals(null, primaryContact, 'A primary contact should have been created by the trigger.');
}
Beyond the Basics: Advanced Testing Concepts
Once you've mastered the fundamentals, you can tackle more complex scenarios:
- Testing Callouts: Use the
HttpCalloutMockinterface to create a "mock" response. This allows you to test your callout logic without ever making a real API call to an external system. - Testing Asynchronous Apex: To test
@future, Queueable, or Batch Apex, place your job invocation betweenTest.startTest()andTest.stopTest(). This tells Salesforce to finish executing the async job before moving on to the next line of your test, allowing you to assert its results. - Testing for Errors: Don't just test the "happy path." Write tests that prove your code fails correctly. Use a
try-catchblock and assert that the expected exception was thrown (e.g.,catch (DmlException e)).
The Role of AI in Modern Testing
Writing tests, especially for complex or legacy code, can be tedious. The first question is often, "What should I even test?" This is where modern AI tools can help.
Understanding what to test starts with understanding the code's possible outcomes. Using a tool like Kodelens, you can quickly ask, 'What are all the possible outcomes of this method?' to identify all the scenarios you need to write tests for. Future AI-powered features could even generate the Arrange-Act-Assert boilerplate for you, letting you focus on writing the crucial assertion logic.
Conclusion: Test for Confidence, Not Just Coverage
The 75% code coverage requirement is the bare minimum—it's a passing grade, not an A+. The real goal of testing is to build a comprehensive suite of tests that gives you the absolute confidence to refactor code and deploy new features without fear.
Adopt the Test Data Factory pattern. Structure your methods with Arrange-Act-Assert. And always ask yourself, "What specific behavior am I proving with this test?"
Pick one of your existing test classes today. Can you tell what it's testing just by reading the method names? If not, spend 15 minutes refactoring it with these principles in mind. Start building your safety net.

