Search code examples
c#entity-framework-corerepository-patternunit-of-work

Trying to Unit Test Generic Repository based on .NET EF Core fails when dealing with DbContext.Entry


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?


Solution

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