Unit Tests Done Right (Part 2)
Tags: testing, dotnet, csharp, beginners
Published: 2022-05-09 12:22:36

---
title: Unit Tests Done Right (Part 2)
published: true
date: 2022-05-09 11:56:01 UTC
tags: testing, dotnet, csharp, beginners
series: Unit Tests Done Right
canonical_url: https://zagosk.in/unit-tests-done-right-part-2
---

In the previous article, [Unit Tests Done Right (Part 1)](https://dev.to/powerz/unit-tests-done-right-4e8j), 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.

## Testing LINQ

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.

```csharp
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?

1. We want to make sure that this method returns a user by ID if it hasn't been deleted.
2. The method returns `null` if the user with the given ID exists, but is marked as deleted.
3. The method returns `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):

```csharp
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:

```csharp
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.

```csharp
[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:

```csharp
[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:

```csharp
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:

![LINQ test results](https://cdn.hashnode.com/res/hashnode/image/upload/v1651907578913/12N7Trvxq.png)

Looks green enough, so let's move on to the next example.

## Testing Mappings (AutoMapper)

### Do we need tests for mappings at first place?

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:

1. It's easy to overlook small, yet important differences in data types. For example, when a property of class A is of type `DateTimeOffset` and the corresponding property of class B is of type `DateTime`. The default mapping will not crash, but will produce an incorrect result.
2. New or removed properties. With mapping tests, whenever we refactor one of the classes, it's impossible to forget to change the other (because the _well-written_ tests won't pass).
3. Typos and different spelling. We're all humans and often don't notice typos, which, in turn, can lead to incorrect mapping results. Example:

```csharp
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 Seriali **z** ed and Seriali **s** ed 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](https://automapper.org), but from a testing standpoint, the choice of mapper makes no difference. For instance, we can replace AutoMapper with [Mapster](https://github.com/MapsterMapper/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:

```csharp
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:

```csharp
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:

```csharp
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:

```csharp
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:

1. Generate a random instance of the `User` class.
2. Map it to the `UserHttpResponse` type.
3. Verify the `Name` property.
4. Verify the remaining properties by comparing `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:

```csharp
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:

```csharp
return new List<object[]>
                       {
                           new object[] { typeof(BlogPost), typeof(BlogPostDeletedEvent) },
                           new object[] { typeof(Comment), typeof(CommentDeletedEvent) }
                       }
                    .GetEnumerator();

```

Pretty cool, isn't it?

## Final Words

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)](https://dev.to/powerz/unit-tests-done-right-4e8j), provide a solid background and understanding of the key principles for writing clean, meaningful, and most importantly, useful unit tests.

Cheers!