DEV Community 👩‍💻👨‍💻

Cover image for Simplify your unit tests with auto-mocking and TypeBuilder
Cesar Aguirre
Cesar Aguirre

Posted on • Originally published at canro91.github.io

Simplify your unit tests with auto-mocking and TypeBuilder

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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>()));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Men at work

Let's use builders to write simpler tests. Photo by Ricardo Gomez Angel on Unsplash

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
            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);

                arg = _mocks[paramType].Object;
            }

            args.Add(arg);
        }

        return (T)ctor.Invoke(args.ToArray());
    }

    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) where U : class
    {
        if (!_mocks.TryGetValue(typeof(U), out var result))
        {
            result = args != null
                ? new Mock<U>(args)
                : new Mock<U>();

            _mocks[typeof(U)] = result;
        }

        return (Mock<U>)result;
    }
}
Enter fullscreen mode Exit fullscreen mode

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>()));
          }
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
});
Enter fullscreen mode Exit fullscreen mode

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>()));
Enter fullscreen mode Exit fullscreen mode

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. To read how to use AutoFixture for auto-mocking, check my post Write simpler tests with Type Builders and AutoFixture.

To read more content on unit testing, check my Unit Testing best practices checklist and Let's refactor a real test. And grab a free copy of that checklist on my eBook Unit Testing 101 - eBook.

Happy testing!

Top comments (0)

🌖🌗🌘 Turn on dark mode in Settings