有没有一种方法可以用 Moq 来模拟 DbSet.Find 方法?

Is there a way to generically mock the DbSet.Find method with Moq?

我目前正在使用扩展方法将 DbSets 模拟为列表:

    public static DbSet<T> AsDbSet<T>(this List<T> sourceList) where T : class
    {
        var queryable = sourceList.AsQueryable();
        var mockDbSet = new Mock<DbSet<T>>();
        mockDbSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(queryable.Provider);
        mockDbSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(queryable.Expression);
        mockDbSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(queryable.ElementType);
        mockDbSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(queryable.GetEnumerator());
        mockDbSet.Setup(x => x.Add(It.IsAny<T>())).Callback<T>(sourceList.Add);
        mockDbSet.Setup(x => x.Remove(It.IsAny<T>())).Returns<T>(x => { if (sourceList.Remove(x)) return x; else return null; } );

        return mockDbSet.Object;
    }

但是,我想不出一种方法来模拟 Find 方法,该方法根据 table 的主键进行搜索。我可以在每个 table 的特定级别执行此操作,因为我可以检查数据库、获取 PK,然后只需模拟该字段的 Find 方法。但是我不能使用通用方法。

我想我也可以添加到 EF 自动生成的部分 类 中,以标记哪个字段是具有属性或其他内容的 PK。但是我们有 100 多个 tables,如果您依靠人们手动维护它,它会使代码更难管理。

EF6 是否提供任何查找主键的方法,或者它是否仅在连接到数据库后才动态知道?

经过一段时间的思考,我想我已经找到了当前可用的 "best" 解决方案。我只有一系列直接检查扩展方法中类型的 if 语句。然后我转换为我需要设置查找行为的类型,并在完成后将其转换回通用类型。它只是伪泛型,但我想不出更好的东西。

        if (typeof(T) == typeof(MyFirstSet))
        {
            mockDbSet.Setup(x => x.Find(It.IsAny<object[]>())).Returns<object[]>(x => (sourceList as List<MyFirstSet>).FirstOrDefault(y => y.MyFirstSetKey == (Guid)x[0]) as T);
        }
        else if (typeof(T) == typeof(MySecondSet))
        {
            mockDbSet.Setup(x => x.Find(It.IsAny<object[]>())).Returns<object[]>(x => (sourceList as List<MySecondSet>).FirstOrDefault(y => y.MySecondSetKey == (Guid)x[0]) as T);
        }
        ...       

据我所知,这个问题没有 'best practice' 答案,但我是这样处理的。我在 AsDbSet 方法中添加了一个可选参数来标识主键,然后可以轻松模拟 Find 方法。

public static DbSet<T> AsDbSet<T>(this List<T> sourceList, Func<T, object> primaryKey = null) where T : class
{
    //all your other stuff still goes here

    if (primaryKey != null)
    {
        mockSet.Setup(set => set.Find(It.IsAny<object[]>())).Returns((object[] input) => sourceList.SingleOrDefault(x => (Guid)primaryKey(x) == (Guid)input.First()));
    }

    ...
}

我写这篇文章的前提是将单个 guid 用作主键,因为这似乎是您的工作方式,但如果您需要更灵活的复合键,原则应该很容易适应等

我的结尾如下 class:

public static class DbSetMocking
{
    #region methods

    public static IReturnsResult<TContext> ReturnsDbSet<TEntity, TContext>( this IReturns<TContext, DbSet<TEntity>> setup, ICollection<TEntity> entities, Func<object[], TEntity> find = null )
        where TEntity : class where TContext : DbContext
    {
        return setup.Returns( CreateMockSet( entities, find ).Object );
    }

    private static Mock<DbSet<T>> CreateMockSet<T>( ICollection<T> data, Func<object[], T> find )
        where T : class
    {
        var queryableData = data.AsQueryable();
        var mockSet = new Mock<DbSet<T>>();
        mockSet.As<IQueryable<T>>().Setup( m => m.Provider ).Returns( queryableData.Provider );
        mockSet.As<IQueryable<T>>().Setup( m => m.Expression ).Returns( queryableData.Expression );
        mockSet.As<IQueryable<T>>().Setup( m => m.ElementType ).Returns( queryableData.ElementType );
        mockSet.As<IQueryable<T>>().Setup( m => m.GetEnumerator() ).Returns( queryableData.GetEnumerator() );

        mockSet.SetupData( data, find );

        return mockSet;
    }

    #endregion
}

可以使用的:

private static MyRepository SetupRepository( ICollection<Type1> type1s, ICollection<Type2> type2s )
{
    var mockContext = new Mock<MyDbContext>();

    mockContext.Setup( x => x.Type1s ).ReturnsDbSet( type1s, o => type1s.SingleOrDefault( s => s.Secret == ( Guid ) o[ 0 ] ) );
    mockContext.Setup( x => x.Type2s ).ReturnsDbSet( type2s, o => type2s.SingleOrDefault( s => s.Id == ( int ) o[ 0 ] ) );

    return new MyRepository( mockContext.Object );
}

我现在正在使用 Entity Framework Core 2,这个解决方案对我来说效果很好。

