I have written a generic repository (BaseRepository), where it's Delete method code is:
public virtual void Delete(TEntity entity)
{
if (dbContext.Entry(entity).State == EntityState.Detached)
{
dbContext.Attach(entity);
}
dBContext.Remove(entity);
}
I want to unit-test the code. Since the DbContext is an external dependency, I just want to verify that when I will call the Repository.Delete(entity), then eventually the DbContext.Remove(entity) is to be called once. However, I have to Mock the behavior of dbContext.Entry... just before the actual dbContext.Remove(entity) call. The Unit Test code is:
[Fact]
public void Delete_SomeEntityToRepository_CallsTheAddMethod_To_DbContext()
{
// Arrange
var testObject = new Customer()
{
Name = "Test-Customer"
};
var dbContextMock = new Mock<DbContext>();
var dbEntityEntryMock = new Mock<EntityEntry<Customer>>();
dbContextMock.Setup(d => d.Entry(testObject)).Returns(dbEntityEntryMock.Object);
dbEntityEntryMock.Setup(e => e.State).Returns(EntityState.Unchanged);
// Act
var repository = new CustomerRepository(dbContextMock.Object);
repository.Delete(testObject);
//Assert
dbContextMock.Verify(x => x.Remove(It.Is<Customer>(y => y == testObject)), Times.AtMost(1));
}
However this code crashes, and actually at line:
dbContextMock.Setup(d => d.Entry(testObject)).Returns(dbEntityEntryMock.Object);
with message:
Can not instantiate proxy of class: Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry`1[[Fx.CommonTests.DataAccess....
System.ArgumentException Can not instantiate proxy of class: Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry`1[[Fx.CommonTests.DataAccess.Customer, Fx.CommonTests, Version=4.3.0.0, Culture=neutral, PublicKeyToken=null]]. Could not find a parameterless constructor. (Parameter 'constructorArguments')...
Obviously the DbContext.Entry(...) is the problem. So, any ideas, about how to mock this???
I tried various variants of the code, as found in several articles, about how can I mock such cases, but always the end was the same. Any Ideas?
This is how you can Mock dbcontext to prevent the test error, but I believe that there may be a different approach to consider when designing this test (see but
section below):
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.ChangeTracking;
using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
using Microsoft.EntityFrameworkCore.Metadata;
using Moq;
// ...
[Fact]
public void Delete_SomeEntityToRepository_CallsTheAddMethod_To_DbContext()
{
// Arrange
var testObject = new Customer();
var internalEntityEntry = GetInternalEntityEntry(testObject);
var dbEntityEntryMock = new Mock<EntityEntry<Customer>>(internalEntityEntry);
dbEntityEntryMock.Setup(e => e.State).Returns(EntityState.Unchanged);
var dbContextMock = new Mock<DbContext>();
dbContextMock.Setup(d => d.Entry(testObject)).Returns(dbEntityEntryMock.Object);
// Act
var repository = new CustomerRepository(dbContextMock.Object);
repository.Delete(testObject);
//Assert
dbContextMock.Verify(x => x.Remove(It.Is<Customer>(y => y == testObject)), Times.AtMost(1));
}
private static InternalEntityEntry GetInternalEntityEntry(Customer testObject)
{
return new InternalEntityEntry(
new Mock<IStateManager>().Object,
new RuntimeEntityType(
name: nameof(Customer), type: typeof(Customer), sharedClrType: false, model: new(),
baseType: null, discriminatorProperty: null, changeTrackingStrategy: ChangeTrackingStrategy.Snapshot,
indexerPropertyInfo: null, propertyBag: false,
discriminatorValue: null),
testObject);
}
Full text here: https://gist.github.com/ctrl-alt-d/3d10384a06fa1e0c515e1f182fb83bb0