Click here to Skip to main content
15,867,765 members
Articles / DevOps / Testing

Organizing Service Mocks in Unit Tests

Rate me:
Please Sign up or sign in to vote.
5.00/5 (2 votes)
6 Nov 2017CPOL4 min read 7K   6   6
Minimize mocking code while still providing all necessary scenarios to exercise your business logic unit tests

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.

  1. Make sure your service is implementing an interface, then implement that interface, except with simulated behavior, and use that in your test.
  2. 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.)

C#
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:

C#
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:

C#
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")
                {
                    //Buy 200 shares if current price is at least 15% lower than last sell.
                    if (currentPrice <= (lastTrade.TradePrice - (lastTrade.TradePrice * .15m)))
                    {
                        return _stockService.Buy(ticker, 200);
                    }
                }
                else //was a buy
                {
                    //Sell 200 shares if current price is 15% higher than last buy.
                    if (currentPrice >= (lastTrade.TradePrice + (lastTrade.TradePrice * .15m)))
                    {
                        return _stockService.Sell(ticker, 200);
                    }
                }
 
                //no trade criteria met, return no trade.
                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:

C#
using Moq;
using Service;
using System;
 
namespace Business.Tests
{
    public static class StockServiceMocks
    {
        /// <summary>
        /// Causes GetCurrentPrice to return a specific price or optionally throws an exception.
        /// </summary>
        /// <param name="mock">Extension object</param>
        /// <param name="currentPrice">The current price 
        /// you want to simulate receiving from the Stock service.</param>
        /// <param name="ex">Optional exception to cause the GetCurrentPrice method to throw.
        /// </param>
        /// <returns></returns>
        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;
        }
 
        /// <summary>
        /// Simulates throwing an exception when calling GetLastTrade.
        /// </summary>
        /// <param name="mock">Extension</param>
        /// <param name="ex">The exception to throw.</param>
        /// <returns></returns>
        public static Mock<IStockService> GetLastTradeThrowsException_Mock
                              (this Mock<IStockService> mock, Exception ex)
        {
            mock.Setup(m => m.GetLastTrade(It.IsAny<string>())).Throws(ex);
            return mock;
        }
 
        /// <summary>
        /// Simulates calling GetLastTrade
        /// </summary>
        /// <param name="mock">Extension</param>
        /// <param name="tradeToReturn">The simulated trade to return</param>
        /// <returns></returns>
        public static Mock<IStockService> GetLastTrade_Mock
                              (this Mock<IStockService> mock, Trade tradeToReturn)
        {
            mock.Setup(m => m.GetLastTrade(It.IsAny<string>())).Returns(tradeToReturn);
            return mock;
        }
 
        /// <summary>
        /// Simulates a Buy and optionally throws exception.
        /// </summary>
        /// <param name="mock">Extension</param>
        /// <param name="tradeToReturn">The simulated Trade returned from Buy()</param>
        /// <param name="ex">Optional exception to throw</param>
        /// <returns></returns>
        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;
        }
 
        /// <summary>
        /// Simulates buying CEX stock specifically.
        /// </summary>
        /// <param name="mock">Extension</param>
        /// <param name="nbrShares">Number of shares to simulate buying</param>
        /// <param name="tradePrice">Simulated trade price</param>
        /// <returns></returns>
        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;
        } 
 
        /// <summary>
        /// Simulates a Sell
        /// </summary>
        /// <param name="mock">Extension</param>
        /// <param name="tradeToReturn">The simulated trade to return from Sell()</param>
        /// <param name="ex">Optional exception to throw when calling Sell()</param>
        /// <returns></returns>
        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.

C#
[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
    };
 
    //Here we simulate the current price being lower than 15% less 
    //than our last sell.
    //This should exercise the logic of a sell only.
    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);
 
    //Only exception will log so validate log was not called
    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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior) EVO Payments International
United States United States
Full stack developer on .Net and related technologies with heavy emphasis on back-end API development and integrations.

Comments and Discussions

 
Suggestionshould explain with code and example Pin
Mou_kol5-Nov-17 21:36
Mou_kol5-Nov-17 21:36 
GeneralRe: should explain with code and example Pin
Bob Crowley5-Nov-17 23:16
Bob Crowley5-Nov-17 23:16 
GeneralRe: should explain with code and example Pin
Mou_kol7-Nov-17 22:00
Mou_kol7-Nov-17 22:00 
GeneralRe: should explain with code and example Pin
Bob Crowley8-Nov-17 3:05
Bob Crowley8-Nov-17 3:05 
QuestionSomething missing? Pin
Klaus Luedenscheidt5-Nov-17 18:51
Klaus Luedenscheidt5-Nov-17 18:51 
AnswerRe: Something missing? Pin
Bob Crowley5-Nov-17 23:16
Bob Crowley5-Nov-17 23:16 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.