Unit Testing with Mocks
When unit testing classes that have followed SOLID principles, we often find classes take on the responsibility of a co-ordinator rather than implementing any specific logic themselves. When testing these classes it makes perfect sense to use mocks (e.g. the Moq library) to control the dependencies being co-ordinated. However, I sometimes see instances where people have misunderstood how these mocks work and why the test ends up not testing what it appears to. This is because the tests use wildcards which means they are not really testing that the underlying code creates the required process, they are testing that the mocks themselves create that process.
Here’s some example code:
That code simply gets some objects from somewhere then passes them off to another class that applies some filtering logic, then another class that applies some sorting logic (let’s assume the logic is complicated enough that we don’t want to do it just in here). It’s just a co-ordinator class, it doesn’t really do any logic of its own.
These Are Not the Tests You’re Looking For
Imagine the test:
On the face of it, that test looks like it makes sure we’re returning the output from the repository after it’s been filtered and sorted, but it doesn’t. The only thing that tests is that the overall method returns whatever the final setup (on
mockSorter) happens to return. Because it uses
It.IsAny<> all the way through, it never actually verifies that the output of the repository is passed to the filter with the appropriate filter parameters. It never verifies that the output of the filter is passed to the sorter with the appropriate sort order. All it tests is that whatever the sorter returns, that’s what the method returns. This means it doesn’t test the process. Imagine the following implementation (which is broken, don’t do this):
That code doesn’t work at all, but the test will continue to pass because of the
It.IsAny<> in the mock setups.
Fixed That For You
So, let’s alter the test to make it work:
Now the test fails because of the broken implementation, and only starts passing when we revert back to the correct implementation. This test verifies that each step of the process is working with the output of the previous step, plus using the arguments provided to the method for filtering/sorting. This is the way that mocks should be used when testing the ‘happy path’.
Always An Exception
That’s not to say that
It.IsAny<> or other similar wildcarding has no place in tests, just that you need to be careful where to use it. Some valid use-cases are:
- When testing the error paths through code you may want the code to throw an exception on all possible inputs rather than just a few known ones, otherwise you’d be replicating logic from dependencies within your mocks. This is clearly a bad idea because that logic may change and your mocks may no longer be representative. For example:
- If the argument to the mock is generated at runtime within the system under test (e.g. an object created with
newor a callback function), so you can’t possibly create a setup for it:
- Creating happy path defaults that you’ll override in certain instances:
It’s also worth noting that that’s not the only test you need, even for the happy path. For example, the test would still pass if the implementation was hard-coded to always pass
SortOrder.Descending so there should be another test that passes
SortOrder.Ascending. It’s better to use a parameterised test for this exact instance:
You also need tests to verify what happens if those dependencies throw exceptions (if they can throw exceptions) or return nulls (if they can return nulls). You don’t need tests for the actual filtering/sorting logic here. Those will be covered in the tests for the filter/sorter classes (which is one good reason to move them to their own classes in the first place, following the Single Responsibility Principle). This allows us to use AutoFixture to generate the outputs of the repository/filter/sorter because the service class never needs to rely on those outputs having a sensible value; it just needs to be concerned with co-ordinating the process with the dependencies properly.
Banner Photo by Rodolfo Clix used with permission from Pexels
Originally published at adamrodger.github.io on October 25, 2018.