How to Test Entity Framework Code with IDbContext and Moq

A legacy Entity Framework Core example that wraps DbContext in an interface and verifies writes with Moq, plus current testing guidance.

Article guideContents, topics, tags, and RSS
Get new notes via RSS

When Entity Framework Core was new, I wanted a common interface for DbContext so application code could depend on an abstraction and tests could substitute it with Moq.

The 2018 solution below copied much of the DbContext surface into an IDbContext interface. It demonstrates the approach used by the original project, but it is not a pattern I would introduce today.

Current testing guidance

Microsoft’s current EF Core testing strategy recommends keeping good coverage against the same database engine used in production. When a fast test double is necessary, place a repository or application-specific gateway in front of EF Core and mock that smaller boundary. SQLite in-memory can be useful in some cases, but it does not behave exactly like every production database.

Avoid mocking DbSet for query behavior. LINQ-to-Objects does not reproduce provider translation, collation, transactions, constraints, or database-specific functions. A mock can still be useful when a test only verifies a narrow command interaction, as the historical sample does.

The historical IDbContext approach

The project already used an application-specific IMyContext implemented by MyContext : DbContext, IMyContext. Accessing inherited operations such as SaveChanges() through that interface meant adding the members to every context interface or introducing a shared base interface.

IDbContext

The original solution created an IDbContext with the members used from DbContext, then made IMyContext inherit from it:

// -------------------------------------------------------------------------------------------------
// Copyright (c) Johan Boström. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
// -------------------------------------------------------------------------------------------------

namespace Example.EntityFramework.Testing.Data.Abstract
{
    using System;
    using System.Collections.Generic;
    using System.Threading;
    using System.Threading.Tasks;
    using Microsoft.EntityFrameworkCore;
    using Microsoft.EntityFrameworkCore.ChangeTracking;
    using Microsoft.EntityFrameworkCore.Infrastructure;
    using Microsoft.EntityFrameworkCore.Internal;

    public interface IDbContext : IDisposable, IInfrastructure<IServiceProvider>, IDbContextDependencies, IDbSetCache, IDbQueryCache, IDbContextPoolable
    {
        DatabaseFacade Database { get; }
        ChangeTracker ChangeTracker { get; }
        EntityEntry Add(object entity);
        EntityEntry<TEntity> Add<TEntity>(TEntity entity) where TEntity : class;
        Task<EntityEntry> AddAsync(object entity, CancellationToken cancellationToken = default(CancellationToken));
        Task<EntityEntry<TEntity>> AddAsync<TEntity>(TEntity entity, CancellationToken cancellationToken = default(CancellationToken)) where TEntity : class;
        void AddRange(IEnumerable<object> entities);
        void AddRange(params object[] entities);
        Task AddRangeAsync(IEnumerable<object> entities, CancellationToken cancellationToken = default(CancellationToken));
        Task AddRangeAsync(params object[] entities);
        EntityEntry<TEntity> Attach<TEntity>(TEntity entity) where TEntity : class;
        EntityEntry Attach(object entity);
        void AttachRange(params object[] entities);
        void AttachRange(IEnumerable<object> entities);
        EntityEntry<TEntity> Entry<TEntity>(TEntity entity) where TEntity : class;
        EntityEntry Entry(object entity);
        bool Equals(object obj);
        object Find(Type entityType, params object[] keyValues);
        TEntity Find<TEntity>(params object[] keyValues) where TEntity : class;
        Task<TEntity> FindAsync<TEntity>(params object[] keyValues) where TEntity : class;
        Task<object> FindAsync(Type entityType, object[] keyValues, CancellationToken cancellationToken);
        Task<TEntity> FindAsync<TEntity>(object[] keyValues, CancellationToken cancellationToken) where TEntity : class;
        Task<object> FindAsync(Type entityType, params object[] keyValues);
        int GetHashCode();
        DbQuery<TQuery> Query<TQuery>() where TQuery : class;
        EntityEntry Remove(object entity);
        EntityEntry<TEntity> Remove<TEntity>(TEntity entity) where TEntity : class;
        void RemoveRange(IEnumerable<object> entities);
        void RemoveRange(params object[] entities);
        int SaveChanges(bool acceptAllChangesOnSuccess);
        int SaveChanges();
        Task<int> SaveChangesAsync(bool acceptAllChangesOnSuccess, CancellationToken cancellationToken = default(CancellationToken));
        Task<int> SaveChangesAsync(CancellationToken cancellationToken = default(CancellationToken));
        DbSet<TEntity> Set<TEntity>() where TEntity : class;
        string ToString();
        EntityEntry Update(object entity);
        EntityEntry<TEntity> Update<TEntity>(TEntity entity) where TEntity : class;
        void UpdateRange(params object[] entities);
        void UpdateRange(IEnumerable<object> entities);
    }
}

