Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Integration Testing of a .NET Core Application with a Database Dependency

0.00/5 (No votes)
10 May 2020 1  
The test problems you face when having a database dependency and how to resolve these problems
The problems that exists with .NET Core database tests are briefly explained. Subsequently, a solution is explained with concrete code examples available on GitHub.

Introduction

Automated testing of an application with a database dependency is a tough job. Unit tests are not going to help you since databases are not perfectly mockable. When doing an update, delete or insert, you can check the result of your query by running a select query afterwards but then you do not check for the unwanted side effects. Maybe more tables are affected than necessary, or maybe more queries are executed than necessary. Here is the solution for such problems.

Background

It will be helpful to have some experience with TDD for .NET Core, preferably with xUnit and also EF Core experience is helpful.

Using the Code

At first, here is the code to test. There is a database context dependency to inject and a method to save the data into the database. The added and saved entity is returned as the output of the method.

public class TodoRepository : ITodoRepository
{
   private readonly ProjectContext _projectContext;

   public TodoRepository(ProjectContext projectContext)
   {
        _projectContext = projectContext;
   }

   public async Task<Entities.TodoItem> SaveItem(TodoItem item)
   {
       var newItem = new Entities.TodoItem()
       {
            To do = item.Todo
       };
       _projectContext.TodoItems.Add(newItem);
       await _projectContext.SaveChangesAsync();
       return newItem;
   }
}

Logically, this dependency needs to be resolved properly. There is a method in the Startup class for this purpose. The repository class described above is added here, just like the database context it needs and the controllers depending on it.

public void ConfigureServices(IServiceCollection services)
{
   services.AddControllers();
   services.AddDbContext<ProjectContext>(options =>
   {
       var connectionString = Configuration["ConnectionString"];
       options.UseSqlite(connectionString,
       sqlOptions =>
       {
            sqlOptions.MigrationsAssembly
               (typeof(Startup).GetTypeInfo().Assembly.GetName().Name);
       });
   });
   services.AddTransient<ITodoRepository, TodoRepository>();
}

It is nice that we can resolve the dependency to test but now we need to use the dependency for testing purposes. This is how such a test should look like:

public class TodoRepositoryTest : TestBase<ITodoRepository>
{
    private ITodoRepository _todoRepository;

    private readonly List<(object Entity, EntityState EntityState)> _entityChanges =
            new List<(object Entity, EntityState entityState)>();

    public TodoRepositoryTest(WebApplicationFactory<Startup> webApplicationFactory) : 
            base(webApplicationFactory, @"Data Source=../../../../project3.db")
    {
    }

    [Fact]
    public async Task SaveItemTest()
    {
        // arrange
        var todoItem = new TodoItem()
        {
            To do = "TestItem"
        };
            
        // act
        var savedEntity = await _todoRepository.SaveItem(todoItem);

        // assert
        Assert.NotNull(savedEntity);
        Assert.NotEqual(0, savedEntity.Id);
        Assert.Equal(todoItem.Todo, savedEntity.Todo);
        var onlyAddedItem = _entityChanges.Single();
        Assert.Equal(EntityState.Added,onlyAddedItem.EntityState);
        var addedEntity = (Database.Entities.TodoItem)onlyAddedItem.Entity;
        Assert.Equal(addedEntity.Id, savedEntity.Id);
    }

    public override void AddEntityChange(object newEntity, EntityState entityState)
    {
        _entityChanges.Add((newEntity, entityState));
    }

    protected override void SetTestInstance(ITodoRepository testInstance)
    {
        _todoRepository = testInstance;
    }
}

The class has the following methods and variables:

  • _todoRepository: The instance to test
  • _entityChanges: The entitychanges (the kind of change like added/updated and the entity itself) to assert on
  • SaveItemTest: The test method that does the real work. It creates the method argument, calls the method and then asserts on everything that is relevant: if the primary key is assigned a value, if there is really only one entity changed, if this entity change is really an addition (not just an update) and if the added entity has the type we expect it to have. We assert on this without running a select query afterwards. This is possibly because we just receive all entity changes while running the test via another method.
  • AddEntityChange: This is the other method just mentioned. It receives all the entity changes with the entity itself included.
  • SetTestInstance: To use the test instance named _todoRepository, it needs to be set by this method.

