How To Deal With Database Dependencies With In-Memory Database And Entity Framework Core

In this post we will address unit tests with the Entity Framework Core in-memory database provider to deal with one of the existing dependencies in a project that I developed a few months ago in .NET 5 (WebApi and Blazor WASM). It consists of a project API with its respective SPA application that exposes information on countries and their codes. It is also defined under a clean architecture and good practices of clean code and design patterns.

Software Engineering - Unit Test: How to deal with Database dependencies with in-memory Database and Entity Framework Core

Initially, we will need to have a Test project within our solution, personally, I like to use a test project from xUnit.

Software Engineering - Unit Test: How to deal with Database dependencies with in-memory Database and Entity Framework Core

After creating our test project we will need to install the Nuget package Microsoft.EntityFrameworkCore.InMemory using the Visual Studio package management console.

Install-Package Microsoft.EntityFrameworkCore.InMemory -Version 5.0.9

Within the project structure, there is a layer called Infrastructure which contains the real data context of Entity Framework Core that connects to the database. It also contains the migrations to the database, a definition of the Generic Repository and the pattern Unit Of Work to manage operations in a centralized way towards the database. Inside the Repositories directory there is a custom implementation for each object represented based on a database table. What we want in this post is to be able to test the functionality of the repository in isolation in our test project, all this without having to connect to the real database.

Software Engineering - Unit Test: How to deal with Database dependencies with in-memory Database and Entity Framework Core

Let's go to the code.

We will need to create an Entity Framework Core context which we will use to pass as a parameter to our fake implementation of UnitOfWork. We will create a directory called ResourcesDatabase at the root of the test project, and then we will create a class called BaseContextTest.cs.

We will add the following code.

using Countries.Infra.Data.DataContext;
using Microsoft.EntityFrameworkCore;
using System;
namespace Countries.UnitTests.ResourcesDatabase {
    public class BaseContextTest: IDisposable {
        protected readonly CountriesDbContext _context;
        public BaseContextTest() {
            var options = new DbContextOptionsBuilder < CountriesDbContext > ().UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()).Options;
            this._context = new CountriesDbContext(options);
            this._context.Database.EnsureCreated();
            DbInitializerTest.Initialize(this._context);
        }
        public void Dispose() {
            this._context.Database.EnsureDeleted();
            this._context.Dispose();
        }
    }
}

Note
In the BaseContextTest class it is necessary to configure the type of database, in this case in memory, and then pass that configuration to an instance of our real context which is the one we need to circumvent in order to test the functionality of the repository.

We will also create a class to initialize our in-memory database.

using Countries.Infra.Data.DataContext;
using Countries.Models.Models;
using System.Linq;
namespace Countries.UnitTests.ResourcesDatabase {
    public class DbInitializerTest {
        public static void Initialize(CountriesDbContext context) {
            if (context.Countries.Any()) {
                return;
            }
            Seed(context);
        }
        private static void Seed(CountriesDbContext context) {
            // Create Countries
            var countries = new Country[] {
                new Country {
                    Name = "Francia", Alpha_2 = "Fr", Alpha_3 = "Fra", NumericCode = "250"
                },
                new Country {
                    Name = "Alemania", Alpha_2 = "De", Alpha_3 = "Deu", NumericCode = "276"
                },
                new Country {
                    Name = "Guatemala", Alpha_2 = "Gt", Alpha_3 = "Gtm", NumericCode = "320"
                }
            };
            context.Countries.AddRange(countries);
            context.SaveChanges();
        }
    }
}

After preparing a fake implementation to test the repository functionality, we proceed to create a class called CountriesRepositoryTest.cs within the Infrastructure.Repositories.Tests / Countries path and within this class we can create unit tests that refer to the different behavior cases that may occur in the repository code. We will test the case in which our repository returns a list of elements from a table in the database, for this we will create a method that refers to that test.

using Countries.Infra.Data.Repositories.Custom;
using Countries.Infra.Data.Repositories.Generic;
using Countries.UnitTests.ResourcesDatabase;
using Moq;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace Countries.UnitTests.Infastructure.Repositories.Tests.Countries {
    public class CountriesRepositoryTests: BaseContextTest {
        [Fact]
        public async Task Countries_List_Success() {
            // Arrange
            var unitOfWorkMock = new Mock < IUnitOfWork > ();
            unitOfWorkMock.SetupGet(x => x.Context).Returns(base._context);
            // Act
            var repository = new CountriesRepository(unitOfWorkMock.Object);
            var result = await Task.Run(() => repository.Get(null, null, string.Empty));
            // Assert
            Assert.NotNull(result);
            Assert.True(condition: result.Any());
            Assert.Equal("Francia", result.ElementAt(0).Name);
            Assert.Equal("Fr", result.ElementAt(0).Alpha_2);
            Assert.Equal("Fra", result.ElementAt(0).Alpha_3);
            Assert.Equal("250", result.ElementAt(0).NumericCode);
        }
    }
}

Expert Tip

When writing unit tests, include only one assertion per test, because if you add multiple assertions in a test case, it is not guaranteed that all of them will be executed. In most unit test frameworks, when an assertion in a unit test fails, subsequent assertions are automatically considered failed. This can be confusing, as functionality in components that are actually correct are shown to fail.

We proceed to execute our newly created unit test and we can verify that the test passes correctly.

Software Engineering - Unit Test: How to deal with Database dependencies with in-memory Database and Entity Framework Core

Conclusions

  • Using a Mocks library is quite useful in code testing since it avoids us creating classes to cover each of the behaviors or results that our code can take and that we need to test.
  • In this post, we saw one of the utilities of in-memory databases applied to code testing and the ease with which we can mock up a database context without having to connect to the real database to test the access functionality. to data and other operations involved.
  • The unit test dependency mock technique is a professional, advanced, and widely used technique that exists in software engineering to write short and precise unit tests and be able to test components that are correctly decoupled.

Link to the repository on GitHub

References