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.
Let's 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.)
namespace Service
{
public interface IStockService
{
Trade Buy(string ticker, decimal nbrShares);
decimal GetCurrentPrice(string ticker);
Trade GetLastTrade(string ticker);
Trade Sell(string ticker, decimal nbrShares);
}
}
Additionally, we have a logging service to log exceptions:
namespace Service
{
public interface ILogService
{
void Log(string logMessage);
}
}
Like good little software architects, we have designed our business logic to take the two services as constructor injected dependencies:
using Service;
using System;
namespace Business
{
public class StocksLogic
{
private readonly IStockService _stockService;
private readonly ILogService _logService;
public StocksLogic(IStockService stockService, ILogService logService)
{
_stockService = stockService;
_logService = logService;
}
public Trade GetRich(string ticker)
{
try
{
Trade lastTrade = _stockService.GetLastTrade(ticker);
decimal currentPrice = _stockService.GetCurrentPrice(ticker);
if (lastTrade.Side == "sell")
{
if (currentPrice <= (lastTrade.TradePrice - (lastTrade.TradePrice * .15m)))
{
return _stockService.Buy(ticker, 200);
}
}
else
{
if (currentPrice >= (lastTrade.TradePrice + (lastTrade.TradePrice * .15m)))
{
return _stockService.Sell(ticker, 200);
}
}
return new Trade { Ticker = ticker, Side = "none" };
}
catch (Exception ex)
{
_logService.Log(ex.Message);
return new Trade { Ticker = ticker, Side = "none" };
}
}
}
}
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:
using Moq;
using Service;
using System;
namespace Business.Tests
{
public static class StockServiceMocks
{
public static Mock<IStockService> GetCurrentPrice_Mock(this Mock<IStockService> mock,
decimal currentPrice, Exception ex = null)
{
if (ex != null)
{
mock.Setup(m => m.GetCurrentPrice(It.IsAny<string>())).Throws(ex);
return mock;
}
mock.Setup(m => m.GetCurrentPrice(It.IsAny<string>())).Returns(currentPrice);
return mock;
}
public static Mock<IStockService> GetLastTradeThrowsException_Mock
(this Mock<IStockService> mock, Exception ex)
{
mock.Setup(m => m.GetLastTrade(It.IsAny<string>())).Throws(ex);
return mock;
}
public static Mock<IStockService> GetLastTrade_Mock
(this Mock<IStockService> mock, Trade tradeToReturn)
{
mock.Setup(m => m.GetLastTrade(It.IsAny<string>())).Returns(tradeToReturn);
return mock;
}
public static Mock<IStockService> Buy_Mock(this Mock<IStockService> mock,
Trade tradeToReturn, Exception ex = null)
{
if (ex != null)
{
mock.Setup(m => m.Buy(It.IsAny<string>(), It.IsAny<decimal>())).Throws(ex);
return mock;
}
mock.Setup(m => m.Buy(It.IsAny<string>(), It.IsAny<decimal>())).Returns(tradeToReturn);
return mock;
}
public static Mock<IStockService> BuySharesOfContrivedExample(this Mock<IStockService> mock,
decimal nbrShares, decimal tradePrice)
{
mock.Setup(m => m.Buy("CEX", nbrShares)).Returns(new Trade
{
Ticker = "CEX",
Side = "buy",
TradeDate = DateTimeOffset.UtcNow,
TradePrice = tradePrice
});
return mock;
}
public static Mock<IStockService> Sell_Mock(this Mock<IStockService> mock,
Trade tradeToReturn, Exception ex = null)
{
if (ex != null)
{
mock.Setup(m => m.Sell(It.IsAny<string>(), It.IsAny<decimal>())).Throws(ex);
return mock;
}
mock.Setup(m => m.Sell(It.IsAny<string>(), It.IsAny<decimal>()))
.Returns(tradeToReturn);
return mock;
}
}
}
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.
[TestMethod]
public void BuyWhenLastTradeWasSell_Test()
{
#region setup the stock service
decimal simulatedCurrentPrice = 8.03m;
Trade simulatedLastTrade = new Trade
{
Ticker = "ABC",
Side = "sell",
TradeDate = DateTimeOffset.Now.AddHours(-1),
TradePrice = 10.00m
};
Trade simulatedBuy = new Trade
{
Ticker = "ABC",
Side = "buy",
TradeDate = DateTimeOffset.Now,
TradePrice = 11.50m
};
var stockServiceMock = new Mock<IStockService>()
.GetCurrentPrice_Mock(simulatedCurrentPrice)
.GetLastTrade_Mock(simulatedLastTrade)
.Buy_Mock(simulatedBuy);
#endregion
#region setup the log service
var logServiceMock = new Mock<ILogService>().Log_Mock("Should not get called");
#endregion
StocksLogic stocksLogic = new StocksLogic(stockServiceMock.Object, logServiceMock.Object);
Trade theBuy = stocksLogic.GetRich("ABC");
Assert.IsNotNull(theBuy);
Assert.AreEqual("buy", theBuy.Side);
Assert.AreEqual(theBuy.TradeDate, simulatedBuy.TradeDate, "Simulated buy date equals buy date");
stockServiceMock.Verify(m => m.Buy(It.IsAny<string>(), It.IsAny<decimal>()), Times.Once);
stockServiceMock.Verify(m => m.Sell(It.IsAny<string>(), It.IsAny<decimal>()), Times.Never);
logServiceMock.Verify(m => m.Log(It.IsAny<string>()), Times.Never);
}
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.