Published on Jul 21, 2022
If you are already familiar with AutoFixture, then most likely you know what I will talk about, otherwise feel free to read ahead.
The best unit test is one without any dependency, but we'll save this topic for later. Very often in real-world projects, you'll find classes that have many dependencies, which makes testing them a bit more challenging.
Now imagine, we're writing a CloudService, which reads a file, encrypts it, stores it in AWS S3, and then sends a notification message to broadcast this "file uploaded" event. To illustrate this in programming world, here are 4 dependencies:
public interface IFileService
{
Task<Stream> ReadFile(string path);
}
public interface IEncryptionService
{
Task<Stream> Encrypt(Stream stream);
}
public interface IStorageService
{
Task<string> SaveToS3(Stream stream);
}
public interface INotificationService
{
Task Notify(string message);
}
And here's the CloudService that relies on the implementations of these interfaces:
public class CloudService
{
private readonly IFileService _fIleService;
private readonly IEncryptionService _encryptionService;
private readonly IStorageService _storageService;
private readonly INotificationService _notificationService;
public CloudService(
IFileService fileService,
IEncryptionService encryptionService,
IStorageService storageService,
INotificationService notificationService)
{
_fileService = fIleService;
_encryptionService = encryptionService;
_storageService = storageService;
_notificationService = notificationService;
}
public async Task<string> DoWork()
{
// The code below does not compile; it's just pseudo code
await _fileService.ReadFile(...);
await _encryptionService.Encrypt(...);
await _storageService.SaveToS3(...);
await _notificationService.Notify(...);
return "OK"
}
}
To test CloudService, we usually choose Moq to mock its dependencies. For example, in NUnit, our Setup method might look like this:
public class CloudServiceTests
{
private Mock<IFileService> _fileService;
private Mock<IEncryptionService> _encryptionService;
private Mock<IStorageService> _storageService;
private Mock<INotificationService> _notificationService;
private CloudService _sut;
[SetUp]
public void Setup()
{
_fileService = new Mock<IFileService>();
_encryptionService = new Mock<IEncryptionService>();
_storageService = new Mock<IStorageService>();
_notificationService = new Mock<INotificationService>();
_sut = new CloudService(
_fileService.Object,
_encryptionService.Object,
_storageService.Object,
_notificationService.Object
);
}
}
CloudService
only has 4 dependencies and it's already several lines of code to
set up. I see classes that have more than 10 dependencies quite often
in real-world projects (and don't get me wrong, because by no means I consider
that is good practice) and it's very tedious and boring to write this kind of
setup code time after time.
To begin with, take a look at AutoFixture. AutoFixture has an extension package AutoFixture.AutoMoq which works very well with Moq. AutoFixture, in summary, provides a way for us to set up our SUT ((system under test, i.e. your testing target)) more easily.
To use AutoFixture, you start by creating a Fixture
and call its Create
method to create your
SUT:
var fixture = new Fixture();
var sut = fixture.Create<CloudService>();
var actual = await sut.DoWork();
Assert.AreEqual("OK", actual);
If you run the code above, AutoFixture will complain that it couldn't create the SUT: AutoFixture was unable to create an instance from IFileService because it's an interface... It's not surprising because indeed we didn't instruct AutoFixture how to deal with those dependencies. To fix the issue, we need to add another package AutoMoq.
Then, we Customize
the fixture
like this:
var fixture = new Fixture().Customize(new AutoMoqCustomization());
// more code omitted...
What this Customize
achieved is to inject mocked dependencies to the constructor
of CloudService
while creating its instance, and those mocked dependencies (Mock<T>
)
have some sensible mocked returned values for their methods. This is why when you run
the test again, it will pass, as these await
calls below all return Task.Completed
.
await _fileService.ReadFile(...);
await _encryptionService.Encrypt(...);
await _storageService.SaveToS3(...);
await _notificationService.Notify(...);
This is not all yet, because we often need to access those mock objects to
setup their method calls. Let's say we want to setup ReadFile for IFileService,
and you can achieve it by using fixture.Inject
:
var fileService = new Mock<IFileService>();
fixture.Inject(fileService.Object);
fileService.Setup(x => x.ReadFile(...)) ...
var sut = fixture.Create<CloudService>();
In fact, the pattern above is so ubiquitously used, that the contributors of the library actually made a convenient method for it, which works like this:
var fileService = fixture.Freeze<Mock<IFileService>>();
fileService.Setup(x => x.ReadFile(...)) ...
var sut = fixture.Create<CloudService>();
This means that when we ask AutoFixture to create an instance of CloudService
,
it will use the Mock<IFileService>
instance as the constructor parameter.
Compared with the code where we manually set up every dependency, this is cleaner and simpler. What's better is that if you add or remove a dependency in CloudService's constructor, your tests won't be significantly affected; most tests won't need any change or fix at all.
Unit testing definitely has its advantages, but one thing I've learned so far is that if people cannot enjoy writing unit tests, it's easy for them to lose interest in maintaining them or adding more of them in future. Therefore, keeping your tests clean and simple is essential to producing unit tests that people actually care about.
© 2022 disasterdev.net. All rights reserved