Advanced Exception Handling in Salesforce Apex Unit Tests

A view on improving (test) code quality by implementing an Exception Testing Framework

Justus van den Berg
10 min readAug 23, 2023

We as Salesforce developers love to reach a 100% code coverage on all our Apex Classes, but there are some exceptions…
Even if you handle these exceptions correctly, you might find that there are scenarios where no exceptions are thrown unless something really bad and unforeseen happens. Some exceptions may not be replicateable in Apex Unit Tests at all.

Joking and coverage aside: You want to validate that your code handles all exceptions as expected, even the ones that should never occur.
To test difficult to reach catch blocks, a common approach is the addition of test specific variables with the use and abuse of Test.IsRunningTest() and the @TestVisible annotation to force Exceptions in order to reach the exception logic.

In principle there is nothing wrong this approach; this is the reason the Test Class Methods and the @TestVisible Annotation exist in the first place.
But it can get messy and very difficult to read and/or understand if you don’t define any rules or coding standards around these “test flag variables”.

Another often overlooked scenario is how a unit test should handle false positives when no exception is thrown when it should have been. This requires an extra line of code in your unit test and is prone to be forgotten or implemented in various interesting ways. I’ll give an example scenario later on.

If you work on a small code base with only a few developers this might not sound like a big issue. But when you scale it to multiple teams that work individually on the same org(s) with potentially hundreds of developers, a standardized approach starts to make sense.

Exceptions and testing them is not very complex, this article’s goal is creating awareness that by adopting a simple framework you can improve code readability, maintainability and overall quality as well as remove a lot of duplicate boiler plate code.

TL;DR :: An Apex Unit Test Utility

I created an Apex Unit Test Utility where one of the features is the testing of exceptions. Through out this article I’ll be referencing the methods from that utility.

The Github repository can be found here:
https://github.com/jfwberg/lightweight-apex-unit-test-util-v2

The managed package can installed using this link:
/packaging/installPackage.apexp?p0=04tP30000007oePIAQ

The unlocked package can installed using this link:
/packaging/installPackage.apexp?p0=04tP30000007og1IAA

Scenarios

I’ll discuss the different scenarios in the below section. The code examples are simplified to get the point across and some things might not make a sense in real-life code.
When I talk about exception handling, it refers to exceptions that are caught in a try/catch block and there is some logic that handles that exception.
A good example for exception handling is a using a logger like Nebula Logger.

Difficult to reach catch blocks

This is one of the most common scenarios: if your code works well, no exception will be thrown what makes it impossible to test without extra work.

try{
// Execute logic
System.debug('I will always work');

}catch(Exception e){
// This code is never reached unless something really goes 🍌🍌
Logger.error(e.getMessage());
}

A solution would be adding a private variable that is only updatable through a test method. We then add in some logic that throws an exception when we set that variable to true and catch it in the test method.

In this example we create a variable called forceException that we can set to true in our test method and will throw a custom StringException.
It would look something like this:

@TestVisible
private static Boolean forceException = false;

public static void exceptionThrowingMethod(){
try{
// We can force this exception by setting forceException to true
// in our test method
if((Test.IsRunningTest() && forceException)){
throw new StringException('Exception forced from Unit Test');
}

// Execute logic
System.debug('I will always work');

}catch(Exception e){
Logger.error(e.getMessage());
}
}

You might think: What is the problem with this approach? Well a number of things:

  • It requires to have a variable for each exception: We can reuse the same variable or create a map or list, but that makes it unclear when what exception is forced if you read through the the code.
  • If you have multiple classes, you are going to have multiple variables and you might get a lot of code duplication.
  • It requires an extra if statement with additional logic to throw a custom exception with a custom message. These are an additional three things that add to the complexity of the method and three additional steps that need to be repeating in multiple places.
  • You have a lack of control in a scenario where there is combination of (nested) exceptions that are thrown in different scenarios. You might need a way force or ignore multiple exceptions in the same test to validate a result. Doing it this way, complicates that process.
  • You are introducing a control switch mechanism inside an Apex Class, this might break the separation of concerns paradigm if you put it in the wrong place.
  • If you have a free for all between teams; naming conventions, variable types and variable locations might go all over the place, reducing code readability, maintainability and overall quality.

