[SalesForce] How to unit test exception handling when the source of an exception is external to the code being tested

Let's say we have some class MyClass:

public class MyClass {
    public void doSomething {
        // do some stuff

        try {
             OtherClass.staticMethod();
        }
        catch (OtherClass.BadException e) {
            // do something with the exception
        }

        // do some other stuff
    }
}

I know that OtherClass.staticMethod can throw an exception, but:

  • the circumstances in which it might do so are not within the purview of this class or its associated unit tests
  • OtherClass has its own unit tests that are (presumably) already testing its functionality and exception handing; MyClass shouldn't have to duplicate those tests
  • OtherClass might be a black box, into which I have no insight; finding conditions that would trigger an exception would be a matter of guessing and testing

The purist in me wants to keep the unit tests implementation-agnostic, which in this case means that they shouldn't care why OtherClass.staticMethod would fail (or even that MyClass references OtherClass at all) – I just want to catch an exception.

Is there a way to generically handle this case? The closest I have seen is something like this:

public class MyClass {
    public boolean throwUnitTestExceptions = false;
    public void doSomething {
        // do some stuff

        try {
             if (Test.isRunningtest() && throwUnitTestExceptions) {
                 throw new OtherClass.BadException();
             }
             else {
                 OtherClass.staticMethod();
             }
        }
        catch (OtherClass.BadException e) {
            // do something with the exception
        }

        // do some other stuff
    }
}

I could then set the throwUnitTestExceptions flag when unit testing the exception handling.

This leaves a bad taste in my mouth, but it would work. Is there a better way that doesn't require knowing the details of OtherClass's implementation to establish conditions that would throw an exception?

Best Answer

You should probably not try to test the exception branch, and just accept the fact that you'll have less than 100% code coverage. This is based on three concepts: (1) you shouldn't include test-only code in a production code branch, (2) exception testing is sometimes non-trivial or even impossible to perform, so you simply have to statically verify (by hand) that the exception should be handled in a graceful manner, and (3) code coverage of 100% is often elusive (because of the prior reason), so one should settle for some value that is well above 75% but less than 100% (my personal target is 90%). If you can cause "otherClass" to throw an exception, great. If not, you should probably just accept that sometimes you can't test something.

While I'm not a big fan of leaving code untested, I am a big fan of being able to ship my code in a reasonable amount of time, and have that code perform as efficiently as possible. Technically, you could just not include error handling at all; the platform will still bubble up the exception in a way that the user can say "something bad happened, here was the error." Of course, the user will probably lose whatever they were working on, so I'd rather leave the exception handling untested rather than not caught at all. Ideally, you can minimize the amount of untested code you have by having a utility class that can transform an exception into a meaningful error message. This means that your code will have a minimum amount of untested code while still offering exception handling.

try {
    otherClass.staticMethod();
} catch(Exception e) {
    Utils.handleException(e);
}

You can then independently test the Utils class to get 100% in that class, while still giving yourself the best possible coverage in the class you're trying to cover. Of course, you'll probably want various types of exception handling methods, such as ones that would report an error via ApexPages.addMessages, one that would use addError on an SObject, etc.