In the last post, we learned about the different types of tests, and how they differ in scope and purpose.
In this post, we’ll figure out ways to limit the scope of tests by controlling the behavior of external dependencies, so that we can isolate the code being tested.
Download the source code to follow along.
Exposing dependencies
Let’s revisit the Random example from the previous installment:
public Game CreateNewRandom(string playerName, string otherPlayerName) { var swap = new Random().Next(2) == 1; var player1 = new Player(swap ? otherPlayerName : playerName, 'X'); var player2 = new Player(swap ? playerName : otherPlayerName, 'O'); return new Game { Player1 = player1, Player2 = player2, PlayerTurn = player1, }; }
Hmmm, how can we unit test this method? There’s no way to control what Random returns, but we need to in order to test what happens when the swap value is true and when it is false.
We have identified Random as an external dependency; now we need to find a way to replace it in order to unit test this method.
The solution I prefer results in a bit more code, but this is a pattern that is easily remembered, implemented, and tested.
First, let’s write the tests:
[TestMethod] public void GameManager_CreateNewRandom_with_random_0_should_not_swap_players() { // Arrange var gameManager = new GameManager(); var firstPlayer = Guid.NewGuid().ToString(); var secondPlayer = Guid.NewGuid().ToString(); // TODO force Random to return 0 // Act var game = gameManager.CreateNewRandom(firstPlayer, secondPlayer); // Assert Assert.IsNotNull(game); Assert.IsNotNull(game.Player1, "game.Player1"); Assert.AreEqual('X', game.Player1.Mark, "game.Player1.Mark"); Assert.AreEqual(firstPlayer, game.Player1.Name, "game.Player1.Name"); Assert.IsNotNull(game.Player2, "game.Player2"); Assert.AreEqual('O', game.Player2.Mark, "game.Player2.Mark"); Assert.AreEqual(secondPlayer, game.Player2.Name, "game.Player2.Name"); } [TestMethod] public void GameManager_CreateNewRandom_with_random_1_should_swap_players() { // Arrange var gameManager = new GameManager(); var firstPlayer = Guid.NewGuid().ToString(); var secondPlayer = Guid.NewGuid().ToString(); // TODO force Random to return 1 // Act var game = gameManager.CreateNewRandom(firstPlayer, secondPlayer); // Assert Assert.IsNotNull(game); Assert.IsNotNull(game.Player1, "game.Player1"); Assert.AreEqual('X', game.Player1.Mark, "game.Player1.Mark"); Assert.AreEqual(secondPlayer, game.Player1.Name, "game.Player1.Name"); Assert.IsNotNull(game.Player2, "game.Player2"); Assert.AreEqual('O', game.Player2.Mark, "game.Player2.Mark"); Assert.AreEqual(firstPlayer, game.Player2.Name, "game.Player2.Name"); }
If we run these, they should fail approximately 1/2 of the time, because we can’t control the Random value… yet.
Let’s extract the Random dependency into another class:
public abstract class RandomProviderBase { public abstract int Get(int? maxValue = null); }
And then create the default (production) implementation:
public class RandomProvider : RandomProviderBase { private readonly Random rnd; public RandomProvider(int? seed = null) { rnd = seed != null ? new Random(seed.Value) : new Random(); } public override int Get(int? maxValue = null) { return maxValue != null ? rnd.Next(maxValue.Value) : rnd.Next(); } }
And then modify GameManager to reference the dependency instead of calling Random directly:
public class GameManager { private RandomProviderBase myRandomProvider; protected RandomProviderBase RandomProvider => myRandomProvider ?? (myRandomProvider = this.MakeRandomProvider()); protected virtual RandomProviderBase MakeRandomProvider() { return new RandomProvider(); } ... public Game CreateNewRandom(string playerName, string otherPlayerName) { var swap = this.RandomProvider.Get(2) == 1; var player1 = new Player(swap ? otherPlayerName : playerName, 'X'); var player2 = new Player(swap ? playerName : otherPlayerName, 'O'); return new Game { Player1 = player1, Player2 = player2, PlayerTurn = player1, }; } }
And then extend from GameManager and create a testable version of it, where we can manipulate the dependencies as the tests require:
public class GameManagerTestable : GameManager { // NOTE this could also just be a plain RandomProviderBase property instead of a lambda, but I like lambdas because there's more flexibility public Func<RandomProviderBase> MakeRandomProviderOverride { get; set; } protected override RandomProviderBase MakeRandomProvider() { return (this.MakeRandomProviderOverride ?? base.MakeRandomProvider)(); } }
Now we have a way to mock a dependency such that we can control its behavior. The dependency doppelgangers that are created and injected in place of the real thing are commonly (albeit inaccurately) called Mocks, or more formally, Test Doubles.
There are great frameworks that easily create these test doubles with whatever functionality you want, but it’s not difficult to create them manually. I would caution you to take the time to more thoroughly understand the process before leaning heavily on any framework. But when you are ready, I can suggest Moq or RhinoMocks for C#, and I have had great success with each.
Different types of Test Doubles
- Dummy – an empty/null/default implementation
- Stub – an implementation which returns hard-coded values or simple calculations, used mostly for simulating indirect inputs
- Mock – a configurable implementation for simulating indirect outputs, used for verifying interactions
- Fake – a more complete implementation which maintains state and is called multiple times, used to simulate things like database access
- Spy – similar to a Mock, but is capable of recording calls made to it, used for verifying interactions
For a more detailed explanation of the different types of Test Doubles, see Dummy vs. Stub vs. Spy vs. Fake vs. Mock
Let’s take a look at each kind of Test Double:
Stubs
Stubs are simple and mostly just return hard-coded values.
public class RandomProviderStubReturns0 : RandomProviderBase { public override int Get(int? maxValue = null) { return 0; } } public class RandomProviderStubReturns1 : RandomProviderBase { public override int Get(int? maxValue = null) { return 1; } }
Now we have a way to control the Random value:
[TestMethod] public void GameManager_CreateNewRandom_with_random_0_should_not_swap_players() { // Arrange var gameManager = new GameManagerTestable(); var firstPlayer = Guid.NewGuid().ToString(); var secondPlayer = Guid.NewGuid().ToString(); gameManager.MakeRandomProviderOverride = () => new RandomProviderStubReturns0(); // Act var game = gameManager.CreateNewRandom(firstPlayer, secondPlayer); // Assert Assert.IsNotNull(game); Assert.IsNotNull(game.Player1, "game.Player1"); Assert.AreEqual('X', game.Player1.Mark, "game.Player1.Mark"); Assert.AreEqual(firstPlayer, game.Player1.Name, "game.Player1.Name"); Assert.IsNotNull(game.Player2, "game.Player2"); Assert.AreEqual('O', game.Player2.Mark, "game.Player2.Mark"); Assert.AreEqual(secondPlayer, game.Player2.Name, "game.Player2.Name"); } [TestMethod] public void GameManager_CreateNewRandom_with_random_1_should_swap_players() { // Arrange var gameManager = new GameManagerTestable(); var firstPlayer = Guid.NewGuid().ToString(); var secondPlayer = Guid.NewGuid().ToString(); gameManager.MakeRandomProviderOverride = () => new RandomProviderStubReturns1(); // Act var game = gameManager.CreateNewRandom(firstPlayer, secondPlayer); // Assert Assert.IsNotNull(game); Assert.IsNotNull(game.Player1, "game.Player1"); Assert.AreEqual('X', game.Player1.Mark, "game.Player1.Mark"); Assert.AreEqual(secondPlayer, game.Player1.Name, "game.Player1.Name"); Assert.IsNotNull(game.Player2, "game.Player2"); Assert.AreEqual('O', game.Player2.Mark, "game.Player2.Mark"); Assert.AreEqual(firstPlayer, game.Player2.Name, "game.Player2.Name"); }
It may seem odd at first because we are testing against GameManagerTestable rather than the GameManager directly – but because the Testable version inherits from the real thing, and has very little code of its own, we can have confidence that we are testing the actual code we want to test, and not the Testable code. These Testable versions should live only in Test projects and not in the actual codebase.
Mocks
If you want to have more control over the test double’s behavior from the test method itself, you can use a Mock instead of a Stub.
public class RandomProviderMock : RandomProviderBase { public Func<int?, int> GetOverride { get; set; } public override int Get(int? maxValue = null) { return this.GetOverride(maxValue); } } [TestMethod] public void GameManager_CreateNewRandom_with_random_0_should_not_swap_players() { // Arrange var gameManager = new GameManagerTestable(); var firstPlayer = Guid.NewGuid().ToString(); var secondPlayer = Guid.NewGuid().ToString(); gameManager.MakeRandomProviderOverride = () => new RandomProviderMock { GetOverride = maxValue => 0 }; ... [TestMethod] public void GameManager_CreateNewRandom_with_random_1_should_swap_players() { // Arrange var gameManager = new GameManagerTestable(); var firstPlayer = Guid.NewGuid().ToString(); var secondPlayer = Guid.NewGuid().ToString(); gameManager.MakeRandomProviderOverride = () => new RandomProviderMock { GetOverride = maxValue => 1 }; ...
Dummies
A Dummy is the test double with the least amount of code. It’s always going to return a default value, a null, or an empty implementation of a class. You would use a Dummy when you don’t care about what it does, you just need to override the real implementation and ensure you don’t throw a NullReferenceException.
public class RandomProviderDummy : RandomProviderBase { public override int Get(int? maxValue = null) { return default(int); } }
(Yes, technically this does the exact same thing as RandomProviderStubReturns0, but that’s not the point)
Fakes
Fakes are test implementations that come close to or exactly match the actual behavior of the real implementation.
public class RandomProviderFake : RandomProviderBase { private int lastValue = 0; public override int Get(int? maxValue = null) { if (lastValue == 1) lastValue = 0; return lastValue++; } }
The actual code here doesn’t matter – all it does is alternate between 0 and 1. But the purpose of the code is such that it is a reasonable approximation of the actual RandomProvider – it returns a 0 exactly 50% of the time and a 1 the rest of the time.
You might write a Fake to replace a database-access class, which saves/reads all data in memory instead of actually talking to a database. Because of the amount of near-real-world logic being implemented, you should be cautious when using Fakes, as these often require almost as much code as the real implementation! The more code, the more fragile the test.
Spies
You use a Spy when you want to verify orchestration: that the code under test is calling external dependencies and is passing the correct parameters and receiving the correct output.
public class RandomProviderSpy : RandomProviderBase { public RandomProviderSpy(RandomProviderBase underlyingImpl) { this.Implementation = underlyingImpl; this.GetSpy = new FuncSpy<int?, int>(this.Implementation.Get); } public RandomProviderBase Implementation { get;} public FuncSpy<int?, int> GetSpy { get; } public override int Get(int? maxValue = null) { this.GetSpy.Method(maxValue); } }
My RandomProviderSpy is a wrapper around another RandomProviderBase implementation, and it observes and captures the call details.
public class FuncSpy<TParam1, TResult> { public Func<int, TParam1, TResult> OnCall { get; } = (callNum, param1) => default(TResult); public IList<Tuple<TParam1, TResult>> Calls { get; } = new List<Tuple<TParam1, TResult>>(); public FuncSpy(Func<TParam1, TResult> onCallFunc = null) { if (onCallFunc != null) this.OnCall = (callNum, param1) => onCallFunc(param1); } public FuncSpy(Func<int, TParam1, TResult> onCallFunc = null) { if (onCallFunc != null) this.OnCall = onCallFunc; } public TResult Method(TParam1 param1) { var result = this.OnCall(this.Calls.Count, param1); this.Calls.Add(new Tuple<TParam1, TResult>(param1, result)); return result; } }
I wrote this FuncSpy helper class to encapsulate the logic for observing and capturing call details. The FuncSpy takes in a lambda for the actual call, and captures the inputs and outputs when making the call.
The only caveat of my FuncSpy implementation is that if the method being spied on is passing back reference types (classes), and those objects are being modified, then it’s impossible to capture the exact state of an object at the time of the call – because we’re only capturing references, not values. Some ways around this limitation would be to A) clone the objects (if they are cloneable) on the way in and out of the FunSpy.Method() or B) perform the assertions at the time of the call instead of after the fact. Each of those approaches has their pluses and minuses.
[TestMethod] public void GameManager_CreateNewRandom_calls_RandomProvider() { // Arrange var gameManager = new GameManagerTestable(); var firstPlayer = Guid.NewGuid().ToString(); var secondPlayer = Guid.NewGuid().ToString(); var expectedReturnValueForRandomProviderGet = 1; var spy = new RandomProviderSpy( new RandomProviderMock{ GetOverride = maxValue => expectedReturnValueForRandomProviderGet, } ); gameManager.MakeRandomProviderOverride = () => spy; // Act var game = gameManager.CreateNewRandom(firstPlayer, secondPlayer); // Assert Assert.AreEqual(1, spy.GetSpy.Calls.Count, "Expected 1 call to RandomProvider.Get()"); Assert.AreEqual(2, spy.GetSpy.Calls[0].Item1, "Expected maxValue parameter of RandomProvider.Get() call #1 to be \"2\""); Assert.AreEqual( expectedReturnValueForRandomProviderGet, spy.GetSpy.Calls[0].Item2, $"Expected return value of RandomProvider.Get() call #1 to be \"{expectedReturnValueForRandomProviderGet}\""); }
In this test we are also making use of RandomProviderMock so that we can specify the return value and remove hard-coded literals from the assert (line 24). The Spy uses the mock as the underlying implementation.
The assertions when using a spy are different because the purpose of the test is different. Testing orchestration is a separate concern than testing “what-happens-if-this-dependency-returns-such-and-such”. You will need both kinds of tests.
Recap
We learned about identifying and exposing external dependencies from our code under test.
We learned about what the different types of Test Doubles are, when to use them, and how to write them.
Stay tuned for the next installment where we’ll discuss another way to create Test Doubles.
If you’ve found this tutorial useful, please share it! Got suggestions or feedback? Please leave a comment – thanks!
Pingback: Tic-Tac-Tutorial Series | PhilChuang.com