EntityFrameworkCore.Testing adds relational support to the Microsoft EntityFrameworkCore in-memory database provider by mocking relational operations. It's easy to use (usually just a single line of code) with implementations for both Moq and NSubstitute.
The aim of this library is to allow you use the in-memory database provider in unit tests where the SUT invokes a relational operation. It'll allow you to specify expected results for these relational operations. It does not test your relational operations.
Microsoft does not recommend mocking a db context and EntityFrameworkCore.Testing follows this advice by sending operations supported by the in-memory database provider to the in-memory database provider.
Your db context must have an accessible constructor.
Your db context set/query properties must be overridable:
public virtual DbSet<TestEntity> TestEntities { get; set; }
If your db context has an accessible constructor with a single DbContextOptions
or DbContextOptions<TDbContext>
parameter, creating a mocked db context is as easy as:
var mockedDbContext = Create.MockedDbContextFor<TestDbContext>();
Any accessible constructor can be used provided it has a DbContextOptions
or DbContextOptions<TDbContext>
parameter:
var mockedLogger = Mock.Of<ILogger<TestDbContext>>();
var dbContextOptions = new DbContextOptionsBuilder<TestDbContext>().UseInMemoryDatabase(Guid.NewGuid().ToString()).Options;
var mockedDbContext = Create.MockedDbContextFor<TestDbContext>(mockedLogger, dbContextOptions);
Both of the above examples automatically create and use a Microsoft in-memory provider instance for the EntityFrameworkCore provider. If you want more control e.g., to specify the EntityFrameworkCore provider instance, use the builder:
var options = new DbContextOptionsBuilder<TestDbContext>().UseInMemoryDatabase(Guid.NewGuid().ToString()).Options;
var dbContextToMock = new TestDbContext(options);
var mockedDbContext = new MockedDbContextBuilder<TestDbContext>().UseDbContext(dbContextToMock).UseConstructorWithParameters(options).MockedDbContext;
There is no requirement to use the Microsoft in-memory provider. The following example uses the SQLite in-memory provider for a db context with a parameterless constructor:
using (var connection = new SqliteConnection("Filename=:memory:"))
{
connection.Open();
var testEntity = _fixture.Create<TestEntity>();
var dbContextToMock = new TestDbContext(new DbContextOptionsBuilder<TestDbContext>().UseSqlite(connection).Options);
dbContextToMock.Database.EnsureCreated();
var mockedDbContext = new MockedDbContextBuilder<TestDbContext>().UseDbContext(dbContextToMock).MockedDbContext;
mockedDbContext.Set<TestEntity>().Add(testEntity);
mockedDbContext.SaveChanges();
Assert.Multiple(() =>
{
Assert.AreNotEqual(default(Guid), testEntity.Id);
Assert.DoesNotThrow(() => mockedDbContext.Set<TestEntity>().Single());
Assert.AreEqual(testEntity, mockedDbContext.Find<TestEntity>(testEntity.Id));
});
}
Start by creating a mocked db context and, if the SUT requires, populate it as if you were using the real thing:
var testEntity = _fixture.Create<TestEntity>();
var mockedDbContext = Create.MockedDbContextFor<TestDbContext>();
mockedDbContext.Set<TestEntity>().Add(testEntity);
mockedDbContext.SaveChanges();
Assert.Multiple(() =>
{
Assert.AreNotEqual(default(Guid), testEntity.Id);
Assert.DoesNotThrow(() => mockedDbContext.Set<TestEntity>().Single());
Assert.AreEqual(testEntity, mockedDbContext.Find<TestEntity>(testEntity.Id));
});
The Moq implementation of Create.MockedDbContextFor<T>()
returns the mocked db context. If you need the mock itself (e.g., to verify an invocation) use Mock.Get(mockedDbSet)
:
var mockedDbContext = Create.MockedDbContextFor<TestDbContext>();
mockedDbContext.Set<TestEntity>().AddRange(_fixture.CreateMany<TestEntity>().ToList());
mockedDbContext.SaveChanges();
Assert.Multiple(() =>
{
var dbContextMock = Mock.Get(mockedDbContext);
dbContextMock.Verify(m => m.SaveChanges(), Times.Once);
});
Use AddFromSqlResult
to add a from SQL result to the mock. The following will return expectedResult
for any FromSql<TestEntity>
invocation:
var expectedResult = _fixture.CreateMany<TestEntity>().ToList();
var mockedDbContext = Create.MockedDbContextFor<TestDbContext>();
mockedDbContext.Set<TestEntity>().AddFromSqlRawResult(expectedResult);
var actualResult = mockedDbContext.Set<TestEntity>().FromSqlRaw("[dbo].[USP_StoredProcedureWithNoParameters]").ToList();
Assert.Multiple(() =>
{
Assert.IsNotNull(actualResult);
Assert.IsTrue(actualResult.Any());
CollectionAssert.AreEquivalent(expectedResult, actualResult);
});
The following will return expectedResult
if the FromSql
SQL query text contains usp_StoredProcedureWithParameters
and a @Parameter2
SQL parameter with a value of Value2
has been provided:
var expectedResult = _fixture.CreateMany<TestEntity>().ToList();
var sqlParameters = new List<SqlParameter> { new SqlParameter("@Parameter2", "Value2") };
var mockedDbContext = Create.MockedDbContextFor<TestDbContext>();
mockedDbContext.Set<TestEntity>().AddFromSqlRawResult("usp_StoredProcedureWithParameters", sqlParameters, expectedResult);
var actualResult = mockedDbContext.Set<TestEntity>()
.FromSqlRaw("[dbo].[USP_StoredProcedureWithParameters] @Parameter1 @Parameter2",
new SqlParameter("@parameter1", "Value1"),
new SqlParameter("@parameter2", "value2"))
.ToList();
Assert.Multiple(() =>
{
Assert.IsNotNull(actualResult);
Assert.IsTrue(actualResult.Any());
CollectionAssert.AreEquivalent(expectedResult, actualResult);
});
SQL query text matching supports partial, case insensitive matches. Individual parameter name and value matching is also case insensitive. Case insensitive interpolated strings are also supported:
var expectedResult = _fixture.CreateMany<TestEntity>().ToList();
var parameter1 = _fixture.Create<DateTime>();
var parameter2 = _fixture.Create<string>();
var mockedDbContext = Create.MockedDbContextFor<TestDbContext>();
mockedDbContext.Set<TestEntity>().AddFromSqlInterpolatedResult($"usp_StoredProcedureWithParameters {parameter1}, {parameter2.ToUpper()}", expectedResult);
var actualResult = mockedDbContext.Set<TestEntity>().FromSqlInterpolated($"USP_StoredProcedureWithParameters {parameter1}, {parameter2.ToLower()}").ToList();
Assert.Multiple(() =>
{
Assert.IsNotNull(actualResult);
Assert.IsTrue(actualResult.Any());
CollectionAssert.AreEquivalent(expectedResult, actualResult);
});
Use AddFromSqlRawResult
and AddFromSqlInterpolatedResult
to add results for FromSqlRaw
and FromSqlInterpolated
invocations. Refer to the FromSql section above for usage.
Use AddToReadOnlySource
, AddRangeToReadOnlySource
and ClearReadOnlySource
to manage a query source.
var expectedResult = _fixture.CreateMany<TestReadOnlyEntity>().ToList();
var mockedDbContext = Create.MockedDbContextFor<TestDbContext>();
mockedDbContext.Set<TestReadOnlyEntity>().AddRangeToReadOnlySource(expectedResult);
Assert.Multiple(() =>
{
CollectionAssert.AreEquivalent(expectedResult, mockedDbContext.Set<TestReadOnlyEntity>().ToList());
CollectionAssert.AreEquivalent(mockedDbContext.Set<TestReadOnlyEntity>().ToList(), mockedDbContext.TestReadOnlyEntities.ToList());
});
Specifying a from SQL result for a query is exactly the same as for a set.
Refer to the Queries section above for usage.
Adding an execute SQL command result is similar to adding a from SQL result with the main difference being the return type. ExecuteSqlCommand
returns an int
(the number of rows affected by executing the SQL command text).
var commandText = "usp_StoredProcedureWithNoParameters";
var expectedResult = 1;
var mockedDbContext = Create.MockedDbContextFor<TestDbContext>();
mockedDbContext.AddExecuteSqlRawResult(commandText, new List<SqlParameter>(), expectedResult);
var result = mockedDbContext.Database.ExecuteSqlRaw("USP_StoredProcedureWithNoParameters");
Assert.AreEqual(expectedResult, result);
All of the overloads have an optional Action<string, IEnumerable<object>>
parameter which allows you to perform operations post invocation. The following provides a basic example where invoking ExecuteSqlCommand
deletes a specified number of rows from a set:
//Arrange
var mockedDbContext = Create.MockedDbContextFor<TestDbContext>();
var itemsToCreate = 100;
mockedDbContext.Set<TestEntity>().AddRange(_fixture.CreateMany<TestEntity>(itemsToCreate).ToList());
mockedDbContext.SaveChanges();
var numberOfRowsToDelete = itemsToCreate / 2;
var rowsToDelete = mockedDbContext.Set<TestEntity>().Take(numberOfRowsToDelete).ToList();
var remainingRows = mockedDbContext.Set<TestEntity>().Skip(numberOfRowsToDelete).ToList();
mockedDbContext.AddExecuteSqlRawResult("usp_MyStoredProc",
numberOfRowsToDelete,
(providedSql, providedParameters) =>
{
mockedDbContext.Set<TestEntity>().RemoveRange(rowsToDelete);
mockedDbContext.SaveChanges();
});
//Act
var actualResult = mockedDbContext.Database.ExecuteSqlRaw($"usp_MyStoredProc {numberOfRowsToDelete}");
//Assert
Assert.Multiple(() =>
{
Assert.That(actualResult, Is.EqualTo(numberOfRowsToDelete));
Assert.That(mockedDbContext.Set<TestEntity>().Count(), Is.EqualTo(itemsToCreate - numberOfRowsToDelete));
Assert.That(mockedDbContext.Set<TestEntity>().ToList(), Is.EquivalentTo(remainingRows));
});
Use AddExecuteSqlRawResult
and AddExecuteSqlInterpolatedResult
to add results for ExecuteSqlRaw
and ExecuteSqlInterpolated
invocations. Refer to the ExecuteSqlCommand section above for usage.
Whenever you add a from SQL or execute SQL command result, EntityFrameworkCore.Testing sets up both the sync and async methods. It also automatically provides support for all sync and async LINQ queryable operations that are not supported by the Microsoft in-memory provider.
The db context and each set, query and their respective query providers are separate mocks. The following Moq example asserts that the DbContext.SaveChanges
and DbSet<TestEntity>.AddRange
methods were both invoked once.
var mockedDbContext = Create.MockedDbContextFor<TestDbContext>();
mockedDbContext.Set<TestEntity>().AddRange(_fixture.CreateMany<TestEntity>().ToList());
mockedDbContext.SaveChanges();
var dbContextMock = Mock.Get(mockedDbContext);
dbContextMock.Verify(m => m.SaveChanges(), Times.Once);
var byTypeDbSetMock = Mock.Get(mockedDbContext.Set<TestEntity>());
byTypeDbSetMock.Verify(m => m.AddRange(It.IsAny<IEnumerable<TestEntity>>()), Times.Once);
var byPropertyDbSetMock = Mock.Get(mockedDbContext.TestEntities);
byPropertyDbSetMock.Verify(m => m.AddRange(It.IsAny<IEnumerable<TestEntity>>()), Times.Once);