Article guideContents, topics, tags, and 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.
