如何模拟 Entity Framework 6 的异步方法?

8

我是一名新手,想要模拟我的基础仓库,这个仓库依赖于Entity Framework 6 DbContext,但是我失败了。我在谷歌上搜索了很多,但是没有得到任何充分的结果。最后,我在使用异步查询进行测试中找到了一个例子,并尝试跟随,但对我来说并不起作用。

以下是我的代码:

DbContext :

public class TimeSketchContext : DbContext
{
    public virtual DbSet<EmployeeSkill> EmployeeSkill { get; set; }
}

基础仓库:

public class BaseRepository<T> : IRepositoryBase<T> where T : class, IEntity, new()
{
    protected readonly DbContext InnerDbContext;
    protected DbSet<T> InnerDbSet;

    public BaseRepository(DbContext innerDbContext)
    {
        InnerDbContext = innerDbContext;
        InnerDbSet = InnerDbContext.Set<T>();
    }

    public virtual Task<T> FindAsync(long id)
    {
        return InnerDbSet.FirstOrDefaultAsync(x=>x.Id == id);
    }

}

Test :

    [Fact]
    public async Task DbTest()
    {
        var dummyData = GetEmployeeSkills();
        var mockSet = new Mock<DbSet<EmployeeSkill>>();

        mockSet.As<IDbAsyncEnumerable<EmployeeSkill>>()
            .Setup(x => x.GetAsyncEnumerator())
            .Returns(new TestDbAsyncEnumerator<EmployeeSkill>(dummyData.GetEnumerator()));

        mockSet.As<IQueryable<EmployeeSkill>>()
            .Setup(x => x.Provider)
            .Returns(new TestDbAsyncQueryProvider<EmployeeSkill>(dummyData.Provider));

        mockSet.As<IQueryable<EmployeeSkill>>().Setup(m => m.Expression).Returns(dummyData.Expression);
        mockSet.As<IQueryable<EmployeeSkill>>().Setup(m => m.ElementType).Returns(dummyData.ElementType);
        mockSet.As<IQueryable<EmployeeSkill>>().Setup(m => m.GetEnumerator()).Returns(dummyData.GetEnumerator());

        var mockContext = new Mock<TimeSketchContext>();
        mockContext.Setup(c => c.EmployeeSkill).Returns(mockSet.Object);

        var baseRepository = new BaseRepository<EmployeeSkill>(mockContext.Object);

        var data = await baseRepository.FindAsync(1);

        Assert.NotEqual(null, data);

    }

    private EmployeeSkill GetEmployeeSkill()
    {
        return new EmployeeSkill
        {
            SkillDescription = "SkillDescription",
            SkillName = "SkillName",
            Id = 1
        };
    }

    private IQueryable<EmployeeSkill> GetEmployeeSkills()
    {
        return new List<EmployeeSkill>
        {
            GetEmployeeSkill(),
            GetEmployeeSkill(),
            GetEmployeeSkill(),
        }.AsQueryable();
    }

结果是:

Assert.NotEqual() 失败

我认为问题在于

 public BaseRepository(DbContext innerDbContext)
 {
     InnerDbContext = innerDbContext;
     InnerDbSet = InnerDbContext.Set<T>();  <<<<<<<<<<<
 }

但是我不明白为什么会出现这个问题以及如何解决它。

我在使用以下工具:

  • Visual Studio 2013 Ultimate
  • Moq
  • xUnit

提前感谢。


1
对于未来的读者:如果你在各处注入一个 DbContext,而无法将其抽象为存储库/接口,并且你绝对需要“模拟”它,因为它是非虚拟的,那么你可以使用 Microsoft Fakes Assemblies 来实现这一点,因为它会执行一些花哨的 IL 操作,让你甚至可以为对象上的非虚拟方法提供替代实现。 - Jesus is Lord
1
我强烈推荐你去看一下"Effort"(https://effort.codeplex.com/),这是一个EF单元测试工具,它可以在内存中创建一个足够真实的EF数据库用于测试。我们之前也曾经尝试过使用模拟EF进行测试,但最终发现这种方式就像苹果和橙子的对比一样不可行。(举个例子,模拟测试使用的是L2O而不是L2E。) - RJB
这看起来是一个不错的工具,稍后会检查一下。无论如何感谢@RJB。 - Hasanuzzaman
对于EF 6,我发现这篇文章中的代码非常有用:https://msdn.microsoft.com/en-us/library/dn314429.aspx我也遇到了模拟AsNoTracking()的问题,而这个出色的答案解决了这个问题:https://dev59.com/ll8d5IYBdhLWcg3wNQRg#27087604 - madannes
该网址会出现谷歌警告,提示前方有危险程序,请勿继续访问。 - Mordy
1个回答

11
您是正确的,问题在于您的InnerDbContext.Set<T>();语句。
在当前版本的EF(6.0.2)中,DbContext.Set<T>方法不是虚拟的,因此无法使用Moq进行模拟。
因此,除非更改BaseRepository的设计以不依赖于整个DbContext而是依赖于一个DbSet<T>,否则您不能轻松使测试通过:
所以类似这样的代码:
public BaseRepository(DbSet<T> dbSet)
{
    InnerDbSet = dbSet;
}

然后你可以直接通过模拟的 DbSet 进行传递。

或者你可以为 DbContext 创建一个包装器接口:

public interface IDbContext
{
    DbSet<T> Set<T>() where T : class;
}

public class TimeSketchContext : DbContext, IDbContext
{
    public virtual DbSet<EmployeeSkill> EmployeeSkill { get; set; }
}

然后在您的 BaseRepository 中使用 IDbContext

public class BaseRepository<T> : IRepositoryBase<T> where T : class, IEntity, new()
{
    protected readonly IDbContext InnerDbContext;
    protected DbSet<T> InnerDbSet;

    public BaseRepository(IDbContext innerDbContext)
    {
        InnerDbContext = innerDbContext;
        InnerDbSet = InnerDbContext.Set<T>();
    }

    public virtual Task<T> FindAsync(long id)
    {
        return InnerDbSet.FirstOrDefaultAsync(x => x.Id == id);
    }
}

最后,您只需要更改测试中的两行内容即可使其通过:
var mockContext = new Mock<IDbContext>();
mockContext.Setup(c => c.Set<EmployeeSkill>()).Returns(mockSet.Object);

因为方法是 FindAsync,但实现却是 FirstAndDefaultAsync,所以被踩了个负评。 - Razort4x

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接