I originally posted an extended version of this post on my blog a couple of weeks ago. It's part of a series I've been publishing, called Unit Testing 101
Writing tests for services with lots of collaborators can be tedious. I know, I know! You will end up with complex Arrange parts and lots of fakes. Let's see how to write simpler tests using auto-mocking with TypeBuilder.
To write simpler tests for services with lots of collaborators, use builder methods to create only the fakes needed in every test. As alternative, use auto-mocking to create a service with its collaborators replaced by fakes or test doubles.
To show auto-mocking, 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 a IDeliveryService
to create a shipment order and a IOrderRepository
to keep track of an 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);
}
}
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>()));
}
}
}
Write simpler tests with Builder methods
One easy alternative to write simpler test is to use builder methods.
With a builder method, we only create the fakes we need inside our tests. And, inside the builder, we create "empty" fakes for the collaborators we don't need for the tested scenario. Something like this:
[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>();
// We add a new MakeOrderService() method
var orderService = MakeOrderService(stockService.Object, paymentGateway.Object);
var order = new Order();
orderService.PlaceOrder(order);
paymentGateway.Verify(t => t.ProcessPayment(order));
}
// Notice we only pass the fakes we need
private OrderService MakeOrderService(IStockService stockService, IPaymentGateway paymentGateway)
{
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()
builder method, we only deal with the fake we care about in our test, IStockService
. This new method takes care of creating the rest of fakes to comply with the OrderService
constructor.
Auto-mocking with TypeBuilder
Builder methods are fine. But, if we have lots of testing scenarios, we need to create builders for every combination of dependencies needed to mock in our tests.
As alternative to plain builder methods, we can use an special builder to automatically create fakes for every dependency of the tested service. That's why we call this technique auto-mocking. Well, to be precise, I would be auto-faking. You got the idea!
Let me introduce you, TypeBuilder
. This is a helper class I've been using in one of my client's projects to create services inside our unit tests.
public class TypeBuilder<T>
{
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
{
Type mockType = typeof(Mock<>).MakeGenericType(paramType);
ConstructorInfo mockCtor = mockType.GetConstructors().First();
var mock = mockCtor.Invoke(null) as Mock;
_mocks.Add(paramType, mock);
}
args.Add(arg);
}
return (T)ctor.Invoke(args.ToArray());
}
public TypeBuilder<T> WithMock<U>(Action<Mock<U>> mockExpression) where U : class
{
var mock = Mock<U>();
mockExpression(mock);
_mocks[typeof(U)] = mock;
return this;
}
public Mock<U> Mock<U>() where U : class
{
if (!_mocks.TryGetValue(typeof(U), out var result))
{
result = new Mock<U>();
_mocks[typeof(U)] = result;
}
return (Mock<U>)result;
}
}
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.
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 a OrderService instance
var service = typeBuilder.Build();
var order = new Order();
service.PlaceOrder(order);
// 4. Retrieve a fake from the builder
typeBuilder.Mock<IPaymentGateway>()
.Verify(t => t.ProcessPayment(It.IsAny<Order>()));
}
}
}
This is what happened. First, we created a builder with var typeBuilder = new TypeBuilder<OrderService>();
.
Then, we customized the IStockService
fake to always return true
with the WithMock<T>()
method. We did it 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 use 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>()));
Did you notice we didn't have to write fakes for every collaborator? We only did it for the IStockService
. The TypeBuilder
took care of creating fakes for all others.
Voilà ! That's how we can use auto-mocking with a TypeBuilder
helper class to simplify the Arrange parts of our tests. If you prefer a more robust alternative, use AutoFixture. It's a free and open source library to create test data with support for auto-mocking with Moq.
Upgrade your unit testing skills with my course: Mastering C# Unit Testing with Real-world Examples on Udemy. Practice with hands-on exercises and learn best practices by refactoring real-world unit tests.
Happy testing!
Top comments (0)