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

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

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 = &quot;Test-Customer&quot;
        };
        
        var dbContextMock = new Mock&lt;DbContext&gt;();
        var dbEntityEntryMock = new Mock&lt;EntityEntry&lt;Customer&gt;&gt;();
        
        dbContextMock.Setup(d =&gt; d.Entry(testObject)).Returns(dbEntityEntryMock.Object);
        dbEntityEntryMock.Setup(e =&gt; e.State).Returns(EntityState.Unchanged);
        
        // Act
        var repository = new CustomerRepository(dbContextMock.Object);
        repository.Delete(testObject);

        //Assert
        dbContextMock.Verify(x =&gt; x.Remove(It.Is&lt;Customer&gt;(y =&gt; y == testObject)), Times.AtMost(1));
    }

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以防止测试错误的方法,但我认为在设计这个测试时可能有不同的方法要考虑(请查看下面的“但是”部分):

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&lt;EntityEntry&lt;Customer&gt;&gt;(internalEntityEntry);
    dbEntityEntryMock.Setup(e =&gt; e.State).Returns(EntityState.Unchanged);

    var dbContextMock = new Mock&lt;DbContext&gt;();
    dbContextMock.Setup(d =&gt; d.Entry(testObject)).Returns(dbEntityEntryMock.Object);

    // Act
    var repository = new CustomerRepository(dbContextMock.Object);
    repository.Delete(testObject);

    //Assert
    dbContextMock.Verify(x =&gt; x.Remove(It.Is&lt;Customer&gt;(y =&gt; y == testObject)), Times.AtMost(1));
}

private static InternalEntityEntry GetInternalEntityEntry(Customer testObject)
{
    return new InternalEntityEntry(
        new Mock&lt;IStateManager&gt;().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&lt;EntityEntry&lt;Customer&gt;&gt;(internalEntityEntry);
    dbEntityEntryMock.Setup(e =&gt; e.State).Returns(EntityState.Unchanged);

    var dbContextMock = new Mock&lt;DbContext&gt;();
    dbContextMock.Setup(d =&gt; d.Entry(testObject)).Returns(dbEntityEntryMock.Object);

    // Act
    var repository = new CustomerRepository(dbContextMock.Object);
    repository.Delete(testObject);

    //Assert
    dbContextMock.Verify(x =&gt; x.Remove(It.Is&lt;Customer&gt;(y =&gt; y == testObject)), Times.AtMost(1));
}

private static InternalEntityEntry GetInternalEntityEntry(Customer testObject)
{
    return new InternalEntityEntry(
        new Mock&lt;IStateManager&gt;().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"):

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:

确定