
Software Architect, Staff Engineer | .NET, C#, microservices | Design and create scalable, high-load, fault tolerant and distributed backend services and Web APIs 👨🏼‍💻
Search for a command to run...

Software Architect, Staff Engineer | .NET, C#, microservices | Design and create scalable, high-load, fault tolerant and distributed backend services and Web APIs 👨🏼‍💻
Don't know if you are going to continue the article series, but could suggest to reveal us your vision of testing DI containers definitions (especially to check if a service is defined in a container or not, quite repetitive errors), testing configuration (maybe both presence and overrides), testing of log records, testing of routes and access rights of API endpoints.
This article is supposed to be the last one in this short series. At the moment I’m working on an open source project which I’m going to blog about later on. The amount of work is rather decent so I’m afraid I don’t have ETA at that point.
As for DI, routing and security, I prefer to cover these with integration tests. Nevertheless, the first two could be nicely covered by unit tests.
Sometimes in the arrange section of tests we can see repeatable (or very similar) code blocks and looks like it is a good candidate to extract identical (or very similar) code to a method to reuse by several tests. But I think the readability and maintainability of the test could degrade. What do you think about this? Should we apply the same best practices for writing tests as for business logic?
There are very similar code in the Arrange sections of Should_return_user_by_id_unless_deleted and Should_return_null_when_user_is_deleted tests, which could be extracted, so I wonder if you prefer to duplicate or not in such cases.
Good question, Roman Izosimov. Usually I prefer to declare simple things explicitly in the "arrange" section, while more complex setup may deserve reusable methods or extension methods for Fixture e.g. F.CreateCustomerWithOrders().
In this series we will review some best practices and must-have libraries which will get your unit tests to the next level.
Write clean and rock-solid unit tests in .NET
(for the "AI detectives": yes, I type "—" on my keyboard by pressing OPT+Shift+-) It’s been a while since my last post. A lot has happened, and it’s time to share some good stuff. To be honest, I missed writing 🙂 As always, we’ll keep it dry and use...

