unit-testingentity-framework-coremockingmoqef-core-8.0

How to Mock EntityEntry in EF Core?


I am trying to write a unit test for a method that handles exceptions and reloads an entity in Entity Framework Core. The method contains the following code:

    catch (DbUpdateConcurrencyException)
    {
        await context.Entry(myEntityInstance).ReloadAsync();
    }
    catch { return null; };

I have successfully mocked SaveChangesAsync to throw the desired exception. However, I am encountering issues when attempting to mock the Entry method:

contextMock.Setup(c => c.Entry(It.IsAny<MyEntity>()).ReloadAsync(default))

This results in an ArgumentException:

Can not instantiate proxy of class: Microsoft.EntityFrameworkCore.ChangeTracking.EntityEntry ...
Could not find a parameterless constructor. (Parameter 'constructorArguments') ---> System.MissingMethodException: Constructor on type 'Castle.Proxies.EntityEntry`1Proxy' not found.

The problem is that EntityEntry<TEntity> is not mock-friendly because it has only one constructor:

[EntityFrameworkInternal]
public EntityEntry(InternalEntityEntry internalEntry)

InternalEntityEntry is an internal EF Core class that depends on instances of other internal classes. There is a GitHub issue Trouble Mocking EntityEntry discussing this problem, but it seems that the EF team is not planning to make any changes.

How can I mock EntityEntry in EF Core to test this method effectively?


Solution

  • After thoroughly examining the EF Core source code, I created the following method to mock EntityEntry. This method relies heavily on internal APIs, but it avoids reflection. If a future breaking change occurs, it can be easily identified and the test can be corrected.

        /// <summary>
        /// Mocks an <see cref="EntityEntry"/>  for a given entity in the specified DbContext.
        /// </summary>
        /// <typeparam name="TContext">The type of the DbContext.</typeparam>
        /// <typeparam name="TEntity">The type of the entity.</typeparam>
        /// <param name="dbContextMock">The mock of the DbContext.</param>
        /// <param name="entity">The entity to mock the entry for.</param>
        /// <returns>A <see cref="Mock"> of the <see cref="EntityEntry"/> for the specified entity.</returns>
        /// <remarks>The method heavilly depends on "Internal EF Core API" and so can fail after EF upgrade. Use with extreme care.</remarks>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "EF1001:Internal EF Core API usage.", Justification = "<Pending>")]
        public static Mock<EntityEntry<TEntity>> MockEntityEntry<TContext, TEntity>(Mock<TContext> dbContextMock, TEntity entity)
        where TContext : DbContext
        where TEntity : class
        {
            var stateManagerMock = new Mock<IStateManager>();
            stateManagerMock
                .Setup(x => x.CreateEntityFinder(It.IsAny<IEntityType>()))
                .Returns(new Mock<IEntityFinder>().Object);
            stateManagerMock
                .Setup(x => x.ValueGenerationManager)
                .Returns(new Mock<IValueGenerationManager>().Object);
            stateManagerMock
                .Setup(x => x.InternalEntityEntryNotifier)
                .Returns(new Mock<IInternalEntityEntryNotifier>().Object);
    
            var entityTypeMock = new Mock<IRuntimeEntityType>();
            var keyMock = new Mock<IKey>();
            keyMock
                .Setup(x => x.Properties)
                .Returns([]);
            entityTypeMock
                .Setup(x => x.FindPrimaryKey())
                .Returns(keyMock.Object);
            entityTypeMock
                .Setup(e => e.EmptyShadowValuesFactory)
                .Returns(() => new Mock<ISnapshot>().Object);
    
            var internalEntityEntry = new InternalEntityEntry(stateManagerMock.Object, entityTypeMock.Object, entity);
    
            var entityEntryMock = new Mock<EntityEntry<TEntity>>(internalEntityEntry);
            dbContextMock
                .Setup(c => c.Entry(It.IsAny<TEntity>()))
                .Returns(() => entityEntryMock.Object);
    
            return entityEntryMock;
        }
    

    Using the result of this method, you can easily create the desired mock for ReloadAsync:

     entityEntryMock.Setup(e => e.ReloadAsync(default)).Returns(Task.CompletedTask);