There are countless articles on the web about unit tests: TDD approach, beginner's guides, mocking frameworks, test coverage tools and more. However, the vast majority of these articles are either too "Hello World"-like or focus more on tools, while missing the key point how to write unit tests that are useful and deliver the most value. In this article I'll share some best practices, which I believe will get your unit tests to the next level.
This is an example-driven article inasmuch as, in my view, this is the best way to explain things in programming. We're going to start with a code example (a C# class plus a few unit tests for this class) and highlight some important points related to what we want to test and how we're going to do that. Then we'll review one of the many ways (my favorite one) to structure unit tests. Finally, I'll share a few awesome libraries that I use in all my test projects.
Ready? Lets' get started🍿
Example 1
UserManager
public class UserManager : IUserManager
{
private readonly IUserRepository _userRepository;
private readonly IUserGuard _userGuard;
public UserManager(IUserRepository userRepository, IUserGuard userGuard)
{
_userRepository = userRepository;
_userGuard = userGuard;
}
public async Task ChangeEmail(int userId, string newEmail, CancellationToken ct = default)
{
var user = await _repository.GetUser(userId, ct);
_guard.UserExists(user);
_guard.UserIsAllowedToChangeEmail(user!);
_guard.EmailIsValid(newEmail);
_guard.EmailIsNew(user!, newEmail);
user!.Email = newEmail;
await _repository.Save(user, ct);
}
}
Test strategy
- We want to verify that all calls to
IUserGuard
happen in the correct order, and, most importantly, come before_repository.Save
. In other words, validation has to go before the data is saved to the database. - Make sure we pass correct parameters to all methods that we call here.
- Our
ChangeEmail
method actually calls_repository.Save
. - We save the
user
object with the new email address.
Tests
public class UserManagerTests
{
public class ChangeEmail : UserManagerTestsBase
{
[Theory]
[AutoData]
public async Task Should_change_email(int userId,
string newEmail,
User user,
CancellationToken ct)
{
// arrange
Repository.GetUser(userId, ct).Returns(user);
// act
await Manager.ChangeEmail(userId, newEmail, ct);
// assert
Received.InOrder(() =>
{
Guard.Received(1).UserExists(user);
Guard.Received(1).UserIsAllowedToChangeEmail(user);
Guard.Received(1).EmailIsValid(newEmail);
Guard.Received(1).EmailIsNew(user, newEmail);
Repository.Received(1).Save(Arg.Is<User>(x => x == user &&
x.Email == newEmail),
ct);
});
}
}
public abstract class UserManagerTestsBase
{
protected readonly UserManager Manager;
protected readonly IUserRepository Repository;
protected readonly IUserGuard Guard;
protected UserManagerTestsBase()
{
Repository = Substitute.For<IUserRepository>();
Guard = Substitute.For<IUserGuard>();
Manager = new UserManager(Repository, Guard);
}
}
}
Notes
- Instead of hardcoding test data, we generated random values using
[AutoData]
(AutoFixture.Xunit2 library). - To validate the order of method calls we used
Received.InOrder(...)
(NSubstitute the best mocking library for .NET). - We used
Arg.Is<User>(x => x.Email == newEmail)
in order to make sure we change email address of theuser
before saving this object to the database.
Now we're going to add one more method to the UserManager
class and unit test it.
Example 2
public class UserManager : IUserManager
{
private readonly IUserRepository _repository;
private readonly IUserGuard _guard;
public UserManager(IUserRepository repository, IUserGuard guard)
{
_repository = repository;
_guard = guard;
}
// public async Task ChangeEmail(int userId, string newEmail, CancellationToken ct = default)
// {...}
public async Task ChangePassword(int userId, string newPassword, CancellationToken ct = default)
{
var user = await _repository.GetUser(userId, ct);
if (user == null)
{
throw new ApplicationException($"User {userId} not found");
}
user.Password = newPassword;
await _repository.Save(user, ct);
}
}
Test strategy
- This method has an
if
statement, so we want to make sure that correct branch of code runs depending on the condition in theif
statement. - Instead of ignoring the exception, we want to validate the type of the thrown exception along with its message.
- Same as in the previous example, we want to verify that we pass the
user
object with an updated password to the_repository.Save
method. - If an exception is thrown, then the
_repository.Save
method must not get called.
Tests
public class UserManagerTests
{
// public class ChangeEmail : UserManagerTestsBase
// {...}
public class ChangePassword : UserManagerTestsBase
{
[Theory, AutoData]
public async Task Should_throw_ApplicationException_when_user_not_found(int userId,
string newPassword,
CancellationToken ct)
{
// act
var action = () => Manager.ChangePassword(userId, newPassword, ct);
// assert
await action.Should()
.ThrowAsync<ApplicationException>()
.WithMessage($"User {userId} not found");
await Repository.DidNotReceiveWithAnyArgs().Save(Arg.Any<User>(), Arg.Any<CancellationToken>());
}
[Theory, AutoData]
public async Task Should_change_password_and_save(int userId,
string newPassword,
User user,
CancellationToken ct)
{
// arrange
Repository.GetUser(userId, ct).Returns(user);
// act
await Manager.ChangePassword(userId, newPassword, ct);
// assert
await Repository.Received(1).Save(Arg.Is<User>(x => x == user &&
x.Password == newPassword),
ct);
}
}
public abstract class UserManagerTestsBase
{
protected readonly UserManager Manager;
protected readonly IUserRepository Repository;
protected readonly IUserGuard Guard;
protected UserManagerTestsBase()
{
Repository = Substitute.For<IUserRepository>();
Guard = Substitute.For<IUserGuard>();
Manager = new UserManager(Repository, Guard);
}
}
}
Notes
- For the
ChangePassword
method we have two tests: first test to validate that an exception gets thrown when a user not found, and the second one to check that we make a call to_repository.Save
. - Note how we test exceptions: instead of try-catch we create a delegate to the test method and then do
action.Should().ThrowAsync<ApplicationException>()
.
Our tests are done, now let's take a look at the unit tests coverage in order to make sure we didn't miss anything and there is no not tested code left.
Tests Structure
To a large extent, the layout of a test project is a matter of taste. Here is the way I prefer to do this. For classes with more than one method (managers, services etc) the structure of the related class with unit tests would be as follows:
public class MyClassTests
{
public class Method1 : MyClassTestsBase
{
[Theory]
[AutoData]
public async Task Should_return_A_when_X(parameters)
{
// arrange
// ...
// act
// ...
// assert
// ...
}
[Theory]
[AutoData]
public async Task Should_throw_B_when_Y(parameters)
{
// arrange
// ...
// act
// ...
// assert
// ...
}
}
public class Method2 : MyClassTestsBase
{
[Fact]
public async Task Should_return_A_when_X()
{
// arrange
// act
// assert
}
[Fact]
public async Task Should_throw_B_when_Y()
{
// arrange
// act
// assert
}
}
public abstract class MyClassTestsBase
{
protected readonly MyClass Instance;
protected readonly IDependency1 Dependency1;
protected readonly IDependency2 Dependency2;
protected UserManagerTestsBase()
{
Dependency1 = Substitute.For<IDependency1>();
Dependency2 = Substitute.For<IDependency2>();
Instance = new MyClass(IDependency1, IDependency2);
}
}
}
This is what it looks like in the test runner:
Unit tests structure for classes with a single method (message handlers, factories etc):
public class MyHandlerTests
{
private readonly MyHandler _handler;
public MyHandlerTests()
{
_handler = new MyHandler();
}
[Fact]
public void Should_do_A_when_X()
{
// arrange
// act
// assert
}
}
Alright, so we have an understanding of the key concepts of unit testing and learned the best way to structure our tests. Now let's check out a few brilliant libraries which, I'm absolutely sure, you'll love.
Libraries and frameworks
xUnit
Its usually a choice between xUnit, nUnit and MSTest, and my personal preference goes to the former. A while ago when Microsoft started using xUnit in its projects, this framework became a default option.
Autofixture Cheat Sheet
Instead of using hard coded values for tests, Autofixture can generate random data for us.
var fixture = new Fixture();
var firstName = fixture.Create<string>();
var numOfUsers = fixture.Create<int>();
var employees = fixture.CreateMany<Employee>();
Autofixture.Xunit2
nuget for [AutoData]
attribute (the one we used in our examples).
NSubstitute
There are a few mocking libraries out there, but I find this one the most explicit, natural and clean. A small bonus: NSubstitute.Analyzers Roslyn analyzers to detect (during compilation) possible errors using NSubstitute.
FluentAssertions
Fantastic assertion framework with a very detailed and well-structured documentation.
A couple of examples taken from their docs:
string actual = "ABCDEFGHI";
actual.Should().StartWith("AB").And.EndWith("HI").And.Contain("EF").And.HaveLength(9);
IEnumerable<int> numbers = new[] { 1, 2, 3 };
numbers.Should().OnlyContain(n => n > 0);
numbers.Should().HaveCount(4, "because we thought we put four items in the collection");
Isn't it beautiful?
Stryker.NET
Stryker offers mutation testing for your .NET Core and .NET Framework projects. It allows you to test your tests by temporarily inserting bugs in your source code.
It's a very interesting tool which modifies the source code and expects tests to fail. If they don't, it complains about incomplete unit tests or poor tests coverage.
Summary
We've analyzed two simple, yet important code examples that cover the most popular scenarios of unit testing. Learned one of the ways to structure and group tests, and made a list of essential libraries that will make your tests clean, explicit and more effective.
Cheers!
Top comments (4)
Nice read, personally I'm more in favor of Mock because it allows me to use extension methods on the Mock to create a MOM structure that doesn't leak into the solution.
For example when you create an extension method for a service resharper will offer it as a suggestion to reference the test project and use the extension.
Also I prefer naming my test methods like so (just as an alternative/discussion point):
public void {Method to test}_{Given}_{When?}_{Then}() {}
So for example:
public void ChangePassword_PasswordHasDifferentValue_ChangesPassword() {}
public void ChangePassword_PasswordIsEqualToPrevious_NoChange() {}
Finally, one thing I'm missing here is using SUT.
I'm currently on a project where we're expecting to use that term to specify which services we're testing and in my experience it really helps to spot what's being tested quickly.
Is there a reason you're not using that?
Hi @marvinbrouwer ,
Thank you for your feedback.
I'm not sure that I got your point regarding Moq and extension methods.
As regards naming and tests structure, I lean towards the structure described in the article because it lets group test not only by class name, but also by method:
Then again, there is no right and wrong approach here and the way you do this makes perfect sense.
As for SUT, I usually follow this rule of thumb: all methods (including mappings and validation rules e.g. when using FluentValidator) except simple one-liners are subjects for unit testing. In order to make sure nothing is missing I use tests coverage tools (Rider / Reshrper can do that out of the box).
Okay okay, the naming strategy isn't the most important thing tbh.
As long as the whole team agrees.
So about the reason why I like Mock is the separation from the tests.
When I have a dependency say a price calculator I usually definite it like a
Mock<IPriceService>
at test's scope.Then in the constructor of the test I do some default setups and per test I'd configure the mock even more.
So when I'm testing a shopping cart that depends on an IPriceService I can mock that by saying
_priceServiceMock.WithDiscount(10, Discount.Percent).WithValidProduct()
or something like that.(I realize my example might not be the best, sorry)
The point is that I write an extension method for
Mock<IPriceService>
instead ofIPriceService
making it clear that the extension method is for mocking and making the test more easy to read.The approach is based on the MOM pattern but very much simplified.
martinfowler.com/bliki/ObjectMothe...
Thanks for the clarification, I see what you mean now.
The extension methods you mentioned look nice indeed, and btw I don't see any reason not to have them with NSubstitute. I reckon it's all about personal preferences, so some people want to be explicit about dependencies (
Mock<IService> _service
) while others go for simplicity (IService _service
) because everything but SUT should be mocked (instead of real implementations).