英文:
Trying to Unit Test Generic Repository based on .NET EF Core fails when dealing with DbContext.Entry
问题
I have written a generic repository (BaseRepository<TEntity>), where its 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 call the Repository.Delete(entity), then eventually the DbContext.Remove(entity) is 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 at the line:
dbContextMock.Setup(d => d.Entry(testObject)).Returns(dbEntityEntryMock.Object);
with the 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 I can mock such cases, but always the end result was the same. Any ideas?
英文:
I have written a generic repository (BaseRepository<TEntity>), 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?
答案1
得分: 2
这是如何模拟dbcontext以防止测试错误的方法,但我认为在设计这个测试时可能有不同的方法要考虑(请查看下面的“但是”部分):
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
但(<- 大 "但"):
- 您不应该这样做:
https://github.com/dotnet/efcore/issues/27110 - 在使用EF时如何编写测试,请阅读:https://learn.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking
- 如果我要为您的通用存储库编写测试,我会这样做:https://gist.github.com/ctrl-alt-d/8bc8d1f9e41a8ea98309397c46933fe4
英文:
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
But (<- big "but"):
- You shouldn't do it:
https://github.com/dotnet/efcore/issues/27110 - Read about how to write tests when you use EF https://learn.microsoft.com/en-us/ef/ef6/fundamentals/testing/mocking
- If I were to write a test for your generic repository, this is how I would approach it: https://gist.github.com/ctrl-alt-d/8bc8d1f9e41a8ea98309397c46933fe4
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论