Skip to content

Testing WITHOUT Mocks or Interfaces!

A common approach people take with testing is mocking. Specifically defining interfaces for dependencies which are then typically mocked so you can test in isolation. While interfaces can be helpful for mocking as well as fakes and stubs, there can be other approaches taken. Meaning you don’t need to create an interface for everything.

YouTube

Check out my YouTube channel, where I post all kinds of content accompanying my posts, including this video showing everything in this post.

Deterministic

Let’s start with a method in an OrderService for creating an Order.

public async Task<Order> CreateOrderAsync(int basketId, Address shippingAddress)
{
var basket = await _basketRepository.GetByIdAsync(basketId);
Guard.Against.NullBasket(basketId, basket);
Guard.Against.EmptyBasketOnCheckout(basket.Items);
var catalogItemsSpecification = new CatalogItemsSpecification(basket.Items.Select(item => item.CatalogItemId).ToArray());
var catalogItems = await _itemRepository.ListAsync(catalogItemsSpecification);
var items = basket.Items.Select(basketItem =>
{
var catalogItem = catalogItems.First(c => c.Id == basketItem.CatalogItemId);
var itemOrdered = new CatalogItemOrdered(catalogItem.Id, catalogItem.Name, _uriComposer.ComposePicUri(catalogItem.PictureUri));
var orderItem = new OrderItem(itemOrdered, basketItem.UnitPrice, basketItem.Quantity);
return orderItem;
}).ToList();
var now = DateTime.UtcNow;
var order = new Order(basket.BuyerId, shippingAddress, items, now);
await _orderRepository.AddAsync(order);
return order;
}
view raw CreateOrder.cs hosted with ❤ by GitHub

To test this method, there are a few dependencies involved. The OrderRepository, ItemRepository, URIComposer. Here’s what the test might look like by using a mocking library.

[Fact]
public async Task TestIsFlaky()
{
var obj = new OrderService(_mockBasketRepo.Object, _mockCatalogRepo.Object, _mockOrderRepo.Object, _uriComposer.Object);
var address = new Address("120 Freidrich Lane", "Austin", "TX", "US", "78744");
var order = await obj.CreateOrderAsync(_basketId, address);
Assert.Equal(address, order.ShipToAddress);
// This is Flaky
Assert.Equal(DateTime.UtcNow, order.OrderDate);
}
view raw Test1.cs hosted with ❤ by GitHub

The problem is, as I’ve described above, is this test is flaky. That’s because the OrderDate is being set by DateTime.UtcNow. That’s non-deterministic.

Sure, we could be more lenient on our assert by maybe using a small range/window, but ultimately we want the result to be deterministic.

Interfaces

You could jump directly to an interface, which I’ve seen quite a bit of for this exact usage case with DateTime.

public interface ISystemDateTime
{
DateTimeOffset UtcNow();
}
public class SystemDateTime : ISystemDateTime
{
public DateTimeOffset UtcNow()
{
return System.DateTime.UtcNow;
}
}
view raw IDateTime.cs hosted with ❤ by GitHub

With the appropriate registration with the ServiceCollection, we can now inject a ISystemDateTime instead of calling DateTime.UtcNow.

public class OrderService : IOrderService
{
private readonly IRepository<Order> _orderRepository;
private readonly IUriComposer _uriComposer;
private readonly IRepository<Basket> _basketRepository;
private readonly IRepository<CatalogItem> _itemRepository;
private readonly ISystemDateTime _systemDateTime;
public OrderServiceWithInterface(IRepository<Basket> basketRepository,
IRepository<CatalogItem> itemRepository,
IRepository<Order> orderRepository,
IUriComposer uriComposer,
ISystemDateTime systemDateTime)
{
_orderRepository = orderRepository;
_uriComposer = uriComposer;
_systemDateTime = systemDateTime;
_basketRepository = basketRepository;
_itemRepository = itemRepository;
}
public async Task<Order> CreateOrderAsync(int basketId, Address shippingAddress)
{
var basket = await _basketRepository.GetByIdAsync(basketId);
Guard.Against.NullBasket(basketId, basket);
Guard.Against.EmptyBasketOnCheckout(basket.Items);
var catalogItemsSpecification = new CatalogItemsSpecification(basket.Items.Select(item => item.CatalogItemId).ToArray());
var catalogItems = await _itemRepository.ListAsync(catalogItemsSpecification);
var items = basket.Items.Select(basketItem =>
{
var catalogItem = catalogItems.First(c => c.Id == basketItem.CatalogItemId);
var itemOrdered = new CatalogItemOrdered(catalogItem.Id, catalogItem.Name, _uriComposer.ComposePicUri(catalogItem.PictureUri));
var orderItem = new OrderItem(itemOrdered, basketItem.UnitPrice, basketItem.Quantity);
return orderItem;
}).ToList();
var now = _systemDateTime.UtcNow();
var order = new Order(basket.BuyerId, shippingAddress, items, now);
await _orderRepository.AddAsync(order);
return order;
}
}
view raw Inject.cs hosted with ❤ by GitHub

