Achieving 100% Apex Test Coverage: A Step-by-Step Guide to Covering Catch Blocks in Salesforce
As a Salesforce Developer, you know the mantra: Test your code. While aiming for the minimum 75% Apex test coverage is mandatory for deployment, striving for 100% is a hallmark of high-quality, reliable code. One of the most common hurdles developers face in reaching that 100% mark is covering the elusive catch block.
How do you test for a failure that you’ve designed your code to handle gracefully?
The answer lies in intentionally causing the failure within your test method. In this guide, we’ll break down exactly how to force an exception to ensure your error-handling logic is tested and your code coverage is complete.
Why is Testing the Catch Block So Important?
Beyond simply satisfying the code coverage metric, testing your catch block is crucial for ensuring application robustness. Your catch block isn’t just dead code; it’s a safety net. It dictates what happens when things go wrong.
Testing it confirms that:
- Your application can handle unexpected errors without crashing.
- The user experience is managed gracefully during a failure.
- Your error-logging or fallback mechanisms work as designed.
The Scenario: A Simple Lead Creation Class
Let’s look at a common scenario: a simple Apex class designed to create a Lead record.
Apex Class: LeadCreation.cls
Apex
public class LeadCreation {
public Lead objLead;
public String lastName;
public LeadCreation() {
}
public PageReference newLead() {
objLead = new Lead(Company = 'Test', LastName = lastName, Status = 'Open - Not Contacted');
try {
// This is the line we want to succeed AND fail
insert objLead;
PageReference pg = new PageReference('/' + objLead.Id);
pg.setRedirect(true);
return pg;
} catch(DMLException e) {
// This is the block we need to cover
return null;
}
}
}
In this class, the newLead() method attempts to insert a new Lead. If the DML operation is successful, it returns a PageReference to the new record. If it fails (e.g., due to a validation rule, missing required field, etc.), it catches the DMLException and returns null.
The Challenge: How to Force a DMLException?
To cover the catch block, we need to make the insert objLead; line fail inside our test context. How can we do that? We need to find a rule that the insert operation will violate.
Looking at the Lead object, we know that LastName is a required field. Our code sets the LastName from the public property lastName. If we call the newLead() method without setting this property first, it will be null. Attempting to insert a Lead with a null LastName will cause the system to throw a DMLException—exactly what we need!
The Solution: The Strategic Test Class
Our test class will have two parts:
- The “Negative Test”: Intentionally cause the DML to fail to cover the
catchblock. - The “Positive Test”: Provide the required data to ensure the
tryblock is also covered.
Here is how to structure the test class to achieve this.
Test Class: LeadCreationTest.cls
Apex
@isTest
private class LeadCreationTest {
@isTest static void leadTest() {
LeadCreation obj = new LeadCreation();
// --- Part 1: Covering the CATCH Block (Negative Test) ---
// We call newLead() when obj.lastName is still null.
// This will cause a DMLException, which our main class catches.
PageReference resultFailure = obj.newLead();
// Assert that the catch block's logic (return null) was executed.
System.assertEquals(null, resultFailure, 'The method should return null on failure.');
// --- Part 2: Covering the TRY Block (Positive Test) ---
// Now, we set the required field.
obj.lastName = 'Testing';
// This call should succeed.
PageReference resultSuccess = obj.newLead();
// Assert that the try block's logic was executed.
System.assertNotEquals(null, resultSuccess, 'A valid PageReference should be returned on success.');
// Optional: Further verify the record was created
Lead createdLead = [SELECT Id, LastName FROM Lead WHERE Id = :resultSuccess.getUrl().substring(1)];
System.assertEquals('Testing', createdLead.LastName);
}
}
Note: I’ve updated your test logic slightly to follow best practices by asserting the results of each call (resultFailure and resultSuccess). This makes the test more robust.
Breaking Down the Test Method
- Instantiate the Class: We create an instance of
LeadCreation. At this point, thelastNameproperty isnull. - Covering the
catchBlock:- We immediately call
obj.newLead(). - Inside the method,
new Lead(..., LastName = null, ...)is created. - The
insert objLead;statement fails becauseLastNameis required. - A
DMLExceptionis thrown by the system. - The
catchblock inLeadCreation.clsexecutes, and the method returnsnull. System.assertEquals(null, resultFailure, ...)confirms that our error-handling logic worked perfectly. Thecatchblock is now covered!
- We immediately call
- Covering the
tryBlock:- We now set the missing piece of information:
obj.lastName = 'Testing';. - We call
obj.newLead()a second time. - This time, the
Leadis valid:new Lead(..., LastName = 'Testing', ...). - The
insert objLead;statement succeeds. - The
tryblock continues to execute, creating and returning a validPageReference. System.assertNotEquals(null, resultSuccess, ...)confirms the method’s success path. Thetryblock is now covered!
- We now set the missing piece of information:
Key Takeaways
- Think Like a Saboteur: To test error handling, you must find a way to break your own code intentionally. Look for required fields, validation rules, governor limits, or unique constraints that you can violate in your test.
- Isolate Test Scenarios: Test the failure path and the success path separately, even if it’s within the same method. This makes your tests clearer and easier to debug.
- Assert Everything: Don’t just run the code; assert the outcome. For a failure, assert that the expected error state occurred (e.g., a
nullreturn). For a success, assert that the expected positive outcome was achieved.
By following this simple pattern of “fail first, then succeed,” you can easily write tests that cover your catch blocks, pushing you closer to that 100% coverage goal and ensuring your Salesforce applications are as resilient as they are functional.