Thursday, August 20, 2020

Microsoft's Fakes and Shims

If you do unit testing you know you need to control the test environment by mocking functionality. For example, if a test requires a certain configuration, you can replace the code that normally gets the configuration from your database with code that simply returns the value the test needs.

Microsoft has mocking functionality called Fakes (which replaces the entire class) and Shims (which replaces selected methods). It is only available in the Enterprise version of Visual Studio. It's available in .Net Framework 4.0 and later and also in .Net Core with Visual Studio 2019 version 16.7 and later.

https://developercommunity.visualstudio.com/content/problem/956128/support-microsoft-fakes-on-net-core.html

Fire up your Enterprise Visual Studio 2019 and start a C#, .Net Framework, console project called StockPriceConsole.

The first thing to do is add a Class Library project called StockPrice. This is the class we will be testing. We start by adding an interface file to the StockPrice project (this is a common technique when mocking) called IStockPrice. It defines two methods and looks like this.


using System;

namespace StockPrice
{
    public interface IStockPrice
    {
        Boolean IsSymbolValid(String Symbol);
        Decimal GetStockPrice(String Symbol);
    }
}

Next we add the production implementation of IStockPrice to the StockPrice project. It implements the two methods defined in the interface. For our purposes a stock symbol must have four characters and the stock price is a random number less than 100. It looks like this


using System;

namespace StockPrice
{
    public class StockPrice : IStockPrice
    {
        public decimal GetStockPrice(string Symbol)
        {
            Random random = new Random();
            if (IsSymbolValid(Symbol))
                return (Decimal)Math.Round(random.NextDouble() * 100, 2);
            else
                throw new ArgumentException("Invalid Symbol");
        }

        public bool IsSymbolValid(string Symbol)
        {
            return (Symbol.Length == 4);
        }
    }
}

Now that we have an implementation of IStockPrice we can run it from the console. Make program.cs in StockPriceConsole look like this.


using System;

namespace StockPrice
{
    class Program
    {
        static void Main(string[] args)
        {
            StockPrice stockPrice = new StockPrice();
            String symbol = "CNET";

            Console.WriteLine(string.Format("Stock price for {0} is {1}", symbol, stockPrice.GetStockPrice(symbol)));

            Console.ReadLine();
        }
    }
}

When you run this you get output that looks something like this.


It's time to start faking it. Add a Class Library project called StockPriceTest and add a reference to StockPrice. Right-click the Reference and select Add Fakes Assembly (see below). Remember, this will only show up in the Enterprise version of Visual Studio.


You will now have a new folder called Fakes containing an XML file called StockPrice.fakes and a new reference called StockPrice.Fakes

If you open the StockPrice.fakes file you will see there's not much to it. If you have problems generating the fakes or shims you can add a Diagnostic attribute to generate useful error messages during the build.

<Fakes xmlns="http://schemas.microsoft.com/fakes/2011/" Diagnostic="true">
  <Assembly Name="StockPrice"/>
</Fakes>

You can tell if the fakes and shims were correctly generated by double-clicking on the StockPrice.Fakes reference and then expanding it. You should see something like this. If the StockPrice.Fakes node has no children, try the Diagnostics trick explained above, rebuild, and look in the Output window.


We will start by looking at Fakes which allows you to substitute an entire class. Any methods you don't redefine are still callable but do nothing and return the default value for their return type ie zero, empty string, or null. In the example below we will redefine both methods of the IStockPrice class.


using System;
using Microsoft.QualityTools.Testing.Fakes;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using StockPrice;

namespace StockPriceTest
{
    [TestClass]
    public class GetStockPriceTest
    {
        [TestMethod]
        public void CheckIBMStockPriceWithFake()
        {
            // Uses fakes to replace the entire StockPrice class
            IStockPrice stockPrice = new StockPrice.Fakes.StubIStockPrice()
            {
                IsSymbolValidString = (symbol) => { return true; },
                GetStockPriceString = (symbol) => { return 100; }
            };

            Decimal IBMStockPrice = stockPrice.GetStockPrice("IBM");

            Assert.AreEqual(100, IBMStockPrice, "IBM stock price should be 100");
        }
    }
}

This test passes because we redefined IsSymbolValid to always be true and GetStockPrice to always return 100. Note the names of the Fakes - they have their argument type(s) appended to them so we can redefine IsSymbolValue(String) separately from IsSymbolValue(Object) etc.

As I said, when you Fake a class, any methods you don't override do nothing. Shims allow you to override some methods but leave others intact. Add another test method to StockPriceTest.cs like this. Note all Shimming must take place within a ShimsContext.

        [TestMethod]
        public void CheckIBMStockPriceWithShim()
        {
            // Uses a shim to replace the IsSymbolValid method only
            using (ShimsContext.Create()) 
            {
                StockPrice.Fakes.ShimStockPrice.AllInstances.IsSymbolValidString = (methodName, symbol) => { return true; };
                IStockPrice stockPrice = new StockPrice.StockPrice();

                Decimal IBMStockPrice = stockPrice.GetStockPrice("IBM");

                Assert.IsTrue((IBMStockPrice < 100), "Invalid stock price");
            }
        }

This test replaces IsSymbolValid so that all symbols are valid, but leaves GetStockPrice intact. This is very useful when you are testing a method that calls another method in the same class. You can Shim the second method but still test the first one.

Your test results should look like this.


No comments:

Post a Comment