使用 C#/Moq Framework 模拟带有输入参数的异步 DbContext 函数
Mocking async DbContext functions with input parameter with C#/Moq Framework
我制作了 DbContext 的 Mock 并填充了测试数据。 DbSet 是一个受保护的 class 所以结果不能被模拟,所以我找到了一个扩展方法。
public static class DbSetMocking
{
private static Mock<DbSet<T>> CreateMockSet<T>(IQueryable<T> data)
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());
return mockSet;
}
public static IReturnsResult<TContext> ReturnsDbSet<TEntity, TContext>(this IReturns<TContext, DbSet<TEntity>> setup, TEntity[] entities)
where TEntity : class
where TContext : DbContext
{
var mockSet = CreateMockSet(entities.AsQueryable());
return setup.Returns(mockSet.Object);
}
public static IReturnsResult<TContext> ReturnsDbSet<TEntity, TContext>(this IReturns<TContext, DbSet<TEntity>> setup, IQueryable<TEntity> entities)
where TEntity : class
where TContext : DbContext
{
var mockSet = CreateMockSet(entities);
return setup.Returns(mockSet.Object);
}
public static IReturnsResult<TContext> ReturnsDbSet<TEntity, TContext>(this IReturns<TContext, DbSet<TEntity>> setup, IEnumerable<TEntity> entities)
where TEntity : class
where TContext : DbContext
{
var mockSet = CreateMockSet(entities.AsQueryable());
return setup.Returns(mockSet.Object);
}
}
创建 DbContext 的模拟:
var db = new Mock<DbContext>();
//Populate
this.db.Setup(x => x.MyObjects).ReturnsDbSet(
new List<MyObject>
{
new MyObject{Id=1, Description="Test"},
}
);
其次,我正在尝试扩展 Mocks 以包含 Find(id) 和 FindAsync(id) 方法。这些方法被放置在 DbSetMocking class 中。查找方法工作正常:
mockSet.Setup(m => m.Find(It.IsAny<object[]>()))
.Returns<object[]>(id => StaticMethodtoFindStuff<T>(queryableData, id));
但是,我无法使用 FindAsync 方法。这是我到目前为止尝试过的:
mockSet.Setup(m => m.FindAsync(It.IsAny<object[]>()))
.Returns(Task.FromResult(StaticMethodtoFindStuff<T>(queryableData, 1)));
这个有效,但是我无法访问该函数设置的参数。试过这个,编译工作正常,但执行失败并显示错误消息:
'System.Object[]' 类型的对象无法转换为 'System.Threading.Tasks.Task`1[System.Object[]]' 类型。
mockSet.Setup(m => m.FindAsync(It.IsAny<object[]>()))
.Returns<Task<object[]>>(d =>
{
return Task.FromResult(StaticMethodtoFindStuff<T>(queryableData, d));
});
有人知道我如何实现这个功能吗?
尝试更改此行:
mockSet.Setup(m => m.FindAsync(It.IsAny<object[]>()))
.Returns<Task<object[]>>(d =>
{
return Task.FromResult(StaticMethodtoFindStuff<T>(queryableData, d));
});
至:
mockSet.Setup(m => m.FindAsync(It.IsAny<object[]>()))
.Returns<object[]>(d =>
{
return Task.FromResult(StaticMethodtoFindStuff<T>(queryableData, d));
});
这里的问题是,.Returns<T>
的通用参数 T
不是结果的类型,而是 第一个参数的类型,在函数内部传递 [= .Setup
方法中的 32=] -- 对于你的 object[].
查看 Moq 源代码以获取更多信息:
引用来源,查看 T1 和 T2 参数注释:
/// <summary>
/// Specifies a function that will calculate the value to return from the method,
/// retrieving the arguments for the invocation.
/// </summary>
/// <typeparam name="T1">The type of the first argument of the invoked method.</typeparam>
/// <typeparam name="T2">The type of the second argument of the invoked method.</typeparam>
/// <param name="valueFunction">The function that will calculate the return value.</param>
/// <return>Returns a calculated value which is evaluated lazily at the time of the invocation.</return>
/// <example>
/// <para>
/// The return value is calculated from the value of the actual method invocation arguments.
/// Notice how the arguments are retrieved by simply declaring them as part of the lambda
/// expression:
/// </para>
/// <code>
/// mock.Setup(x => x.Execute(
/// It.IsAny<int>(),
/// It.IsAny<int>()))
/// .Returns((int arg1, int arg2) => arg1 + arg2); //I fixed that line, it's different in the documentation and is incorrect
/// </code>
/// </example>
IReturnsResult<TMock> Returns<T1, T2>(Func<T1, T2, TResult> valueFunction);
你还可以看到,它定义了 Returns 的多个重载,具有可变数量的泛型参数,这样你就可以模拟一个最多有 16 个参数的方法。
终于明白了。原来我在那里遗漏了一个 'async' 关键字。代码是:
mockSet.Setup(m => m.FindAsync(It.IsAny<object[]>()))
.Returns<object[]>(async (d) =>
{
return await Task.FromResult(StaticMethodtoFindStuff<T>(queryableData, d));
});
我制作了 DbContext 的 Mock 并填充了测试数据。 DbSet 是一个受保护的 class 所以结果不能被模拟,所以我找到了一个扩展方法。
public static class DbSetMocking
{
private static Mock<DbSet<T>> CreateMockSet<T>(IQueryable<T> data)
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());
return mockSet;
}
public static IReturnsResult<TContext> ReturnsDbSet<TEntity, TContext>(this IReturns<TContext, DbSet<TEntity>> setup, TEntity[] entities)
where TEntity : class
where TContext : DbContext
{
var mockSet = CreateMockSet(entities.AsQueryable());
return setup.Returns(mockSet.Object);
}
public static IReturnsResult<TContext> ReturnsDbSet<TEntity, TContext>(this IReturns<TContext, DbSet<TEntity>> setup, IQueryable<TEntity> entities)
where TEntity : class
where TContext : DbContext
{
var mockSet = CreateMockSet(entities);
return setup.Returns(mockSet.Object);
}
public static IReturnsResult<TContext> ReturnsDbSet<TEntity, TContext>(this IReturns<TContext, DbSet<TEntity>> setup, IEnumerable<TEntity> entities)
where TEntity : class
where TContext : DbContext
{
var mockSet = CreateMockSet(entities.AsQueryable());
return setup.Returns(mockSet.Object);
}
}
创建 DbContext 的模拟:
var db = new Mock<DbContext>();
//Populate
this.db.Setup(x => x.MyObjects).ReturnsDbSet(
new List<MyObject>
{
new MyObject{Id=1, Description="Test"},
}
);
其次,我正在尝试扩展 Mocks 以包含 Find(id) 和 FindAsync(id) 方法。这些方法被放置在 DbSetMocking class 中。查找方法工作正常:
mockSet.Setup(m => m.Find(It.IsAny<object[]>()))
.Returns<object[]>(id => StaticMethodtoFindStuff<T>(queryableData, id));
但是,我无法使用 FindAsync 方法。这是我到目前为止尝试过的:
mockSet.Setup(m => m.FindAsync(It.IsAny<object[]>()))
.Returns(Task.FromResult(StaticMethodtoFindStuff<T>(queryableData, 1)));
这个有效,但是我无法访问该函数设置的参数。试过这个,编译工作正常,但执行失败并显示错误消息:
'System.Object[]' 类型的对象无法转换为 'System.Threading.Tasks.Task`1[System.Object[]]' 类型。
mockSet.Setup(m => m.FindAsync(It.IsAny<object[]>()))
.Returns<Task<object[]>>(d =>
{
return Task.FromResult(StaticMethodtoFindStuff<T>(queryableData, d));
});
有人知道我如何实现这个功能吗?
尝试更改此行:
mockSet.Setup(m => m.FindAsync(It.IsAny<object[]>()))
.Returns<Task<object[]>>(d =>
{
return Task.FromResult(StaticMethodtoFindStuff<T>(queryableData, d));
});
至:
mockSet.Setup(m => m.FindAsync(It.IsAny<object[]>()))
.Returns<object[]>(d =>
{
return Task.FromResult(StaticMethodtoFindStuff<T>(queryableData, d));
});
这里的问题是,.Returns<T>
的通用参数 T
不是结果的类型,而是 第一个参数的类型,在函数内部传递 [= .Setup
方法中的 32=] -- 对于你的 object[].
查看 Moq 源代码以获取更多信息:
引用来源,查看 T1 和 T2 参数注释:
/// <summary>
/// Specifies a function that will calculate the value to return from the method,
/// retrieving the arguments for the invocation.
/// </summary>
/// <typeparam name="T1">The type of the first argument of the invoked method.</typeparam>
/// <typeparam name="T2">The type of the second argument of the invoked method.</typeparam>
/// <param name="valueFunction">The function that will calculate the return value.</param>
/// <return>Returns a calculated value which is evaluated lazily at the time of the invocation.</return>
/// <example>
/// <para>
/// The return value is calculated from the value of the actual method invocation arguments.
/// Notice how the arguments are retrieved by simply declaring them as part of the lambda
/// expression:
/// </para>
/// <code>
/// mock.Setup(x => x.Execute(
/// It.IsAny<int>(),
/// It.IsAny<int>()))
/// .Returns((int arg1, int arg2) => arg1 + arg2); //I fixed that line, it's different in the documentation and is incorrect
/// </code>
/// </example>
IReturnsResult<TMock> Returns<T1, T2>(Func<T1, T2, TResult> valueFunction);
你还可以看到,它定义了 Returns 的多个重载,具有可变数量的泛型参数,这样你就可以模拟一个最多有 16 个参数的方法。
终于明白了。原来我在那里遗漏了一个 'async' 关键字。代码是:
mockSet.Setup(m => m.FindAsync(It.IsAny<object[]>()))
.Returns<object[]>(async (d) =>
{
return await Task.FromResult(StaticMethodtoFindStuff<T>(queryableData, d));
});