First In First Out (FIFO) in Message Brokers
Today, let's dive into the somewhat unpredictable and potentially risky behavior of the C# 8.0 "Using Declaration" feature. As always, no boring long introduction paragraphs, let's get straight to the point! What is Using Declaration? In C# 7.0 we di...
In recent months, the buzz around AI assistants, particularly our "old new" friend ChatGPT, has been impossible to ignore. If you've ever requested it to generate code for you, chances are you received a fairly decent result. However, it likely requi...
The title may sound bold and clickbait-ish, but it is what it is... A few days ago I stumbled upon a post on LinkedIn (unfortunately, I didn't save the link) in which a programmer demonstrated his solution to a LeetCode challenge. The code was fine a...
In the previous article, Unit Tests Done Right (Part 1), we explored some of the best practices for unit testing and then compiled a list of must-have libraries that greatly improve the quality of tests. However, it didn't cover some common scenarios like testing LINQ and mappings, so I decided to fill this gap with one more example-driven post.
Ready to further improve your unit testing skills? Lets' get started🍿
The examples in this article make extensive use of the awesome tools described in the previous post, so it would be a good idea to start with Part 1 so that the code we're about to analyze makes more sense.
It's a matter of fact that all C# developers love LINQ, but we should also treat it with respect and cover queries with tests. By the way, this is one of the many advantages of LINQ over SQL (have you ever seen a real person who wrote at least one unit test for SQL? Neither have I).
Let's take a look at an example.
public class UserRepository : IUserRepository
{
private readonly IDb _db;
public UserRepository(IDb db)
{
_db = db;
}
public Task<User?> GetUser(int id, CancellationToken ct = default)
{
return _db.Users
.Where(x => x.Id == id)
.Where(x => !x.IsDeleted)
.FirstOrDefaultAsync(ct);
}
// other methods
}
In this example, we have a typical repository with a method that returns users by ID, and _db.Users returns IQueryable<User>. So what do we need to test here?
null if the user with the given ID exists, but is marked as deleted.null if the user with the given ID doesn't exist.In other words, all Where, OrderBy and other method calls must be covered by tests. Now let's write and discuss the first test (đź’ˇreminder: the test structure was explained in the previous article):
public class UserRepositoryTests
{
public class GetUser : UserRepositoryTestsBase
{
[Fact]
public async Task Should_return_user_by_id_unless_deleted()
{
// arrange
var expectedResult = F.Build<User>()
.With(x => x.IsDeleted, false)
.Create();
var allUsers = F.CreateMany<User>().ToList();
allUsers.Add(expectedResult);
Db.Users.Returns(allUsers.Shuffle().AsQueryable());
// act
var result = await Repository.GetUser(expectedResult.Id);
// assert
result.Should().Be(expectedResult);
}
[Fact]
public async Task Should_return_null_when_user_is_deleted()
{
// see below
}
[Fact]
public async Task Should_return_null_when_user_doesnt_exist()
{
// see below
}
}
public abstract class UserRepositoryTestsBase
{
protected readonly Fixture F = new();
protected readonly UserRepository Repository;
protected readonly IDb Db;
protected UserRepositoryTestsBase()
{
Db = Substitute.For<IDb>();
Repository = new UserRepository(Db);
}
}
}
First of all, we created a user that meets the requirements (not deleted) and added it to a bunch of other users (with random different IDs and IsDeleted values). Then we mocked the data source to return the shuffled dataset. Note, that we shuffled the list of users to place the expectedResult in a random position. Finally, we called Repository.GetUser and verified the result.
Shuffle() is a small yet useful extension method:
public static class EnumerableExtensions
{
private static readonly Random _randomizer = new();
public static T GetRandomElement<T>(this ICollection<T> collection)
{
return collection.ElementAt(_randomizer.Next(collection.Count));
}
public static IEnumerable<T> Shuffle<T>(this IEnumerable<T> objects)
{
return objects.OrderBy(_ => Guid.NewGuid());
}
}
The second test is almost identical to the first one.
[Fact]
public async Task Should_return_null_when_user_is_deleted()
{
// arrange
var testUser = F.Build<User>()
.With(x => x.IsDeleted, true)
.Create();
var allUsers = F.CreateMany<User>().ToList();
allUsers.Add(testUser);
Db.Users.Returns(allUsers.Shuffle().AsQueryable());
// act
var result = await Repository.GetUser(testUser.Id);
// assert
result.Should().BeNull();
}
Here we mark our user as deleted and check that the result is null.
For the last test, we generate a list of random users and a unique ID that doesn't belong to any of them:
[Fact]
public async Task Should_return_null_when_user_doesnt_exist()
{
// arrange
var allUsers = F.CreateMany<User>().ToList();
var userId = F.CreateIntNotIn(allUsers.Select(x => x.Id).ToList());
Db.Users.Returns(allUsers.Shuffle().AsQueryable());
// act
var result = await Repository.GetUser(userId);
// assert
result.Should().BeNull();
}
CreateIntNotIn() is another useful method often used in tests:
public static int CreateIntNotIn(this Fixture f, ICollection<int> except)
{
var maxValue = except.Count * 2;
return Enumerable.Range(1, maxValue)
.Except(except)
.ToList()
.GetRandomElement();
}
Let's run our tests:

âś… Looks green enough, so let's move on to the next example.
Despite many devs claim that it is boring or a waste of time, I believe unit testing of mappings plays a key role in the development process for the following reasons:
DateTimeOffset and the corresponding property of class B is of type DateTime. The default mapping will not crash, but will produce an incorrect result.public class ErrorInfo
{
public string StackTrace { get; set; }
public string SerializedException { get; set; }
}
public class ErrorOccurredEvent
{
public string StackTrace { get; set; }
public string SerialisedException { get; set; }
}
public class ErrorMappings : Profile
{
public ErrorMappings()
{
CreateMap<ErrorInfo, ErrorOccurredEvent>();
}
}
It's pretty easy to overlook the issue of different spelling in the code above, and Rider / Resharper won't help with that either, because both Serialized and Serialised look fine to it. In this case, the mapper will always set the target property to null, which is definitely not desirable.
I hope I managed to convince you and proved the value of unit tests for mappings, so let's move on to the next example. We're going to use AutoMapper, but from a testing standpoint, the choice of mapper makes no difference. For instance, we can replace AutoMapper with Mapster and it won't affect our tests in any way. Moreover, the existing tests will indicate whether our mapping refactoring was successful or not, which is one of the points of having unit tests 🙂
Say we have these entities:
public class User
{
public int Id { get; init; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public bool IsAdmin { get; set; }
public bool IsDeleted { get; set; }
}
public class UserHttpResponse
{
public int Id { get; init; }
public string Name { get; set; }
public string Email { get; set; }
public bool IsAdmin { get; set; }
}
public class BlogPost
{
public int Id { get; set; }
public int UserId { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public string Text { get; set; }
}
public class BlogPostDeletedEvent
{
public int Id { get; set; }
public int UserId { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public string Text { get; set; }
}
public class Comment
{
public int Id { get; set; }
public int BlogId { get; set; }
public int UserId { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public string Text { get; set; }
}
public class CommentDeletedEvent
{
public int Id { get; set; }
public int BlogId { get; set; }
public int UserId { get; set; }
public DateTimeOffset CreatedAt { get; set; }
public string Text { get; set; }
}
And mappings:
public class MappingsSetup : Profile
{
public MappingsSetup()
{
CreateMap<User, UserHttpResponse>()
.ForMember(x => x.Name, _ => _.MapFrom(x => $"{x.FirstName} {x.LastName}"));
CreateMap<BlogPost, BlogPostDeletedEvent>();
CreateMap<Comment, CommentDeletedEvent>();
}
}
Nothing especially fancy: the mapping for User >> UserHttpResponse is slightly customized while the other two are default "map as is" instructions. Let's write tests for our mapping profile.
To begin with, here is the base class that you can use for all unit tests for mappings:
public abstract class MappingsTestsBase<T> where T : Profile, new()
{
protected readonly Fixture F;
protected readonly IMapper M;
public MappingsTestsBase()
{
F = new Fixture();
M = new MapperConfiguration(x => { x.AddProfile<T>(); }).CreateMapper();
}
}
And our first test for User >> UserHttpResponse mapping:
public class MappingsTests
{
public class User_TO_UserHttpResponse : MappingsTestsBase<MappingsSetup>
{
[Theory, AutoData]
public void Should_map(User source)
{
// act
var result = M.Map<UserHttpResponse>(source);
// assert
result.Name.Should().Be($"{source.FirstName} {source.LastName}");
result.Should().BeEquivalentTo(source, _ => _.Excluding(x => x.FirstName)
.Excluding(x => x.LastName)
.Excluding(x => x.Password)
.Excluding(x => x.IsDeleted));
source.Should().BeEquivalentTo(result, _ => _.Excluding(x => x.Name));
}
}
}
In this test we:
User class.UserHttpResponse type.Name property.result ≡ source and source ≡ result (in order not to miss anything). Note that we exclude every property that is not present in any of the classes, instead of using ExcludingMissingMembers() which excludes properties with typos and distinct spelling (the test won't be able to detect the SerializedException vs SerialisedException issue).Default mapping tests for classes with the same properties (e.g. BlogPost >> BlogPostDeletedEvent) can be written in a more generic and elegant way:
public class SimpleMappings : MappingsTestsBase<MappingsSetup>
{
[Theory]
[ClassData(typeof(MappingTestData))]
public void Should_map(Type sourceType, Type destinationType)
{
// arrange
var source = F.Create(sourceType, new SpecimenContext(F));
// act
var result = M.Map(source, sourceType, destinationType);
// assert
result.Should().BeEquivalentTo(source);
}
private class MappingTestData : IEnumerable<object[]>
{
public IEnumerator<object[]> GetEnumerator()
{
return new List<object[]>
{
new object[] { typeof(BlogPost), typeof(BlogPostDeletedEvent) },
new object[] { typeof(Comment), typeof(CommentDeletedEvent) }
}
.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}
You may have noticed that lovely [ClassData(typeof(MappingTestData))] attribute. This is a clean way to separate the test data generated by the MappingTestData class from the test implementation. As you can see, adding a new test for a new default mapping is a matter of one line of code:
return new List<object[]>
{
new object[] { typeof(BlogPost), typeof(BlogPostDeletedEvent) },
new object[] { typeof(Comment), typeof(CommentDeletedEvent) }
}
.GetEnumerator();
Pretty cool, isn't it?
Looks like you've read this far🎉 I hope it wasn't too boring🙂
Anyway, today we've dealt with unit tests for LINQ and mappings, which, in combination with techniques described in the previous post Unit Tests Done Right (Part 1), provide a solid background and understanding of the key principles for writing clean, meaningful, and most importantly, useful unit tests.
Cheers!