是否有一种方法可以通用地模拟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是否提供了找到主键的任何方法,或者它只在连接到数据库后才动态地知道?
经过一段时间的思考,我认为我已经找到了目前可用的"最佳"解决方案。我只有一系列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);