We can be deterministic within our test by returning a specific DateTime for UtcNow.

[Fact]
public async Task TestWithInterface()
{
var utcNow = new DateTimeOffset(new DateTime(2022, 11, 28));
_mockSystemDateTime.Setup(x => x.UtcNow()).Returns(utcNow);
var obj = new OrderServiceWithInterface(_mockBasketRepo.Object, _mockCatalogRepo.Object, _mockOrderRepo.Object, _mockUriComposer.Object, _mockSystemDateTime.Object);
var address = new Address("120 Freidrich Lane", "Austin", "TX", "US", "78744");
var order = await obj.CreateOrderAsync(_basketId, address);
Assert.Equal(utcNow, order.OrderDate);
}
view raw Test.cs hosted with ❤ by GitHub

Function

If you have a class/interface with one method, you have a function. The ISystemDateTime is exactly that. We have more options than interfaces when it comes to abstractions. In this case, using a delegate is also an option.

public delegate DateTimeOffset UtcNow();
public static class DateTimeFunctions
{
public static DateTimeOffset UtcNow()
{
return DateTime.UtcNow;
}
}
view raw gistfile1.txt hosted with ❤ by GitHub

Again, by registering this delegate and the static method implementation, we can inject that delegate rather than an interface.

public class OrderService : IOrderService
{
private readonly IRepository<Order> _orderRepository;
private readonly IUriComposer _uriComposer;
private readonly UtcNow _utcNow;
private readonly IRepository<Basket> _basketRepository;
private readonly IRepository<CatalogItem> _itemRepository;
public OrderServiceDelegate(IRepository<Basket> basketRepository,
IRepository<CatalogItem> itemRepository,
IRepository<Order> orderRepository,
IUriComposer uriComposer,
UtcNow utcNow)
{
_orderRepository = orderRepository;
_uriComposer = uriComposer;
_utcNow = utcNow;
_basketRepository = basketRepository;
_itemRepository = itemRepository;
}
public async Task<Order> CreateOrderAsync(int basketId, Address shippingAddress)
{
var basket = await _basketRepository.GetByIdAsync(basketId);
Guard.Against.NullBasket(basketId, basket);
Guard.Against.EmptyBasketOnCheckout(basket.Items);
var catalogItemsSpecification = new CatalogItemsSpecification(basket.Items.Select(item => item.CatalogItemId).ToArray());
var catalogItems = await _itemRepository.ListAsync(catalogItemsSpecification);
var items = basket.Items.Select(basketItem =>
{
var catalogItem = catalogItems.First(c => c.Id == basketItem.CatalogItemId);
var itemOrdered = new CatalogItemOrdered(catalogItem.Id, catalogItem.Name, _uriComposer.ComposePicUri(catalogItem.PictureUri));
var orderItem = new OrderItem(itemOrdered, basketItem.UnitPrice, basketItem.Quantity);
return orderItem;
}).ToList();
var now = _utcNow();
var order = new Order(basket.BuyerId, shippingAddress, items, now);
await _orderRepository.AddAsync(order);
return order;
}
}
view raw Delegate.cs hosted with ❤ by GitHub

Our test becomes less cumbersome as we can easily create a stub for returning a deterministic DateTime without additional libraries or dependencies.

[Fact]
public async Task TestWithDelegate()
{
var utcNow = new DateTimeOffset(new DateTime(2022, 11, 28));
var obj = new OrderServiceDelegate(_mockBasketRepo.Object, _mockCatalogRepo.Object, _mockOrderRepo.Object,
_mockUriComposer.Object, () => utcNow);
var address = new Address("120 Freidrich Lane", "Austin", "TX", "US", "78744");
var order = await obj.CreateOrderAsync(_basketId, address);
Assert.Equal(utcNow, order.OrderDate);
}
view raw Test.cs hosted with ❤ by GitHub

Values

As we break this down, you might also wonder why we even inject some abstraction when creating the order but instead pass the value of the DateTime to the order creation—basically moving up the call stack.

