Sponsor: Do you build complex software systems? See how NServiceBus makes it easier to design, build, and manage software systems that use message queues to achieve loose coupling. Get started for free.
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.
Check out my YouTube channel, where I post all kinds of content accompanying my posts, including this video showing everything in this post.
Let’s start with a method in an OrderService for creating an Order.
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.
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.
You could jump directly to an interface, which I’ve seen quite a bit of for this exact usage case with DateTime.
With the appropriate registration with the ServiceCollection, we can now inject a ISystemDateTime instead of calling DateTime.UtcNow.
We can be deterministic within our test by returning a specific DateTime for UtcNow.
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.
Again, by registering this delegate and the static method implementation, we can inject that delegate rather than an interface.
Our test becomes less cumbersome as we can easily create a stub for returning a deterministic DateTime without additional libraries or dependencies.
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.
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
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.
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.
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.
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.