Dependency injection is great.
Making your business logic dependent on interfaces is great.
But, did you ever find it cumbersome to mock those interfaces such that all of your business logic is well exercised?
Yeah, me too. In this article I’ll demonstrate how I organize my mocks to keep the bloat to a minimum.
When looking at advice on mocking services in unit tests you usually get one of two pieces of advice.
- Make sure your service is implementing an interface, then implement that interface, except with simulated behavior, and use that in your test.
- Create a mock of each individual operation you need inside each unit test method.
#1 suffers from some issues. You almost never have a single set of data that will adequately exercise your business logic. Do you really want to implement a whole interface in order to have a GetCustomer method return a valid customer, another interface to have it return a null and another to have it throw an exception? No, you don’t want to implement what might end up being dozens of interfaces.
#2 is also a pain. You end up repeating a significant amount of mocking code across all of your tests because you are basically doing #1 except without defining new classes. And it can be even worse than #1 because some tests will of course use some of the same simulations.
Instead, create a class for each service interface and implement all the mocks you will need. In these methods, accept as parameters what the method returns, as well as an Exception. This way, one or two methods can cover nearly any simulation you could need. Let’s make it clear with an example:
The examples will use the Moq libray, but the concepts are applicable to any mocking framework. The full Visual Studio 2017 solution can be found on GitHub. It comes complete with functioning unit tests that exercise the business logic of a rudimentary stock trading application.
Lets say we have the following inteface that when implemented, will be used by our business logic layer to make stock trades. (Trade is a class with ticker symbol, price, date, buy or sell, etc)
Additionally we have a logging service to log exceptions:
Like good little software architects, we have designed our business logic to take the two services as constructor injected dependencies:
This class has a single public method called GetRich(). It performs an ingenious algorithm to make a killing on the stock market. Let’s call the strategy “buy low, sell high”. Revolutionary right? Well, before you go off and get rich, please at least finish reading…
The things we will want to test are:
- When the last trade was a Buy, execute a Sell if the current stock price is at least 15% higher than what we last bought it for.
- When the last trade was a Sell, execute a Buy if the current stock price is at least 15% lower than what we last sold it for.
- For both Buys and Sells, make sure no trade takes place if the 15% threshold is not reached.
- For both Buys and Sells, if an exception is thrown, make sure the correct message is logged
If we were designing this per the advice of #1, we already have 6 implementations of the IStockService interface.
Here is a class that will implement all service operations that we need to cover all of our tests. If new simulations are thought of or needed in the future then we will just add them in at that time. I have implemented a small variety of scenarios for demonstration, but notice most methods accept as a parameter the same thing that the method returns. In this way you can service any number of simulations because the caller will decide what it wants back:
The class is static, and if your eyes haven’t glazed over by the great wall of code you may have noticed that is because all the methods are extension methods on the Mock of the interface we are simulating.
Finally getting to the point, now that we have an extension method for every interface method, we can chain up a mock that does exactly what we want for each individual method. First we create a couple of Trade instances that are simulations we want returned from our “service”. Then we chain together the service methods that are called within StocksLogic.GetRich(). The chained methods, when called by the business logic, will simply return the simulated data.
We can then assert that indeed a Buy was done since the last trade was a sell, and the price threshold was reached. We can even assert that Sell was never called, and that Log was never called.
This ends up being pretty good test coverage for the scenario where we want a Buy trade to occur.
Take a look at the entire Visual Studio solution for the bigger picture and wider test coverage.