C# Unit Testing Faking and Mocking

C# Unit Testing Faking and Mocking

Let me see if I can butcher this. So far all of our testing has just been with methods which are self contained they don’t call any other classes method or method within the same class. Most applications have many layers, and most modern applications use dependency injection and abstraction. Both of these are not required but simplify the process for Mocking abstraction and Faking Property Values. Were going to use an additional library called “Fake It Easy”, so go ahead in NuGet and install this package.

Fake It Easy Library

Adding Some Abstraction

To get started I abstracted away the methods to Create, Update, Delete and Get Users, this way we can have multiple abstractions that handle the data to different storages, database, local, cloud, etc.

namespace UnitTesting
{
    public interface IDatabaseAccess
    {
        public IEnumerable<User> GetAllUsers();
        public User GetUserById(int userId);
        public DateOnly GetCreationDate(int userId);
        public bool CreateUser(User user);
        public bool UpdateUser(User user);
        public bool DeleteUser(int userId);
    }
    public class DatabaseAccess : IDatabaseAccess
    {
        public IEnumerable<User> GetAllUsers()
        {
            return new[]
            {
                new User { Id = 1, Name = "John Doe", CreationDate = new DateOnly(2024,6,28) },
                new User { Id = 2, Name = "Jane Doe", CreationDate = new DateOnly(1998,5,14) },
                new User { Id = 3, Name = "John Smith", CreationDate = new DateOnly(1978,8,5) }
            };
        }

        public User GetUserById(int userId)
        {
            return new User
            {
                Id = userId,
                Name = "John Doe",
                CreationDate = new DateOnly(2024, 6, 14)
            };
        }

        public DateOnly GetCreationDate(int userId)
        {
            return new DateOnly(2024, 6, 14);
        }
        public bool CreateUser(User user)
        {
            return true;
        }

        public bool UpdateUser(User user)
        {
            return true;
        }

        public bool DeleteUser(int userId)
        {
            return true;
        }
    }
}

Our original class now uses this abstraction layer to make the calls to the storage location, however we now have an Interface being passed in as part of the dependency injection i.e. inversion of control.

namespace UnitTesting
{
    public class UserService
    {
        private readonly IDatabaseAccess _databaseAccess;
        public UserService(IDatabaseAccess databaseAccess)
        {
            _databaseAccess = databaseAccess;
        }

        public IEnumerable<User> GetAllUsers()
        {
            return _databaseAccess.GetAllUsers();
        }

        public User GetUserById(int userId)
        {
            return _databaseAccess.GetUserById(userId);
        }

        public DateOnly GetCreationDate(int userId)
        {
            return _databaseAccess.GetCreationDate(userId);
        }

        public bool CreateUser(User user)
        {
            return _databaseAccess.CreateUser(user);
        }   

        public bool UpdateUser(User user)
        {
            return _databaseAccess.UpdateUser(user);
        }

        public bool DeleteUser(int userId)
        {
            return _databaseAccess.DeleteUser(userId);
        }
    }

    public class User
    {
        public int Id { get; set; } = 0;
        public string Name { get; set; } = string.Empty;
        public DateOnly? CreationDate { get; set; } = null;
    }
}

Mocking and Faking

Back to testing, within our testing we don’t want to write, read, delete or update data from the database, so we want to be able to Mock the Database Access class and Fake values that are expected back from this.

We could do the following but this still means that were calling the real Database Access and the methods that are accessing the storage location.

public class UserServiceTest
{
    private IDatabaseAccess _databaseAccess;
    private UserService _userService;

    public UserServiceTest()
    {
        _databaseAccess = new DatabaseAccess();
        _userService = new UserService(_databaseAccess);
    }

So instead, were going to use Fake It Easy to Mock the Database Access object and then Fake expected values. This is achieved by adding the Using statement for FakeItEasy, and then we can Fake the IDatabaseAccess.

using FakeItEasy;
using FluentAssertions;

namespace UnitTesting
{
    public class UserServiceTest
    {
        private IDatabaseAccess _databaseAccess;
        private UserService _userService;

        public UserServiceTest()
        {
            // Dependency Injection
            _databaseAccess = A.Fake<IDatabaseAccess>();

            // Software Under Test (SUT)
            _userService = new UserService(_databaseAccess);
        }

Now that we have Mocked the Database Access we can now Fake the Expected Results.

Get User By Id

Lets start of simply, just as before within the Arrange section we need to define everything that’s required. We have already Mocked the database access we now need to Fake the return of the GetUserById method being called from the Database Access layer. This is done by using the CallTo method, within which me define exactly which method is being called and then defining the retuned result, in this case a new User Object.

This is where the secret sauce happens, when we make the real call to the GetUserById the result is replaced with the Fake result. Very clever.

[Fact]
public void UserService_GetUserById_ReturnNewUser()
{
	// Arrange
	int userId = 1;
	A.CallTo(() => _databaseAccess.GetUserById(userId)).Returns(new User
                                                                      {
                                                                        Id = userId,
                                                                        Name = "John Doe",
                                                                        CreationDate = new DateOnly(2024, 6, 14)
                                                                        });

	// Act
	var result = _userService.GetUserById(userId);

	// Assert
	result.Should().BeOfType<User>();
	result.Should().BeEquivalentTo(new User
                                         {
                                           Id = userId, 
                                           Name = "John Doe", 
                                           CreationDate = new DateOnly(2024,6,14) 
                                           });
}

Final Mocked and Faked Test Cases

Completed Test Cases Below.

using FakeItEasy;
using FluentAssertions;

namespace UnitTesting
{
    public class UserServiceTest
    {
        private IDatabaseAccess _databaseAccess;
        private UserService _userService;

