尝试对基于.NET EF Core的通用存储库进行单元测试在处理DbContext.Entry时失败

huangapple go评论95阅读模式
英文:

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:

  1. public virtual void Delete(TEntity entity)
  2. {
  3. if (dbContext.Entry(entity).State == EntityState.Detached)
  4. {
  5. dbContext.Attach(entity);
  6. }
  7. dBContext.Remove(entity);
  8. }

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:

  1. [Fact]
  2. public void Delete_SomeEntityToRepository_CallsTheAddMethod_To_DbContext()
  3. {
  4. // Arrange
  5. var testObject = new Customer()
  6. {
  7. Name = "Test-Customer"
  8. };
  9. var dbContextMock = new Mock<DbContext>();
  10. var dbEntityEntryMock = new Mock<EntityEntry<Customer>>();
  11. dbContextMock.Setup(d => d.Entry(testObject)).Returns(dbEntityEntryMock.Object);
  12. dbEntityEntryMock.Setup(e => e.State).Returns(EntityState.Unchanged);
  13. // Act
  14. var repository = new CustomerRepository(dbContextMock.Object);
  15. repository.Delete(testObject);
  16. // Assert
  17. dbContextMock.Verify(x => x.Remove(It.Is<Customer>(y => y == testObject)), Times.AtMost(1));
  18. }

However, this code crashes at the line:

  1. 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:

  1. public virtual void Delete(TEntity entity)
  2. {
  3. if (dbContext.Entry(entity).State == EntityState.Detached)
  4. {
  5. dbContext.Attach(entity);
  6. }
  7. dBContext.Remove(entity);
  8. }

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:

  1. [Fact]
  2. public void Delete_SomeEntityToRepository_CallsTheAddMethod_To_DbContext()
  3. {
  4. // Arrange
  5. var testObject = new Customer()
  6. {
  7. Name = &quot;Test-Customer&quot;
  8. };
  9. var dbContextMock = new Mock&lt;DbContext&gt;();
  10. var dbEntityEntryMock = new Mock&lt;EntityEntry&lt;Customer&gt;&gt;();
  11. dbContextMock.Setup(d =&gt; d.Entry(testObject)).Returns(dbEntityEntryMock.Object);
  12. dbEntityEntryMock.Setup(e =&gt; e.State).Returns(EntityState.Unchanged);
  13. // Act
  14. var repository = new CustomerRepository(dbContextMock.Object);
  15. repository.Delete(testObject);
  16. //Assert
  17. dbContextMock.Verify(x =&gt; x.Remove(It.Is&lt;Customer&gt;(y =&gt; y == testObject)), Times.AtMost(1));
  18. }

However this code crashes, and actually at line:
dbContextMock.Setup(d =&gt; 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以防止测试错误的方法,但我认为在设计这个测试时可能有不同的方法要考虑(请查看下面的“但是”部分):

  1. using Microsoft.EntityFrameworkCore;
  2. using Microsoft.EntityFrameworkCore.ChangeTracking;
  3. using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
  4. using Microsoft.EntityFrameworkCore.Metadata;
  5. using Moq;
  6. // ...
  1. [Fact]
  2. public void Delete_SomeEntityToRepository_CallsTheAddMethod_To_DbContext()
  3. {
  4. // Arrange
  5. var testObject = new Customer();
  6. var internalEntityEntry = GetInternalEntityEntry(testObject);
  7. var dbEntityEntryMock = new Mock&lt;EntityEntry&lt;Customer&gt;&gt;(internalEntityEntry);
  8. dbEntityEntryMock.Setup(e =&gt; e.State).Returns(EntityState.Unchanged);
  9. var dbContextMock = new Mock&lt;DbContext&gt;();
  10. dbContextMock.Setup(d =&gt; d.Entry(testObject)).Returns(dbEntityEntryMock.Object);
  11. // Act
  12. var repository = new CustomerRepository(dbContextMock.Object);
  13. repository.Delete(testObject);
  14. //Assert
  15. dbContextMock.Verify(x =&gt; x.Remove(It.Is&lt;Customer&gt;(y =&gt; y == testObject)), Times.AtMost(1));
  16. }
  17. private static InternalEntityEntry GetInternalEntityEntry(Customer testObject)
  18. {
  19. return new InternalEntityEntry(
  20. new Mock&lt;IStateManager&gt;().Object,
  21. new RuntimeEntityType(
  22. name: nameof(Customer), type: typeof(Customer), sharedClrType: false, model: new(),
  23. baseType: null, discriminatorProperty: null, changeTrackingStrategy: ChangeTrackingStrategy.Snapshot,
  24. indexerPropertyInfo: null, propertyBag: false,
  25. discriminatorValue: null),
  26. testObject);
  27. }

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):

  1. using Microsoft.EntityFrameworkCore;
  2. using Microsoft.EntityFrameworkCore.ChangeTracking;
  3. using Microsoft.EntityFrameworkCore.ChangeTracking.Internal;
  4. using Microsoft.EntityFrameworkCore.Metadata;
  5. using Moq;
  6. // ...
  1. [Fact]
  2. public void Delete_SomeEntityToRepository_CallsTheAddMethod_To_DbContext()
  3. {
  4. // Arrange
  5. var testObject = new Customer();
  6. var internalEntityEntry = GetInternalEntityEntry(testObject);
  7. var dbEntityEntryMock = new Mock&lt;EntityEntry&lt;Customer&gt;&gt;(internalEntityEntry);
  8. dbEntityEntryMock.Setup(e =&gt; e.State).Returns(EntityState.Unchanged);
  9. var dbContextMock = new Mock&lt;DbContext&gt;();
  10. dbContextMock.Setup(d =&gt; d.Entry(testObject)).Returns(dbEntityEntryMock.Object);
  11. // Act
  12. var repository = new CustomerRepository(dbContextMock.Object);
  13. repository.Delete(testObject);
  14. //Assert
  15. dbContextMock.Verify(x =&gt; x.Remove(It.Is&lt;Customer&gt;(y =&gt; y == testObject)), Times.AtMost(1));
  16. }
  17. private static InternalEntityEntry GetInternalEntityEntry(Customer testObject)
  18. {
  19. return new InternalEntityEntry(
  20. new Mock&lt;IStateManager&gt;().Object,
  21. new RuntimeEntityType(
  22. name: nameof(Customer), type: typeof(Customer), sharedClrType: false, model: new(),
  23. baseType: null, discriminatorProperty: null, changeTrackingStrategy: ChangeTrackingStrategy.Snapshot,
  24. indexerPropertyInfo: null, propertyBag: false,
  25. discriminatorValue: null),
  26. testObject);
  27. }

Full text here: https://gist.github.com/ctrl-alt-d/3d10384a06fa1e0c515e1f182fb83bb0

But (<- big "but"):

huangapple
  • 本文由 发表于 2023年6月11日 22:04:07
  • 转载请务必保留本文链接:https://go.coder-hub.com/76450849.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定