public async Task<Order> CreateOrderAsync(int basketId, Address shippingAddress, DateTimeOffset orderDate)
{
var basket = await _basketRepository.GetByIdAsync(basketId);
Guard.Against.NullBasket(basketId, basket);
Guard.Against.EmptyBasketOnCheckout(basket.Items);
var catalogItemsSpecification = new CatalogItemsSpecification(basket.Items.Select(item => item.CatalogItemId).ToArray());
var catalogItems = await _itemRepository.ListAsync(catalogItemsSpecification);
var items = basket.Items.Select(basketItem =>
{
var catalogItem = catalogItems.First(c => c.Id == basketItem.CatalogItemId);
var itemOrdered = new CatalogItemOrdered(catalogItem.Id, catalogItem.Name, _uriComposer.ComposePicUri(catalogItem.PictureUri));
var orderItem = new OrderItem(itemOrdered, basketItem.UnitPrice, basketItem.Quantity);
return orderItem;
}).ToList();
var order = new Order(basket.BuyerId, shippingAddress, items, orderDate);
await _orderRepository.AddAsync(order);
return order;
}
view raw Value.cs hosted with ❤ by GitHub

Now the caller is responsible for passing a DateTime to the CreateOrder, simplifying the test even more as we no longer have a dependency to pass to the OrderService

[Fact]
public async Task TestWithValue()
{
var utcNow = new DateTimeOffset(new DateTime(2022, 11, 28));
var obj = new OrderService(_mockBasketRepo.Object, _mockCatalogRepo.Object, _mockOrderRepo.Object, _mockUriComposer.Object);
var address = new Address("120 Freidrich Lane", "Austin", "TX", "US", "78744");
var order = await obj.CreateOrderAsync(_basketId, address, utcNow);
Assert.Equal(utcNow, order.OrderDate);
}
view raw Test.cs hosted with ❤ by GitHub

Abstract Classes

Have you ever needed to test an implementation that needed to use HttpClient? If so, you’re faced with the same issue where you want a deterministic result.

First, here’s an example of an ExchangeRateClient for getting currency exchange rates.

public class ExchangeRateClient : IExchangeRateClient
{
private readonly HttpClient _httpClient;
private readonly IMemoryCache _memoryCache;
public ExchangeRateClient(HttpClient httpClient, IMemoryCache memoryCache)
{
_httpClient = httpClient;
_memoryCache = memoryCache;
}
private string Key(DateOnly date)
{
return $"Rate:{date.ToString("yyyy-MM-dd")}";
}
public async Task<decimal> Rate(DateOnly date, Currency baseCurrency, Currency toCurrency)
{
return await _memoryCache.GetOrCreateAsync(Key(date), async _ =>
{
var result = await _httpClient.GetFromJsonAsync<JsonResponse>(
$"https://data.fixer.io/api/{date.ToString("yyyy-MM-dd")}?base={baseCurrency}&symbols={toCurrency}");
if (result == null)
{
throw new InvalidOperationException("Not a valid JSON response.");
}
return result.Rates.Single(x => x.Key == toCurrency.ToString()).Value;
});
}
private class JsonResponse
{
public Dictionary<string, decimal> Rates { get; set; }
}
}

HttpClient doesn’t have an interface. So how do you test? Well, it does support providing your implementation of an HttpMessageHandler, where you must implement the SendAsync method.

public class ExchangeRateClientTests
{
private readonly MemoryCache _cache;
public ExchangeRateClientTests()
{
_cache = new MemoryCache(new MemoryCacheOptions());
}
[Fact]
public async Task Rate()
{
var exchangeRate = 1.5m;
var httpClient = new HttpClient(new TestHttpHandler(Currency.CAD, exchangeRate));
var obj = new ExchangeRateClient(httpClient, _cache);
var result = await obj.Rate(new DateOnly(2022, 12, 01), Currency.USD, Currency.CAD);
Assert.Equal(exchangeRate, result);
}
}
public class TestHttpHandler : HttpMessageHandler
{
private readonly Currency _toCurrency;
private readonly decimal _exchangeRate;
public TestHttpHandler(Currency toCurrency, decimal exchangeRate)
{
_toCurrency = toCurrency;
_exchangeRate = exchangeRate;
}
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var response = new HttpResponseMessage(HttpStatusCode.OK);
var json = JsonConvert.SerializeObject(new { Rates = new Dictionary<string, decimal> { { _toCurrency.ToString(), _exchangeRate } } });
response.Content = new StringContent(json);
return Task.FromResult(response);
}
}
view raw Test.cs hosted with ❤ by GitHub

Abstractions

You don’t always need to default to interfaces. You have other abstractions like delegates and abstract classes, and changing your design to move non-deterministic calls up the call stack so you can pass values instead.

Join!

Developer-level members of my YouTube channel or Patreon get access to a private Discord server to chat with other developers about Software Architecture and Design and access to source code for any working demo application I post on my blog or YouTube. Check out my Patreon or YouTube Membership for more info.

Learn more about Software Architecture & Design.
Join thousands of developers getting weekly updates to increase your understanding of software architecture and design concepts.


Leave a Reply

Your email address will not be published. Required fields are marked *