In the previous email, we talked about mocking types that you own. To re-iterate, this guideline is about writing your own adapters on top of third-party libraries and mocking those adapters instead of the underlying types. This is beneficial because:
-
You often don’t have a deep understanding of how the third-party code works.
-
Even if that code already provides built-in interfaces, it’s risky to mock those interfaces, because you have to be sure the behavior you mock matches what the external library actually does.
-
Adapters abstract non-essential technical details of the third-party code and define the relationship with the library in your application’s terms.
In that email, I used an example from one of my past projects:
public class LocationsGateway : ILocationsGateway { private readonly LocationsClient _client; public async Task<Result<Coordinates>> GetCoordinatesByAddress( string street, string city, string zip, string state) { OdsServiceResponse<GeocodedAddressListV1> latLongInfo = await _client .GetLocationsByAddressAsync( street + " " + city + " " + zip + " " + state); if (latLongInfo == null || latLongInfo.Result) return Result.Fail<Coordinates>( "No coordinates available for the address entered."); Coordinates coordinates = Coordinates.Create( latLongInfo.Result.GeocodedAddresses[0].Latitude, latLongInfo.Result.GeocodedAddresses[0].Longitude).Value; return Result.Ok(coordinates); } } public interface ILocationsGateway { Task<Result<Coordinates>> GetCoordinatesByAddress( string street, string city, string zip, string state); }
In this example:
-
LocationsClient
is a class from a third-party NuGet library that reaches out to an unmanaged dependency (a geolocation service) -
LocationsGateway
is a wrapper on top ofLocationsClient
-
ILocationsGateway
is an interface forLocationsGateway
; its purpose is to enable mocking
Notice how this wrapper (LocationsGateway
) addresses the issues I described above:
-
You don’t know 100% how the underlying library works and whether all your assumptions about its behavior are correct. Having a wrapper mitigates this uncertainty — you can make any assumption about the wrapper because you wrote it yourself.
If those assumptions turn out to not match the underlying library’s behavior, you’ll just need to adjust the wrapper, there’s no need to modify the code that works with that wrapper. All changes to the geolocation-related functionality become restricted to one place — the wrapper itself.
-
The wrapper uses the project’s domain language: it returns an instance of
Coordinates
— a Value Object that belongs to the domain model.
The LocationsGateway
adapter, in effect, acts as an anti-corruption layer between our code and the external world. It also helps us to expose only the features we need and hide all the unnecessary complexity.
There are a couple of best practices related to how you should write wrappers for external services. First of all, in a lot of cases, you’ll need two such wrappers per service.
In the above example, there is only one wrapper (LocationsGateway
) because that wrapper is rather simple. In most cases, though, you’ll need to do multiple things with the external service.
For example, in the book, I showed two classes — Bus
and MessageBus
— that allowed us to put messages on the message bus:
public class MessageBus { private readonly IBus _bus; public void SendEmailChangedMessage(int userId, string newEmail) { _bus.Send("Type: USER EMAIL CHANGED; " + $"Id: {userId}; " + $"NewEmail: {newEmail}"); } } public interface IBus { void Send(string message); }
The difference between these two classes is that:
-
IBus
is a wrapper on top of the message bus SDK library. This wrapper encapsulates non-essential technical details, such as connection credentials, and exposes a nice, clean interface for sending arbitrary text messages to the bus. -
MessageBus
is a wrapper on top ofIBus
; it defines messages specific to your domain.MessageBus
helps you keep all such messages in one place and reuse them across the application.
It’s possible to merge IBus
and MessageBus
together, but that would be a suboptimal solution. These two responsibilities — hiding the external library’s complexity and holding all application messages in one place — are best kept separated.
The only exception is when you need the external service to do one thing only. LocationsGateway
is a good example here — it uses the geolocation service’s GetLocationsByAddress()
to retrieve the coordinates, and nothing more. MessageBus
, on the other hand, sends multiple messages using the same IBus
's API.
The second guideline related to wrappers is that it doesn’t apply to managed dependencies. Remember, mocks are for unmanaged dependencies only. Thus, there’s no need to abstract in-memory or managed dependencies (such as the application database).
For instance, if a library provides a date and time API, you can use that API as is, because it doesn’t reach out to unmanaged dependencies. Similarly, there’s no need to abstract an ORM as long as it’s used for accessing a database that isn’t visible to external applications. Of course, you can introduce your own wrapper on top of any library, but it’s rarely worth the effort for anything other than unmanaged dependencies.
--Vlad
Enjoy this message? Here are more things you might like:
Workshops — I offer a 2-day workshop for organizations on Domain-Driven Design and Unit Testing. Reply to this email to discuss.
Unit Testing Principles, Patterns and Practices — A book for people who already have some experience with unit testing and want to bring their skills to the next level.
Learn more »
My Pluralsight courses — The topics include Unit Testing, Domain-Driven Design, and more.
Learn more »