我正在尝试为处理异常并重新加载 Entity Framework Core 中的实体的方法编写单元测试。该方法包含以下代码:
catch (DbUpdateConcurrencyException)
{
await context.Entry(myEntityInstance).ReloadAsync();
}
catch { return null; };
我已成功模拟
SaveChangesAsync
并抛出所需的异常。但是,我在尝试模拟 Entry
方法时遇到了问题:
contextMock.Setup(c => c.Entry(It.IsAny<MyEntity>()).ReloadAsync(default))
这会导致 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.
问题是
EntityEntry<TEntity>
不是模拟友好的,因为它只有一个构造函数:
[EntityFrameworkInternal]
public EntityEntry(InternalEntityEntry internalEntry)
InternalEntityEntry
是一个内部 EF Core 类,它依赖于其他内部类的实例。有一个 GitHub 问题 Trouble Mocking EntityEntry 讨论了这个问题,但 EF 团队似乎不打算进行任何更改。
如何在
EF Core中模拟
EntityEntry
来有效测试此方法?
彻底检查
EF Core
源代码后,我创建了以下方法来模拟EntityEntry
。该方法严重依赖内部API,但避免了反射。如果未来发生重大变化,可以轻松识别并纠正测试。
/// <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;
}
使用此方法的结果,您可以轻松地为
ReloadAsync
创建所需的模拟:
entityEntryMock.Setup(e => e.ReloadAsync(default)).Returns(Task.CompletedTask);