With IMyContext : IDbContext, application services could receive IMyContext instead of the concrete class.

Testing with ease

That interface could then be mocked without constructing DbContext. The sample service adds an entity and saves the context; the test verifies those two commands.

Sample

The business logic:

// -------------------------------------------------------------------------------------------------
// Copyright (c) Johan Boström. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
// -------------------------------------------------------------------------------------------------

namespace Example.EntityFramework.Testing.BusinessLogic
{
    using System.Threading.Tasks;
    using Data.Db;

    public class Business
    {
        private readonly ICustomContext context;

        public Business(ICustomContext context)
        {
            this.context = context;
        }

        public void AddCustomEntity(CustomEntity testEntity)
        {
            context.CustomEntities.Add(testEntity);
            context.SaveChanges();
        }
    }
}

The test verifies that the entity was added to the set and that SaveChanges() was called:

// -------------------------------------------------------------------------------------------------
// Copyright (c) Johan Boström. All rights reserved.
// Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
// -------------------------------------------------------------------------------------------------

namespace Example.EntityFramework.Testing.Tests
{
    using System.Threading;
    using System.Threading.Tasks;
    using BusinessLogic;
    using Data.Db;
    using Microsoft.EntityFrameworkCore;
    using Moq;
    using Xunit;

    public class BusinessTests
    {
        private readonly Mock<ICustomContext> testContext;
        private readonly Mock<DbSet<CustomEntity>> testEntities;

        public BusinessTests()
        {
            // Initiate ICustomContext
            testContext = new Mock<ICustomContext>();

            // Initiate DbSet
            testEntities = new Mock<DbSet<CustomEntity>>();

            // Setup DbSet
            testContext.Setup(ctx => ctx.CustomEntities).Returns(testEntities.Object);
        }

        [Fact]
        public void AddingTestEntity()
        {
            var business = new Business(testContext.Object);
            business.AddCustomEntity(new CustomEntity
            {
                Id = 1,
                Name = "TestName"
            });

            testEntities.Verify(set => set.Add(It.Is<CustomEntity>(e => e.Id == 1 && e.Name == "TestName")), Times.Once);
            testContext.Verify(ctx => ctx.SaveChanges(), Times.Once);
        }
    }
}

Conclusion

This interface made the original command-style test straightforward, but copying the complete DbContext API creates a wide and fragile abstraction. Framework-internal interfaces in the sample also changed as EF Core evolved.

For new code, test important EF behavior against a real database and introduce a smaller domain-specific boundary only where it earns its maintenance cost. The original sample remains in zarxor/Example.EntityFramework.Testing.

Johan Boström smiling in a navy jacket and white shirt.
Author portrait

About the author

Johan Boström

Technology leader · Solution architect · Developer

My primary work is leading developers and working as a solution architect. I still develop when hands-on work helps the team or the solution.

I mainly work across systems, integrations, and AI. This archive keeps the practical details, lessons, and implementation notes worth finding again.