单元测试 FindAsync

8
我已经按照这篇优秀的MSDN文章中提供的异步查询提供程序建立了一个测试项目:http://msdn.microsoft.com/en-US/data/dn314429#async,效果非常好。
但是当我添加一个调用FindAsync的方法时:
public async Task<Blog> GetBlog(int blogId)
{
    return await _context.Blogs.FindAsync(blogId);
}

请按照以下格式添加单元测试:

[TestMethod]
public async Task GetAllBlogsAsync_gets_blog()
{
    var data = new List<Blog>
    {
        new Blog { BlogId = 1, Name = "BBB" },
        new Blog { BlogId = 2, Name = "ZZZ" },
        new Blog { BlogId = 3, Name = "AAA" },
    }.AsQueryable();

    var mockSet = new Mock<DbSet<Blog>>();
    mockSet.As<IDbAsyncEnumerable<Blog>>()
        .Setup(m => m.GetAsyncEnumerator())
        .Returns(new TestDbAsyncEnumerator<Blog>(data.GetEnumerator()));

    mockSet.As<IQueryable<Blog>>()
        .Setup(m => m.Provider)
        .Returns(new TestDbAsyncQueryProvider<Blog>(data.Provider));

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

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

    var service = new BlogService(mockContext.Object);
    var blog = await service.GetBlog(2);

    Assert.AreEqual("ZZZ", blog.Name);
}

然而,当我的测试方法调用GetBlog时,await _context.Blogs.FindAsync(blogId);会抛出一个NullReferenceException, 它在TestingDemo.BlogService.<GetBlog>d__5.MoveNext()中被引发。
有什么建议可以在调用FindAsync的方法上实现单元测试,并使用MSDN文章中找到的测试方法吗:http://msdn.microsoft.com/en-US/data/dn314429#async

你确定问题不是出在你的模拟设置上吗?那看起来是最复杂的部分,也有可能会受到“NullReferenceExceptions”的影响。 - Tim S.
@Tim S - 你说得没错,但这几乎是与 MSDN 文章底部使用的 GetAllBlogsAsync 相同的嘲讽设置,而该设置运行良好。 - Andy
测试外部依赖不仅非常困难(正如您从创建的模拟对象的混乱中可以看到的那样),而且其价值存疑。您不仅在测试外部依赖项,还在合成条件下进行测试,因为您没有调用实际数据库,并且在您的测试场景中存在许多移动部件,在您的实际场景中则不存在(反之亦然)。 - 48klocs
2个回答

12

NullReferenceExceptionasync方法中的MoveNext内几乎总是由于从另一个异步方法返回null引起的。

在这种情况下,看起来FindAsync正在返回null,这是有道理的,因为我没有看到您在模拟它。您目前正在模拟IQueryableGetAsyncEnumerator方面,但不是FindAsync。示例文章未提供完整的DbSet模拟解决方案。


7
当然可以!我曾天真地认为文章提供了完整的实现。做一些像这样的事情:mockSet.Setup(t => t.FindAsync(It.IsAny<int>())).Returns(Task.FromResult(fakeBlog)); 看起来是有效的 - 这是否是您所指的类型? - Andy
2
@Andy:是的,那就是我想说的。 - Stephen Cleary
@Andy 在你的评论中,fakeBlog 是什么? - LJNielsenDk
1
@LJNielsenDk 这是一个我们在这里创建的虚假博客,例如 var fakeBlog = new Blog { BlogId = 7, Name = "Fake Blog" };,然后在测试期间设置FindAsync方法返回这个虚假博客(使用上面的注释中的代码)。 - Andy
@StephenCleary 谢谢!我也遇到了同样的问题。但是现在,如果我这样做,mockSet.Setup(c => c.FirstOrDefaultAsync(It.IsAny<Expression<Func<UserAccount, bool>>>())).Returns(Task.FromResult<UserAccount>(localUserAccount)); 我会得到一个异常,说:Invalid setup on an extension method: c => c.UserAccount.FirstOrDefaultAsync<UserAccount>(It.IsAny<Expression1>())` 我无法想象出错在哪里。FirstOrDefault不是扩展方法,它是从接口的基类中实现的。 - Razort4x
@Razort4x:抱歉,我已经很久没有模拟异步EF了。我建议您在SO上发布自己的问题。 - Stephen Cleary

0
我也遇到了这个问题。还有另一种解决方案,特别是当你只期望一个结果,并且不需要依赖上下文缓存时,这种方法快速而简单。那就是使用SingleOrDefaultAsync而不是FindAsync。
旧版本:
public async Task<Blog> GetBlog(int blogId)
{
    return await _context.Blogs.FindAsync(blogId);
}

新版本:

public async Task<Blog> GetBlog(int blogId)
{
    return await _context.Blogs.Where(b => b.BlogId == blogId).SingleOrDefaultAsync();
}

这个版本将与你所拥有的模拟代码一起工作。

(这篇帖子的答案解释了差异:在async await中使用Include


2
应该有一个好的方法来实现这个。FindAsync方法首先会查找实体是否已经加载并返回它。如果没有,它将从数据库中获取。按照您建议的执行查询将总是从数据库中获取。 - Jacques Bourque
1
你正在改变实现方式,而不仅仅是语法。 - Max

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