Test Doubles
Have more control over dependencies in your tests
Introduction
When I first started learning to write automated tests, I was introduced to what is referred to as test doubles and I was completely confused about what they were for and how to use them. Not only that, but most people just kept saying they were “mocking” dependencies and that made everything even more confusing. I have spent quite a bit of time trying to improve my understanding of test doubles and what I’ve found is that the articles and books that are popular regarding test doubles were difficult for me to learn when to use test doubles in the real world. My goal is to more simply define what test doubles are and where different test doubles are used to give you better situational awareness as to when test doubles make sense to implement. The books and articles that have greatly influenced this article include xUnit Test Patterns, Mocks Aren’t Stubs and The Little Mocker.
What’s what?
Words matter when talking about test doubles. Gerard Meszaros originally coined the term Test Double years ago and described it as similar to a stunt double in a movie. The idea is that a real dependency is too expensive and may be difficult to use in a test and therefore, it makes sense to use a stand-in dependency or test double which is cheaper and easier to control.
What I’ve found with using test doubles in practice is that nobody ever seems to refer to test doubles by that term. With heavy use of mocking frameworks in modern automated tests, the term “mocking” is typically what I hear when a developer talks about using a test double to replace a dependency. When referring to dependencies where a developer wants to use a stub or spy, they might still refer to those things simply as a mock. These terms are more easily related to discussing using a mocking framework such as Moq.
When it comes down to it, using a mocking framework and referring to it as mocking is not wrong. In reality, you are creating a mock object every time you utilize the mocking framework to replace a real dependency. We’ll get into what a mock object is later on but for now, just keep this in mind.
I prefer to use the term “Test Double” as an umbrella term for different types of test doubles and then refer to each type of test double by their name (Dummy, Stub, Spy, Mock, or Fake) because it allows me to be more clear about what my intentions are. I find the term “mocking” to have multiple meanings and it is not always clear what we are doing with a dependency without being more explicit.
Dummy
A dummy test double is probably the most insignificant test double you’re going to use. In fact, you don’t actually use it necessarily. That’s the whole point of the test double. The dummy test double never actually gets used and is only ever implemented to take up space as a required parameter or value. If for some reason the dummy test double is used, the expectation would be that an exception is thrown.
Most of the time a dummy test double will simply be a null value being used. I find myself adding null values for required parameters in tests to a point that I don’t even think about the fact that I’m using a dummy test double at all. The only time where I have to pause and think about it more is when null checking is in place for the parameter. In those cases, it makes sense to create a dummy object.
Depending on the language I’m using will usually dictate how complex the dummy object needs to be. In cases where I’m using a statically typed language then I will want to create a dummy object that shares an interface with the actual object. In that case, my dummy object would have all methods throw an exception if used. In cases where I’m using a dynamically typed language then I would simply use something like String as a dummy object.
Stub
Sometimes when we need to test a feature of our application we also rely on other components to provide specific information that drives the outcome of our test. This specific information needs to be predictable so that our feature is tested under the same conditions every time. In this case, we need to be able to control what information the other component provides. There are multiple ways to control what information is provided by a depended-on component (DOC) but the easiest way is to use a stub test double.
A stub is designed to replace a DOC with canned responses for method calls. When we test our feature then the system under test (SUT) uses the stub in place of the DOC to provide specific information that we control. This ensures that our test operates under the same conditions every time.
This is an example of a test using a stub:
using Api;
using NUnit.Framework;
namespace ApiTests
{
public class ShippingControllerTests
{
[Test]
public void GetAddress_ReturnsAmazonHQAddress()
{
var shippingController = new ShippingController(new ShippingServiceStub());
var result = shippingController.GetAddress();
Assert.AreEqual("410 Terry Ave. North", result.StreetAddress);
Assert.AreEqual("Seattle", result.City);
}
}
public class ShippingServiceStub : IShippingService
{
public Address GetDefaultAddress()
{
return new("410 Terry Ave. North", "Seattle");
}
}
}
The full example of this code can be found on GitHub.
Spy
Let’s say that we have a feature that we are testing and the SUT has a dependency that we are sending information and expecting a result. However, we are expecting our SUT to make a decision and send very specific information to the dependency. Ideally, we would want to have some type of observation point where we could verify that the information sent to the dependency was correct. In the case of our SUT, verifying that the dependency was called with specific information will indicate to us that the correct decision was made in the SUT. Introducing a spy test double will provide us with the ability to verify what calls are occurring on the dependency.
A spy is designed to do everything a stub can do while also being able to record what parameters are being passed to a method as well as how many times a method is called. I’ve used a spy in cases where I was saving a form in a frontend test and I wanted to verify that a call to an API occurred only once and with specific information without actually using the real dependency.
Because a spy is just an advanced stub, you have the option to use canned responses for the methods that you’re spying on. After you execute your SUT, you can then verify how many times your spy was called and what parameters were passed to it. This verification of the spy would always occur in the test and happens after execution.
This is an example of a test using a spy:
using Api;
using NUnit.Framework;
namespace ApiTests
{
public class AuthenticationControllerTests
{
[Test]
public void IsAuthenticated_ReturnsTrue_WhenIsAdmin()
{
var authenticationServiceSpy = new AuthenticationServiceSpy();
var authenticationController = new AuthenticationController(new IdentityProviderServiceStub(),
authenticationServiceSpy);
var result = authenticationController.IsAuthenticated();
Assert.AreEqual(1, authenticationServiceSpy.GetNumberOfCalls());
Assert.AreEqual("Stub Username", authenticationServiceSpy.GetUsername());
Assert.AreEqual(true, result);
}
}
public class IdentityProviderServiceStub : IIdentityProviderService
{
public Identity GetIdentity()
{
return new() { Username = "Stub Username", IdentityType = IdentityType.Admin };
}
}
public class AuthenticationServiceSpy : IAuthenticationService
{
private int _numberOfCalls;
private string _username;
public bool ValidateAuthorization(string username)
{
_numberOfCalls++;
_username = username;
var authenticationService = new AuthenticationService();
return authenticationService.ValidateAuthorization(username);
}
public int GetNumberOfCalls()
{
return _numberOfCalls;
}
public string GetUsername()
{
return _username;
}
}
}
The full example of this code can be found on GitHub.
Mock
I find the use of a mock test double to just be a great way to handle duplication between several tests that would normally just use spies. When testing a feature, you could end up creating several tests that verify that one method is called one time with different parameters per test. However, instead of setting up a spy for each test and verifying that the spy was called correctly after execution on each test, you could employ the use of a mock test double.
A mock is designed to be an object that replaces the DOC to handle calls to DOC methods while also verifying that the method was called with the correct parameters and a correct number of times. When setting up a test, you will need to install the mock object in place of the actual dependency. You would then want to set the expected parameters as well as the expected number of calls on the mock object for a specific method. You could then have the mock automatically verify that expected conditions match the actual conditions when the method actually executes or you can explicitly run the verify after execution.
Just like the stub and spy, you can also identify specific return values when setting up the mock object in a specific test. In my experience, I don’t typically use a mock object unless I’m using a mocking framework. I don’t use one primarily because I like to keep my verification inside of the test so that at a glance, I can tell exactly what the test is checking for. If I was to use a mock object then I would need to use a little more brainpower to decipher what the test is verifying. I also don’t typically write enough tests that are similar enough where the cost of handling the duplication with a mock object outweighs the cost of just keeping the duplication.
These are examples of tests using a mock:
Hand-Written
using Api;
using NUnit.Framework;
namespace ApiTests
{
public class AuthenticationControllerTests
{
[Test]
public void IsAuthenticated_ReturnsTrue_WhenIsAdmin()
{
var authenticationServiceMock = new AuthenticationServiceMock();
authenticationServiceMock.Setup(1, "Admin");
var authenticationController = new AuthenticationController(new IdentityProviderServiceAdminStub(),
authenticationServiceMock);
var result = authenticationController.IsAuthenticated();
authenticationServiceMock.Verify();
Assert.AreEqual(true, result);
}
[Test]
public void IsAuthenticated_ReturnsFalse_WhenIsViewer()
{
var authenticationServiceMock = new AuthenticationServiceMock();
authenticationServiceMock.Setup(0);
var authenticationController = new AuthenticationController(new IdentityProviderServiceViewerStub(),
authenticationServiceMock);
var result = authenticationController.IsAuthenticated();
authenticationServiceMock.Verify();
Assert.AreEqual(false, result);
}
[Test]
public void IsAuthenticated_ReturnsTrue_WhenIsEditor()
{
var authenticationServiceMock = new AuthenticationServiceMock();
authenticationServiceMock.Setup(1, "Editor");
var authenticationController = new AuthenticationController(new IdentityProviderServiceEditorStub(),
authenticationServiceMock);
var result = authenticationController.IsAuthenticated();
authenticationServiceMock.Verify();
Assert.AreEqual(true, result);
}
}
public class IdentityProviderServiceAdminStub : IIdentityProviderService
{
public Identity GetIdentity()
{
return new() { Username = "Admin", IdentityType = IdentityType.Admin };
}
}
public class IdentityProviderServiceViewerStub : IIdentityProviderService
{
public Identity GetIdentity()
{
return new() { Username = "Viewer", IdentityType = IdentityType.Viewer };
}
}
public class IdentityProviderServiceEditorStub : IIdentityProviderService
{
public Identity GetIdentity()
{
return new() { Username = "Editor", IdentityType = IdentityType.Editor };
}
}
public class AuthenticationServiceMock : IAuthenticationService
{
private int ExpectedNumberOfCalls { get; set; }
private string ExpectedUsername { get; set; }
private int _actualNumberOfCalls;
private string _actualUsername;
public void Setup(int numberOfCalls, string username)
{
Setup(numberOfCalls);
ExpectedUsername = username;
}
public void Setup(int numberOfCalls)
{
ExpectedNumberOfCalls = numberOfCalls;
}
public bool ValidateAuthorization(string username)
{
_actualNumberOfCalls++;
_actualUsername = username;
var authenticationService = new AuthenticationService();
return authenticationService.ValidateAuthorization(username);
}
public void Verify()
{
Assert.AreEqual(ExpectedNumberOfCalls, _actualNumberOfCalls);
Assert.AreEqual(ExpectedUsername, _actualUsername);
}
}
}
The full example of this code can be found on GitHub.
Mocking Framework
using Api;
using Moq;
using NUnit.Framework;
namespace ApiTests
{
public class AuthenticationControllerTests
{
private Mock<IAuthenticationService> _authenticationServiceMock;
private Mock<IIdentityProviderService> _identityProviderServiceMock;
[SetUp]
public void SetUp()
{
_authenticationServiceMock = new Mock<IAuthenticationService>();
_authenticationServiceMock.Setup(x =>
x.ValidateAuthorization(It.IsAny<string>())).Returns(true);
_identityProviderServiceMock = new Mock<IIdentityProviderService>();
}
[Test]
public void IsAuthenticated_ReturnsTrue_WhenIsAdmin()
{
var adminIdentity = new Identity {Username = "Admin", IdentityType = IdentityType.Admin};
_identityProviderServiceMock.Setup(x => x.GetIdentity()).Returns(adminIdentity);
var authenticationController = new AuthenticationController(_identityProviderServiceMock.Object,
_authenticationServiceMock.Object);
var result = authenticationController.IsAuthenticated();
_authenticationServiceMock.Verify(x =>
x.ValidateAuthorization("Admin"), Times.Once);
Assert.AreEqual(true, result);
}
[Test]
public void IsAuthenticated_ReturnsFalse_WhenIsViewer()
{
var viewerIdentity = new Identity { Username = "Viewer", IdentityType = IdentityType.Viewer };
_identityProviderServiceMock.Setup(x => x.GetIdentity()).Returns(viewerIdentity);
var authenticationController = new AuthenticationController(_identityProviderServiceMock.Object,
_authenticationServiceMock.Object);
var result = authenticationController.IsAuthenticated();
_authenticationServiceMock.Verify(x =>
x.ValidateAuthorization(It.IsAny<string>()), Times.Never);
Assert.AreEqual(false, result);
}
[Test]
public void IsAuthenticated_ReturnsTrue_WhenIsEditor()
{
var editorIdentity = new Identity { Username = "Editor", IdentityType = IdentityType.Editor };
_identityProviderServiceMock.Setup(x => x.GetIdentity()).Returns(editorIdentity);
var authenticationController = new AuthenticationController(_identityProviderServiceMock.Object,
_authenticationServiceMock.Object);
var result = authenticationController.IsAuthenticated();
_authenticationServiceMock.Verify(x =>
x.ValidateAuthorization("Editor"), Times.Once);
Assert.AreEqual(true, result);
}
}
}
The full example of this code can be found on GitHub.
Fake
In some tests, you will want to persist data in a database or through a web service. However, using these real dependencies could be slow or maybe not even available. In these cases, it might make sense to utilize a fake object in place of a dependency during a test.
A fake object is meant to replace a DOC with the same functionality as the DOC but in a way that allows the test to use it more efficiently and quickly than using the real dependency. This is different than most of the other test doubles (stub, spy, and mock) in that the fake object is not expected to return any canned answers or verify any calls that were made. For the most part, the fake object has real functionality, but will likely take shortcuts and implement the functionality in a much simpler way.
When I’m creating a fake object, I’m essentially taking a boundary DOC and recreating it in memory. This allows my tests to operate quickly as well as letting a garbage collector clean up the dependency when I’m done with it.
You would not use a mocking framework to create a fake. Instead, creating a fake object is something you’re likely going to either hand-write or use an existing package to generate. In the example below, I’m using the Microsoft.EntityFrameworkCore.InMemory package to create an in-memory database.
This is an example of a test using a fake:
using System;
using System.Linq;
using Api;
using Microsoft.EntityFrameworkCore;
using NUnit.Framework;
namespace ApiTests
{
public class SettingServiceTests
{
[Test]
public void Insert()
{
var options = new DbContextOptionsBuilder<SettingsStoreContext>()
.UseInMemoryDatabase(Guid.NewGuid().ToString())
.Options;
var settingsDbContext = new SettingsStoreContext(options);
var settingsService = new SettingsService(settingsDbContext);
var settingsModel = new settings
{id = Guid.NewGuid(), category = "Theme", name = "DarkMode", value = "true"};
settingsService.Insert(settingsModel);
var settingsDbRecords = settingsDbContext.Settings.ToList();
Assert.AreEqual(1, settingsDbRecords.Count);
var settingsDbRecord = settingsDbRecords.Single();
Assert.That(settingsDbRecord.Equals(settingsModel));
}
}
}
The full example of this code can be found on GitHub.
Conclusion
At the end of the day, you’re probably only going to use a stub or spy in most cases where you’re trying to control a dependency and verify that a feature works correctly. You’ll likely use a dummy without even knowing it and a fake will be set up only in very specific cases and where things are slow without it. I hope that I’ve laid out some simpler ways of knowing when to use test doubles and have at least provided a more confident understanding of how to incorporate test doubles into your testing strategy.