        public UserServiceTest()
        {
            // Dependency Injection
            _databaseAccess = A.Fake<IDatabaseAccess>();

            // Software Under Test (SUT)
            _userService = new UserService(_databaseAccess);
        }

        [Fact]
        public void UserService_GetAllUsers_ReturnAllUsers()
        {
            // Arrange
            A.CallTo(() => _databaseAccess.GetAllUsers()).Returns(new[]
            {
                new User { Id = 1, Name = "John Doe", CreationDate = new DateOnly(2024,6,28) },
                new User { Id = 2, Name = "Jane Doe", CreationDate = new DateOnly(1998,5,14) },
                new User { Id = 3, Name = "John Smith", CreationDate = new DateOnly(1978,8,5) }
            });

            // Act
            var result = _userService.GetAllUsers();

            // Assert
            result.Should().BeOfType<User[]>();
            result.Should().ContainEquivalentOf(new User
            {
                Id = 1,
                Name = "John Doe",
                CreationDate = new DateOnly(2024, 6, 28)
            });
            result.Should().Contain(x => x.Id == 2 && x.Name == "Jane Doe");
        }

        [Fact]
        public void UserService_GetUserById_ReturnUser()
        {
            // Arrange
            int userId = 1;
            A.CallTo(() => _databaseAccess.GetUserById(userId)).Returns(new User
            {
                Id = userId,
                Name = "John Doe",
                CreationDate = new DateOnly(2024, 6, 14)
            });

            // Act         
            var result = _userService.GetUserById(userId);

            // Assert
            result.Should().NotBeNull();
            result.Id.Should().Be(userId);
            result.Name.Should().NotBeNullOrEmpty();
            result.Name.Should().NotBeNullOrWhiteSpace();
            result.Id.Should().NotBe(0);
        }

        [Fact]
        public void UserService_GetCreationDate_ReturnDate()
        {
            // Arrange
            int userId = 1;
            A.CallTo(() => _databaseAccess.GetCreationDate(userId)).Returns(new DateOnly(2024, 6, 14));

            // Act
            var result = _userService.GetCreationDate(userId);

            // Assert
            result.Should().NotBe(default);
            result.Should().Be(new DateOnly(2024,6,14));
            result.Should().BeAfter(new DateOnly(2024, 6, 13));
            result.Year.Should().Be(2024);
            result.Month.Should().Be(6);
            result.Day.Should().Be(14);
            result.DayOfWeek.Should().Be(DayOfWeek.Friday);
            result.DayOfYear.Should().Be(166);           
        }

        [Fact]
        public void UserService_GetUserById_ReturnNewUser()
        {
            // Arrange
            int userId = 1;
            A.CallTo(() => _databaseAccess.GetUserById(userId)).Returns(new User
            {
                Id = userId,
                Name = "John Doe",
                CreationDate = new DateOnly(2024, 6, 14)
            });

            // Act
            var result = _userService.GetUserById(userId);

            // Assert
            result.Should().BeOfType<User>();
            result.Should().BeEquivalentTo(new User
            {
                Id = userId, 
                Name = "John Doe", 
                CreationDate = new DateOnly(2024,6,14) 
            });
        }

        [Fact]
        public void UserService_CreateUser_ReturnTrue()
        {
            // Arrange
            var user = new User
            {
                Id = 1,
                Name = "John Doe",
                CreationDate = new DateOnly(2024, 6, 14)
            };
            A.CallTo(() => _databaseAccess.CreateUser(user)).Returns(true);

            // Act
            var result = _userService.CreateUser(user);

            // Assert
            result.Should().BeTrue();
        }

        [Fact]
        public void UserService_UpdateUser_ReturnTrue()
        {
            // Arrange
            var user = new User
            {
                Id = 1,
                Name = "John Doe",
                CreationDate = new DateOnly(2024, 6, 14)
            };
            A.CallTo(() => _databaseAccess.UpdateUser(user)).Returns(true);

            // Act
            var result = _userService.UpdateUser(user);

            // Assert
            result.Should().BeTrue();
        }

        [Fact]
        public void UserService_DeleteUser_ReturnTrue()
        {
            // Arrange
            int userId = 1;
            A.CallTo(() => _databaseAccess.DeleteUser(userId)).Returns(true);

            // Act
            var result = _userService.DeleteUser(userId);

            // Assert
            result.Should().BeTrue();
        }
    }
}

When we execute the test cases no real data is written to or read from the storage, but the methods are tested.

Mocked and Faked Results

Wrap Up

The hard thing for me was understanding, its seems that were saying what should come back from the storage location and then testing it against the same object definition. However you have to keep in mind that something may change the data within the method being called, in my case I kept it very simple and directly returned the data. In reality we maybe customizing, editing, updating the data before returning it, this is what were testing.