Apex Error Handling Testing Strategies

The inspiration for this post comes from a question on the Salesforce Ohana Slack. Join link. The person asked how does one write Apex test code to cover the error logic in a catch block. Their particular example was code for deactivating users in a particular profile who haven’t logged in in 75 days.

/*
    This code isn't the exact same Ohana Slack code but close enough.
 */

public static void deactivateProfileUsers(String profileName) {
    try {
        List<User> usersToDeactivate =
        [SELECT Id
           FROM User
          WHERE Profile.Name = :profileName
            AND IsActive = true
            AND LastLoginDate != LAST_N_DAYS:75];

        for (User userToDeactivate : usersToDeactivate) {
            userToDeactivate.IsActive = false;
        }

        update usersToDeactivate;
    }
    catch (Exception ex) {
        throw new AuraHandledException(ex.getMessage());
    }
}

Apex Error Handling Testing Strategies

Now let’s discuss different strategies for handling this. First, there’s no silver bullet here. Pick the strategy that works for you on a case-by-case basis.

No Try-Catch Block

One doesn’t always need to have a try-catch block to do error handling logic. In the code sample provided, the error logic rethrows an AuraHandledException which lets a lightning component, Aura or LWC, to have specific error information instead of the generic error message without the specific error. However, in my experience, most errors come from the DMLException when one tries to insert, update, or delete records. That exception’s detailed errors like “page errors” and “field errors” are provided to lightning components. Albeit the error object provided is hard to traverse but the data is there. That’s one reason I typically don’t wrap fairly simple DML logic like this in a try-catch. One downside is if the error isn’t a DML Exception, then we get the general error without further details for “security reasons”. One benefit is one doesn’t need to write apex test code for the catch logic.

No Test Code For Error Code

Now let’s say one has a catch block but its logic is short say 1-3 lines. If the non-error code is thoroughly tested and the class has at least 75% code coverage, or whatever your threshold is, don’t bother with test code for the error code. Blasphemy aka WTF is what some are thinking. It’s probably not worth your time to test it out. I wouldn’t write test code using the original code at the beginning since it’s one line.

Throw Exception Using Input If Possible

Causing an exception to occur in test code can often be a challenge. Sometimes “bad” input can generate a run-time exception.

Let’s change the code a little so that the profile record is queried first so one can provide a bad profile name to generate a SOQL exception. Admittedly, this isn’t actually needed but is useful for demo sake.

/*
    This code isn't the exact same Ohana Slack code but close enough.
 */

public static void deactivateProfileUsers(String profileName) {
    try {
        Profile profile =
        [SELECT Id
           FROM Profile
          WHERE Name = :profileName];

        List<User> usersToDeactivate =
        [SELECT Id
           FROM User
          WHERE ProfileId = :profile.Id
            AND IsActive = true
            AND LastLoginDate != LAST_N_DAYS:75];

        for (User userToDeactivate : usersToDeactivate) {
            userToDeactivate.IsActive = false;
        }

        update usersToDeactivate;
    }
    catch (Exception ex) {
        throw new AuraHandledException(ex.getMessage());
    }
}

Giving the deactivateUsers a non-existent profile name causes a SOQL error. That error will then be caught and rethrown so the test error code can assert the expected error occurred.

// Test Code
String nonExistentProfile = 'adflasdflasjffasdlk';
deactivateProfileUsers(nonExistentProfile); // Throws a SOQL query because no profile is found with that name.

// Assert expected SOQL Error

One downside is that this approach isn’t always applicable as seen in the original code above. A benefit is that it doesn’t require any special logic or more sophisticated logic like mocks.

Throw Test Exception Inline Using Toggle Switch

One can write their production code to have a toggle switch to throw an exception for test purposes. This makes testing the error code easier but I would call this an anti-pattern and don’t recommend it. This is included for completeness sake. The mocking via dependency injection strategy is better.

In the example code below, the toggle switch is in the code using a static variable and the production code uses it to throw a test exception when it is true. The test code sets it to true and asserts the test exception is encountered.

public static final String TEST_EXCEPTION_MESSAGE = 'Test Error';

@testVisible
private static throwDeactivateProfileUsersTestException = false;

public static void deactivateProfileUsers(String profileName) {
    try {
        if (throwDeactivateProfileUsersTestException &&
            Test.isRunningTest()) {
            throw new AuraHandledException(TEST_EXCEPTION_MESSAGE);
        }

        List<User> usersToDeactivate =
        [SELECT Id
           FROM User
          WHERE Profile.Name = :profileName
            AND IsActive = true
            AND LastLoginDate != LAST_N_DAYS:75];

        for (User userToDeactivate : usersToDeactivate) {
            userToDeactivate.IsActive = false;
        }

        update usersToDeactivate;
    }
    catch (Exception ex) {
        throw new AuraHandledException(ex.getMessage());
    }
}
// Test Code
DeactivateProfileUsersClass.throwDeactivateProfileUsersTestException = true;

String existingProfileName = 'Admin';
DeactivateProfileUsersClass.deactivateProfileUsers(existingProfileName);

// Catch exception and assert test error encountered.

Use Mocks & Dependency Injection To Provide Different Code

Another approach is to use mocks via dependency injection. The idea here is to provide an interface or class to the function to provide the functionality. This lets one use different implementations, effectively changing the code in the function as needed. This can get really sophisticated but here’s one implementation using an interface and function parameter dependency injection.

public interface GetUsersProvider {
    List<User> getUsers();
}
// The function takes the interface as input so the calling code can provide different implementation as needed. This is one way to do dependency injection.
public static void deactivateProfileUsers(GetUsersProvider userProvider) {
    try {
        List<User> usersToDeactivate = userProvider.getUsers();

        for (User userToDeactivate : usersToDeactivate) {
            userToDeactivate.IsActive = false;
        }

        update usersToDeactivate;
    }
    catch (Exception ex) {
        throw new AuraHandledException(ex.getMessage());
    }
}
public class GetProfileUsersProvider implements GetUsersProvider {
    private profileName = '';

    public GetProfileUsersProvider(String profileName) {
        this.profileName = profileName;
    }

    public List<User> getUsers() {
        return
        [SELECT Id
           FROM User
          WHERE Profile.Name = :profileName
            AND IsActive = true
            AND LastLoginDate != LAST_N_DAYS:75];
    }
}
public class GetUsersErrorProvider implements GetUsersProvider {
    public static final String TEST_ERROR = 'Test Error';

    public List<User> getUsers() {
        throw new AuraHandledException(TEST_ERROR);
    }
}
// Regular Test
String existingProfileName = 'Some Existing Profile To Test With';
GetProfileUsersProvider profileUserProvider = new GetProfileUsersProvider(existingProfileName);

DeactivateProfileUsersClass.deactivateProfileUsers(profileUserProvider);

// assert deactivated users here.
// Error Handling Test
GetUsersErrorProvider errorProvider = new GetUsersErrorProvider();

DeactivateProfileUsersClass.deactivateProfileUsers(errorProvider);

// assert expected test error here. Try-Catch excluded for brevity.

This approach is cleaner and more flexible, in my opinion, but requires more code and takes more time to develop. This strategy is the recommended approach if needed. I will do this if there’s complex error handling logic and this is one approach to do this. Again, there are lots of ways to implement mocking and this is one approach. One can also use a mocking framework.

What strategies do you have? Any other thoughts? Please share in the comments below.

Leave a Reply

Your email address will not be published.