首先,我会使用class名称加上后缀“Id”来查找主键。 (如果您遵循其他约定,则必须更改它以满足您的需要。)

        //Find primary key. Here the PK must follow the convention "Class Name" + "Id" 
        Type type = typeof(T);
        string colName = type.Name + "Id";
        var pk = type.GetProperty(colName);
        if (pk == null)
        {
            colName = type.Name + "ID";
            pk = type.GetProperty(colName);
        }

既然知道了Pk,就可以用下面的代码支持Find了

        dbSet.Setup(x => x.Find(It.IsAny<object[]>())).Returns((object[] id) =>
        {
            var param = Expression.Parameter(type, "t");
            var col = Expression.Property(param, colName);
            var body = Expression.Equal(col, Expression.Constant(id[0]));
            var lambda = Expression.Lambda<Func<T, bool>>(body, param);
            return queryable.FirstOrDefault(lambda);
        });

因此,通用模拟支持DbSet.Find的完整代码如下:

public static DbSet<T> GetQueryableMockDbSet<T>(List<T> sourceList) where T : class
    {
        var queryable = sourceList.AsQueryable();
        var dbSet = new Mock<DbSet<T>>();

        dbSet.As<IQueryable<T>>().Setup(m => m.Provider).Returns(queryable.Provider);
        dbSet.As<IQueryable<T>>().Setup(m => m.Expression).Returns(queryable.Expression);
        dbSet.As<IQueryable<T>>().Setup(m => m.ElementType).Returns(queryable.ElementType);
        dbSet.As<IQueryable<T>>().Setup(m => m.GetEnumerator()).Returns(() => queryable.GetEnumerator());
        dbSet.Setup(d => d.Add(It.IsAny<T>())).Callback<T>((s) => sourceList.Add(s));

        //Find primary key. Here the PK must follow the convention "Class Name" + "Id" 
        Type type = typeof(T);
        string colName = type.Name + "Id";
        var pk = type.GetProperty(colName);
        if (pk == null)
        {
            colName = type.Name + "ID";
            pk = type.GetProperty(colName);
        }

        dbSet.Setup(x => x.Find(It.IsAny<object[]>())).Returns((object[] id) =>
        {
            var param = Expression.Parameter(type, "t");
            var col = Expression.Property(param, colName);
            var body = Expression.Equal(col, Expression.Constant(id[0]));
            var lambda = Expression.Lambda<Func<T, bool>>(body, param);
            return queryable.FirstOrDefault(lambda);
        });

        return dbSet.Object;

    } //GetQueryableMockDbSet

我的解决方案是添加一个参数来指定实体的键:

public static Mock<DbSet<TEntity>> Setup<TContext, TEntity, TKey>(this Mock<TContext> mockContext,
    Expression<Func<TContext, DbSet<TEntity>>> expression, List<TEntity> sourceList,
    Func<TEntity, TKey> id)
    where TEntity : class
    where TContext : DbContext
{
    IQueryable<TEntity> data = sourceList.AsQueryable();
    Mock<DbSet<TEntity>> mock = data.BuildMockDbSet();

    // make adding to and searching the list work
    mock.Setup(d => d.Add(It.IsAny<TEntity>())).Callback(add(sourceList));
    mock.Setup(d => d.Find(It.IsAny<object[]>())).Returns<object[]>(s => find(sourceList, id, s));

    // make context.Add() and Find() work
    mockContext.Setup(x => x.Add(It.IsAny<TEntity>())).Callback(add(sourceList));
    mockContext.Setup(x => x.Find<TEntity>(It.IsAny<object[]>()))
        .Returns<object[]>(s => find(sourceList, id, s));
    mockContext.Setup(x => x.Find(typeof(TEntity), It.IsAny<object[]>()))
        .Returns<Type, object[]>((t, s) => find(sourceList, id, s));
    mockContext.Setup(expression).Returns(mock.Object);
    return mock;
}

private static Action<TEntity> add<TEntity>(IList<TEntity> sourceList)
    where TEntity : class
{
    return s => sourceList.Add(s);
}

private static TEntity find<TEntity, TKey>(IList<TEntity> sourceList, Func<TEntity, TKey> id, object[] s) where TEntity : class
{
    return sourceList.SingleOrDefault(e => id(e).Equals(s[0]));
}

您可以将其用作

mockContext.Setup(m => m.Users, users, x => x.UsedId);

BuildMockDbSet 来自 MockQueryable 库(可从 NuGet 获得)。


编辑: 顺便说一句,如果你真的不想每次调用上面的函数时都指定键,而且你知道你的键大多是int类型,您可以创建另一个重载,例如:

public static Mock<DbSet<TEntity>> Setup<TContext, TEntity>(this Mock<TContext> mockContext,
    Expression<Func<TContext, DbSet<TEntity>>> expression, List<TEntity> sourceList)
    where TEntity : class
    where TContext : DbContext
{
    return Setup(mockContext, expression, sourceList, x => x.GetKey<int>());
}

其中 GetKey 由扩展方法实现:

public static object? GetKey(this object entity)
{
    PropertyInfo keyInfo = entity.GetType().GetProperties().Where(prop => Attribute.IsDefined(prop, typeof(KeyAttribute))).SingleOrDefault();

    if (keyInfo == null)
        return null;

    return keyInfo.GetValue(entity);
}

public static TKey GetKey<TKey>(this object entity)
{
    return (TKey)GetKey(entity);
}

所以现在您可以简单地称呼它为

var mockUsers = mockContext.Setup(m => m.Users, users);