The SetTestInstance method is called from the base class which has all boilerplate code to setup database integration tests. This is the base class:

public abstract class TestBase<TTestType> : IDisposable, ITestContext, 
                IClassFixture<WebApplicationFactory<Startup>>
{
    protected readonly HttpClient HttpClient;

    protected TestBase(WebApplicationFactory<Startup> webApplicationFactory,
                       string newConnectionString)
    {
        HttpClient = webApplicationFactory.WithWebHostBuilder(whb =>
        {
            whb.ConfigureAppConfiguration((context, configbuilder) =>
            {
                configbuilder.AddInMemoryCollection(new Dictionary<string, string>
                {
                        {"ConnectionString", newConnectionString}
                });
            });
            whb.ConfigureTestServices(sc =>
            {
                sc.AddSingleton<ITestContext>(this);
                ReplaceDbContext(sc, newConnectionString);
                var scope = sc.BuildServiceProvider().CreateScope();
                var testInstance = scope.ServiceProvider.GetService<TTestType>();
                SetTestInstance(testInstance);
             });
         }).CreateClient();
     }

     public void Dispose()
     {
         Dispose(true);
         GC.SuppressFinalize(this);
     }

     public abstract void AddEntityChange(object newEntity, EntityState entityState);

     private void ReplaceDbContext(IServiceCollection serviceCollection, 
                                   string newConnectionString)
     {
         var serviceDescriptor =
             serviceCollection.FirstOrDefault
                   (descriptor => descriptor.ServiceType == typeof(ProjectContext));
         serviceCollection.Remove(serviceDescriptor);
         serviceCollection.AddDbContext<ProjectContext, TestProjectContext>();
     }

     protected abstract void SetTestInstance(TTestType testInstance);

     protected virtual void Dispose(bool disposing)
     {
         if (disposing) HttpClient.Dispose();
     }
}
The most important part of the base class is the constructor. In xUnit, the initialization of a test is typically done in the constructor. Once this is done correctly, the test can be easily implemented. These are the most important methods that are called there:
  • AddInMemoryCollection: Here, we set configuration parameters that are specific for testing, the connection string in our case.
  • AddSingleton: The test itself is resolved as a singleton in order to get the updates from the database context.
  • ReplaceDbContext: The existing database context needs to replaced by a database context that inherits from it to extend its functionality with the possibility to update the test.
  • CreateClient: A method call to trigger the code in the Program class and Startup class.
  • GetService: The instance to call test method from need be resolved with this method call. This is possible since the code in the Program class and Startup class is triggered.
  • SetTestInstance: The instance to call test methods from needs to be set by calling this method.

Since we introduce a new dependency here (TestProjectContext), we need to implement this dependency:

public class TestProjectContext : ProjectContext
{
   private readonly ITestContext _testContext;

   public TestProjectContext(DbContextOptions<ProjectContext> options, 
                             ITestContext testContext) : base(options)
   {
        _testContext = testContext;
   }

   public override async Task<int> SaveChangesAsync
                   (CancellationToken cancellationToken = new CancellationToken())
   {
        Action updateEntityChanges = () => { };
        var entries = ChangeTracker.Entries();
        foreach (var entry in entries)
        {
             var state = entry.State;
             updateEntityChanges += () => _testContext.AddEntityChange(entry.Entity, state);
        }

        var result = await base.SaveChangesAsync(cancellationToken);
        updateEntityChanges();
        return result;
    }
}

Each time some entity change is saved (which is in this application always done by SaveChangesAsync), the changes are copied from the ChangeTracker into an update action that is invoked after the changes are really saved to the database. In this way, our test class always receives the saved changes too assert on. Testing problems are solved now. The full code is on GiHub.

Points of Interest

I really like this way of working I discovered. Writing the boilerplate code is annoying, but it is a one time job. The code is reusable for each database test using Entity Framework Core 3.1. I can test on everything I need to test on. The updates, inserts and deletes that are done, the affected entities and also if the total number of entities that are changed, make sense. This can all be done without running any select queries after the tests.

History

  • 10th May, 2020: Initial version

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here