A framework as a solution

If you take a framework or utility approach, you can package the exception forcing logic into a utility package that can be used for all your Apex Exception testing.
In the below example I use a test utility class called “Tst” in the “utl” namespace that contains a method called forceException() with a string parameter to set an identifier. We match this identifier in the test method to throw the exception exactly when we need to.

Naming convention note: I prefer my utility methods (especially packaged ones) to have short names. Long repeated names clog up your code. It’s purely a preference. Method names should be descriptive, but the repeated prefixes can be short if they are clear enough. This is purely my personal preference and not best practice or anything. Always follow your company’s coding guidelines.

You execute the forceException() method in the part of your code where it makes most sense and fits in with the logic flow. If the sole purpose is to force the exception, the top of the method makes the most sense because no logic needs to be executed. (Every tiny bit of performance improvement helps)

public static void exceptionThrowingMethod(){
try{
// Force an exception using a utility method
utl.Tst.forceException('DEBUG_ALWAYS_WORK_MESSAGE_EXCEPTION');

// Execute the logic as normal
System.debug('I will always work');

}catch(Exception e){
throw new StringException('Custom Handled Exception Message');
}
}

If we look at this example, we don’t have to worry about any variables or custom exceptions that we throw and handle, our utility does the magic for us.
It’s now also a lot easier to read: An exception is forced, if in the test method the same identifier is added.
The identifier is a string so we can give it a human readable name. It can be anything, but I prefer to stick to a constant like naming convention.

Using this approach, all developers in all teams can use the same methodology to implement exception testing in their code with minimal effort and maximum readability.

The test method looks something like this:

@IsTest
static void testLogic(){

// Add the exception identifier
utl.Tst.addForcedException('DEBUG_ALWAYS_WORK_MESSAGE_EXCEPTION');

try{
// Execute the logic. This should now throw an exception
Logic.exceptionThrowingMethod();

}catch(Exception e){
// Assert that the exception message matches with the default
// forced message
utl.Tst.assertForcedExceptionMessage(e);
}
}

In the test method we call a method named utl.Tst.addForcedException(). As a parameter we match the identifier that we specified in our apex method.
This tells that if the code runs in a test and this identifier has been added in the test, the utility will throw an exception that we control.

To simply validate that the exception message matches the default message thrown by the forcedException message there is a method called utl.Tst.assertForcedExceptionMessage(e) that can be used.

The utility comes with a couple of additional methods to validate common exception message types. utl.Tst.assertExceptionMessage() is the most basic one. More details on additional utilities later.

Conditional logic

Another common, almost similar scenario is a condition that is difficult or impossible to meet; for example guard clauses.
Here is an example to illustrate how an if statement who’s condition is always equal to false, will need additional code to reach the exception.

public static void exceptionThrowingMethod(){
try{
// Because this is always false, the exception is never thrown
if(false){
throw new StringException('This exception is never thrown');
}

// Execute the logic as normal
System.debug('I will always work');

}catch(Exception e){
// This code is never reached
Logger.error(e.getMessage());
}
}

This can be sorted in a similar way as the first scenario: by adding a variable and use that in an inline check like this:

@TestVisible
private static Boolean forceException = false;

// Now we can add an or statement
if(false || (Test.IsRunningTest() && forceException)){
throw new StringException('This exception is never thrown');
}

Exactly the same framework solution applies in this case only we update to a different method that uses the same principle. The utl.Tst.forceCondition() method. This method returns a Boolean that returns TRUE if the test is running and the matching identifier has been added in the test method.

// Now we can add an or statement
if(false || utl.Tst.forceCondition('DEBUG_ALWAYS_WORK_MESSAGE_EXCEPTION')){
throw new StringException('This exception is never thrown');
}

We now need to update the method in the test to utl.Tst.addForcedCondition() so it will use the condition logic.

