Write simpler tests with Type Builders and AutoFixture
21 Jun 2021 #tutorial #csharp #showdevWriting tests for services with lots of collaborators can be tedious. I know! We will end up with complex Arrange parts and lots of fakes. Let’s see three alternatives to write simpler tests with builder methods, Type Builders and AutoFixture.
To write simpler tests for services with lots of collaborators, use builder methods to create only the fakes needed in every test. As an alternative, use auto-mocking to create a service with its collaborators replaced by test doubles.
To show these three alternatives, let’s bring back our OrderService
class. We used it to show the difference between stubs and mocks. Again, the OrderService
checks if an item has stock available to then charge a credit card.
This time, let’s add an IDeliveryService
to create a shipment order and an IOrderRepository
to keep track of order status. With these two changes, our OrderService
will look like this:
public class OrderService
{
private readonly IPaymentGateway _paymentGateway;
private readonly IStockService _stockService;
private readonly IDeliveryService _deliveryService;
private readonly IOrderRepository _orderRepository;
public OrderService(IPaymentGateway paymentGateway,
IStockService stockService,
IDeliveryService deliveryService,
IOrderRepository orderRepository)
{
_paymentGateway = paymentGateway;
_stockService = stockService;
_deliveryService = deliveryService;
_orderRepository = orderRepository;
}
public OrderResult PlaceOrder(Order order)
{
if (!_stockService.IsStockAvailable(order))
{
throw new OutOfStockException();
}
// Process payment, ship items, and store order status...
return new PlaceOrderResult(order);
}
}
I know, I know! We could argue our OrderService
is doing a lot of things! Bear with me.
Let’s write a test to check if the payment gateway is called when we place an order. We’re using Moq to write fakes. This test will look like this:
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace WithoutAnyBuilders;
[TestClass]
public class OrderServiceTestsBefore
{
[TestMethod]
public void PlaceOrder_ItemInStock_CallsPaymentGateway()
{
var stockService = new Mock<IStockService>();
stockService.Setup(t => t.IsStockAvailable(It.IsAny<Order>()))
.Returns(true);
var paymentGateway = new Mock<IPaymentGateway>();
var deliveryService = new Mock<IDeliveryService>();
var orderRepository = new Mock<IOrderRepository>();
var service = new OrderService(paymentGateway.Object,
stockService.Object,
deliveryService.Object,
orderRepository.Object);
var order = new Order();
service.PlaceOrder(order);
paymentGateway.Verify(t => t.ProcessPayment(It.IsAny<Order>()));
}
}
Sometimes, we need to create fakes for our collaborators even when the behavior under test doesn’t need them.
1. Builder methods
One easy alternative to writing simpler tests is to use builder methods.
With a builder method, we only create the fakes we need inside our tests. And, inside the builder method, we create “empty” fakes for the collaborators we don’t need for the tested scenario.
We used this idea of builder methods to write better tests by making our tests less noisy and more readable.
Our test with a builder method looks like this:
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace WithABuilderMethod;
[TestClass]
public class OrderServiceTestsBuilder
{
[TestMethod]
public void PlaceOrder_ItemInStock_CallsPaymentGateway()
{
var stockService = new Mock<IStockService>();
stockService.Setup(t => t.IsStockAvailable(It.IsAny<Order>()))
.Returns(true);
var paymentGateway = new Mock<IPaymentGateway>();
var orderService = MakeOrderService(stockService.Object, paymentGateway.Object);
// ^^^^^
// We add a new MakeOrderService method
var order = new Order();
orderService.PlaceOrder(order);
paymentGateway.Verify(t => t.ProcessPayment(order));
}
private OrderService MakeOrderService(IStockService stockService, IPaymentGateway paymentGateway)
// ^^^^^
// Notice we only pass the fakes we need
{
var deliveryService = new Mock<IDeliveryService>();
var orderRepository = new Mock<IOrderRepository>();
var service = new OrderService(paymentGateway,
stockService,
deliveryService.Object,
orderRepository.Object);
return service;
}
}
With the MakeOrderService()
method, we only deal with the mocks we care about in our test: the ones for IStockService
and IPaymentService
.
2. Auto-mocking with TypeBuilder
Builder methods are fine. But, we can use a special builder to create testable services with all its collaborators replaced by fakes or test doubles. This way, we don’t need to create builder methods for every combination of services we need inside our tests.
Let me introduce you to TypeBuilder
. This is a helper class I’ve been using in one of my client’s projects to create services inside our unit tests.
This TypeBuilder
class uses reflection to find all the parameters in the constructor of the service to build. And, it uses Moq to build fakes for each parameter.
TypeBuilder
expects a single constructor. But, we can easily extend it to pick the one with more parameters.
public class TypeBuilder<T>
{
private readonly Dictionary<Type, object> _instances = new Dictionary<Type, object>();
private readonly Dictionary<Type, Mock> _mocks = new Dictionary<Type, Mock>();
public T Build()
{
Type type = typeof(T);
ConstructorInfo ctor = type.GetConstructors().First();
ParameterInfo[] parameters = ctor.GetParameters();
var args = new List<object>();
foreach (var param in parameters)
{
Type paramType = param.ParameterType;
object arg = null;
if (_mocks.ContainsKey(paramType))
{
arg = _mocks[paramType].Object;
}
else if (_instances.ContainsKey(paramType))
{
arg = _instances[paramType];
}
if (arg == null)
{
if (!_mocks.ContainsKey(paramType))
{
Type mockType = typeof(Mock<>).MakeGenericType(paramType);
ConstructorInfo mockCtor = mockType.GetConstructors().First();
var mock = mockCtor.Invoke(null) as Mock;
_mocks.Add(paramType, mock);
}
arg = _mocks[paramType].Object;
}
args.Add(arg);
}
return (T)ctor.Invoke(args.ToArray());
}
public TypeBuilder<T> WithInstance<U>(U instance, bool force = false) where U : class
{
if (instance != null || force)
{
_instances[typeof(U)] = instance;
}
return this;
}
public TypeBuilder<T> WithMock<U>(Action<Mock<U>> mockExpression) where U : class
{
if (mockExpression != null)
{
var mock = Mock<U>();
mockExpression(mock);
_mocks[typeof(U)] = mock;
}
return this;
}
public Mock<U> Mock<U>(object[] args = null, bool createInstance = true) where U : class
{
if (!_mocks.TryGetValue(typeof(U), out var result) && createInstance)
{
result = args != null
? new Mock<U>(args)
: new Mock<U>();
_mocks[typeof(U)] = result;
}
return (Mock<U>)result;
}
}
Let’s rewrite our sample test to use the TypeBuilder
class.
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace WithTypeBuilder;
[TestClass]
public class OrderServiceTestsTypeBuilder
{
[TestMethod]
public void PlaceOrder_ItemInStock_CallsPaymentGateway()
{
// 1. Create a builder
var typeBuilder = new TypeBuilder<OrderService>();
// ^^^^^
// 2. Configure a IStockService fake with Moq
typeBuilder.WithMock<IStockService>(mock =>
// ^^^^^
{
mock.Setup(t => t.IsStockAvailable(It.IsAny<Order>()))
.Returns(true);
});
// 3. Build an OrderService instance
var service = typeBuilder.Build();
// ^^^^^
var order = new Order();
service.PlaceOrder(order);
// Retrieve a fake from the builder
typeBuilder.Mock<IPaymentGateway>()
// ^^^^
.Verify(t => t.ProcessPayment(It.IsAny<Order>()));
}
}
This is what happened. First, we create a builder with var typeBuilder = new TypeBuilder<OrderService>();
.
Then, to register a custom fake, we used the method WithMock<T>()
. And inside it, we configured the behavior of the fake.
In our case, we created a fake StockService
that returns true
for any order. We did that in these lines:
typeBuilder.WithMock<IStockService>(mock =>
{
mock.Setup(t => t.IsStockAvailable(It.IsAny<Order>()))
.Returns(true);
});
After that, with the method Build()
we got an instance of the OrderService
class with fakes for all its parameters. But, the fake for IStockService
has the behavior we added in the previous step.
Finally, in the Assert part, we retrieved a fake from the builder with Mock<T>()
. We used it to verify if the payment gateway was called or not. We did this here:
typeBuilder.Mock<IPaymentGateway>()
.Verify(t => t.ProcessPayment(It.IsAny<Order>()));
This TypeBuilder
class comes in handy to avoid creating builders manually for every service in our unit tests.
Did you notice in our example that we didn’t have to write fakes for all collaborators? We only did it for the IStockService
. The TypeBuilder
took care of the other fakes.
3. Auto-mocking with AutoFixture
If you prefer a more battle-tested solution, let’s replace our TypeBuilder
with AutoFixture.
What AutoFixture does
From its docs, AutoFixture “is a tool designed to make Test-Driven Development more productive and unit tests more refactoring-safe”.
AutoFixture creates test data for us. It helps us to simplify the Arrange parts of our tests.
To start using AutoFixture, let’s install its NuGet package AutoFixture
.
For example, we can create orders inside our tests with:
Fixture fixture = new Fixture();
fixture.Create<Order>();
AutoFixture will initialize all properties of an object to random values. Optionally, we can hardcode our own values if we want to.
AutoMoq
AutoFixture has integrations with mocking libraries like Moq to create services with all its parameters replaced by fakes. To use these integrations, let’s install the NuGet package AutoFixture.AutoMoq
.
Let’s rewrite our sample test, this time to use AutoFixture with AutoMoq. It will look like this:
using AutoFixture;
using AutoFixture.AutoMoq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace WithAutoFixture;
[TestClass]
public class OrderServiceTestsAutoFixture
{
// 1. Create a field for AutoFixture
private readonly IFixture Fixture = new Fixture()
.Customize(new AutoMoqCustomization());
// ^^^^^
[TestMethod]
public void PlaceOrder_ItemInStock_CallsPaymentGateway()
{
var stockedService = Fixture.Freeze<Mock<IStockService>>();
// ^^^^^
// 2. Use Freeze to create a custom fake
stockedService.Setup(t => t.IsStockAvailable(It.IsAny<Order>()))
.Returns(true);
var paymentGateway = Fixture.Freeze<Mock<IPaymentGateway>>();
var service = Fixture.Create<OrderService>();
// ^^^^^
// 3. Use Create to grab an auto-mocked instance
var order = new Order();
service.PlaceOrder(order);
paymentGateway.Verify(t => t.ProcessPayment(order));
}
}
Notice this time, we used a field in our test to hold a reference to AutoFixture Fixture
class. Also, we needed to add the AutoMoqCustomization
behavior to make AutoFixture a type builder.
To retrieve a fake reference, we used the Freeze<T>()
method. We used these references to plug the custom behavior for the IStockService
fake and to verify the IPaymentGateway
fake.
Voilà! That’s how we can use a TypeBuilder
helper class and AutoFixture to simplify the Arrange parts of our tests. If you prefer a simple solution, use the TypeBuilder
class. But, if you don’t mind adding an external reference to your tests, use AutoFixture. Maybe, you can use it to create test data too.
If you want to know what fakes and mocks are, check What are fakes in unit testing: mocks vs stubs and learn these 5 tips to write better stubs and mocks. And, don’t miss the rest of my Unit Testing 101 series where I cover more subjects like this one.
Happy testing!