是否有一种方法可以通用地模拟DbSet.查找Moq方法

本文关键字:方法 模拟 Moq 查找 DbSet 一种 是否 | 更新日期: 2023-09-27 18:11:33

我目前正在使用扩展方法一般模拟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方法,该方法基于表的主键进行搜索。我可以在每个表的特定级别上这样做因为我可以检查数据库,获取PK,然后模拟该字段的Find方法。但是我不能使用泛型方法。

我想我也可以去添加到EF自动生成的部分类中,以标记哪个字段是带有属性或其他东西的PK。但是我们有超过100个表,如果你依靠人工维护,这使得代码更难管理。

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

是否有一种方法可以通用地模拟DbSet.查找Moq方法

经过一段时间的思考,我认为我已经找到了目前可用的"最佳"解决方案。我只有一系列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);
        }
        ...       

据我所知,这个问题没有"最佳实践"的答案,但这里是我如何处理它的。我在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作为主键,因为这似乎是您的工作方式,但如果您需要更灵活的组合键等,该原则应该很容易适应。

我以以下类结束:

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 );
}

我现在使用的是实体框架核心2,这个解决方案对我来说很好。

首先,我将通过使用带有后缀"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,您可以使用以下代码支持查找

        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。查找你可以看到下面:

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获得)。


Edit:顺便说一下,如果您真的不想在每次调用上述函数时都指定键,并且您知道大多数键都是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);