@IsTest
static void testLogic(){

// Add the CONDITION identifier
utl.Tst.addForcedCondition('DEBUG_ALWAYS_WORK_MESSAGE_EXCEPTION');

try{
// Execute the logic. This should now throw an exception
Logic.exceptionThrowingMethod();

}catch(Exception e){
// Assert that the exception message matches with the one thrown
utl.Tst.assertExceptionMessage(
'This exception is never thrown',
e
);
}
}

In this case the exception message has been defined by the developer. A method to assert the custom exception messages called utl.Tst.assertExceptionMessage(expectedMessage, Exception) is used here to make life easier.
There are a number of method overloads to allow for formatted messages like “Field {0} does not exist on sObject {1}” to assert common exception messages with ease.

Dealing with false positives

What do I mean by false positives? This is a scenario where in a test method you catch an exception and assert it in the catch block of the test method. But in reality no exception was thrown at all.
The test method passes correctly, but no exception was asserted and your test becomes invalid.

@IsTest
static void testException(){
try{
// This method should thrown an exception, but if it does not,
// the test method still passes, giving us a false positive
Logic.exceptionThrowingMethod();

}catch(Exception e){

// This asserting code is NOT reached if no exception is thrown
// making the test a false positive
Assert.areEqual(
'Expectected Exception Message',
e.getMessage()
)
}
}

A solution for this is to throw an exception in your test method that is after the logic where you expected an issue.
The custom exception that we throw is a safe guard against reaching part of the code we should not have reached.

@IsTest
static void testException(){
try{
// This method should throw an exception, if not it continues
Logic.exceptionThrowingMethod();

// This part of the code should not be reached, because we expect
// that an exception has been thrown that our test caught
throw new UnitTestException(
'This part of the code should not have ' +
'been reached. Expected that an exception was thrown.' +
'Validate why no exception has been thrown'
);

}catch(Exception e){
// Will now FAIL, because the code threw a UnitTextException
Assert.areEqual(
'Expectected Exception Message',
e.getMessage()
);
}
}

The utility approach is a standard method that can be put in place in all exception test methods. It’s called utl.Tst.assertExceptionHasBeenThrow() and looks like this when implemented:

@IsTest
static void testException(){
try{
// This method should throw an exception, if not it continues
Logic.exceptionThrowingMethod();

// Make sure this part of the code is not reached without throwing an error
utl.Tst.assertExceptionHasBeenThrow();

}catch(Exception e){
// Will now FAIL, because the get the above error message
Assert.areEqual(
'Expectected Exception Message',
e.getMessage()
);
}
}

This makes it a single line that reads what that part of the code does. This can be reused in every test method that tests exceptions, making everyone’s life a lot easier.

Lightweight - Apex Unit Test Utility v2 functionality

The Apex Unit Test Utility v2 package contains a set of pre-built methods that you can implement in your Apex Code. You could modify it, extend it etc. It’s purpose is to give a good idea how even a lightweight solution can have a big impact.

All methods are in the class called “Tst” and if you install the managed or unlocked package you will have to add the “utl” namespace as well. As per the examples above.

If you decide to implement your own version you should remove the global modifiers and make everything private with @TestVisible annotation. The methods that are to be implemented in normal apex code should be made public. There is an example in the force-app/custom folder in the repository.

Note

Some argue that “normal” code should not be responsible for allowing the testing of exceptions and that is why 75% coverage should be enough to ignore these scenarios.
My personal view is that forced exceptions should only be implemented where it makes sense and where exception handing logic is in place. Testing a one liner that cannot fail like a debug statement does not need extra testing. A one liner with a DML statement or an HTTP Call-out on the other hand, definitely should.

Final notes

I hope this was insightful and gives some food for thought. I cannot state the importance of proper testing and not just reaching a coverage goal enough. As always feel free to leave any feedback.

At the time of writing I am a Salesforce employee, the above article describes my personal views and techniques only. They are in no way, shape or form official advice. It’s purely informative.
Anything from the article is not per definition the view of Salesforce as an Organization.

Image by: Ferenc Almasi

--

--