Home > Software design >  Is a stub required to unit test try-catch block?
Is a stub required to unit test try-catch block?

Time:08-05

I have some code that contains a try-catch block. Upon checking my code coverage, I noticed that my catch block isn't covered.

public bool TryDoSomething()
{
    try
    {
        SomePrivateMethod();
      
        return true;
    }
    catch(Exception e)
    {
        // some logs

        return false;
    }
}

I know that, using a stub, I can easily make it throw for me, thus entering the catch block. But what if the code is self contained like the example above?

Do I have to somehow extract the code just to make it throw? Like this

public bool TryDoSomething()
{
    try
    {
        _someDependencyInjectedEarlier.SomeMethod();
      
        return true;
    }
    catch(Exception e)
    {
        // some logs

        return false;
    }
}

Or the other way around (feels odd):

public bool TryDoSomething()
{
    return _injectedHelper.Try<Exception>(
        () => SomePrivateMethods(),
        e => { /* some logs */ }
    );
}

with Try being

    public bool Try<T>(Action action, Action<T> one rror) where T: Exception
    {
        try
        {
            action();
            return true;
        }
        catch (T e)
        {
            one rror(e);
            return false;
        }
    }

Or maybe some IL generated at runtime to make it throw? Which seems very complicated and wrong as well.

What are my options? Should I be posting this on another sub instead?

CodePudding user response:

You need to use mocking frameworks like Mockito to write unit tests. Here is an example from 2013 using Mockito with PowerMock.

Mocking private method with Mockito

With a mocking framework, you intercept calls to real methods and provide mocked responses without actually executing the called methods. In this case, since you want to trigger an exception, you need to have Mockito intercept the call and throw an exception.

Here is example Mockito code to do that:

doThrow(new Exception()).when(mockedObject).SomePrivateMethod(...);

You may want your original code to be restructured to make mocking that called method easier.

CodePudding user response:

When writing unit tests for your objects, don't think of mocks in terms of "methods" or "try blocks". Think of mocks in terms of two things:

  • Inputs
  • Dependencies

The unit test provides those two things, in the form of directly inputting values and mocking dependencies.

So the question isn't:

How can I get this method call to fail?

But rather:

What input or dependency can cause this functionality to fail?

As a contrived example, imagine the method being invoked does this:

return 1   1;

There are no inputs, no dependencies. All of the logic is fully enclosed within the object being tested. And that logic can't fail. So there's no need for a try/catch in the first place and the solution is to remove that structure.

Or, suppose the method being invoked does this:

return 1 / this.x;

Where does this.x come from? That's potentially an input. For example, maybe it's supplied to the constructor when creating the object. In this case, if you were to supply 0 to the constructor, you'd trigger an exception. The unit test should supply 0 and validate the result.

Or, suppose the method being invoked does this:

return this._someService.SomeOperation();

What is _someService? Is that another object on which this object depends? Then that's a dependency to be mocked. If it's being internally constructed:

this._someService = new SomeService();

Then now is the time to "invert the dependency" and make it a constructor parameter instead of internally creating it. If it's already a constructor parameter, that's how the unit test would provide a mock.


Overall the point is that the unit test isn't testing implementation (it doesn't care what/where the try/catch structure it, it doesn't care what private methods are invoked, etc.), it's testing functionality. And the object should advertise the inputs/dependencies it needs to perform that functionality.

If there are no inputs or dependencies then the logic shouldn't be able to fail, because such failure potential would literally be hard-coded into the logic itself. You can manually test it during development to harden it against failures. But if there are inputs and/or dependencies, those can provide a situation in which the logic being tested could potentially fail. Those should be mocked for tests.

  • Related