【问题标题】:How to correctly write async XUnit test?如何正确编写异步 XUnit 测试?
【发布时间】:2019-07-19 23:32:50
【问题描述】:

我正在使用异步 xUnit 测试,但我注意到传递行为不一致:

public async Task FetchData()
{
    //Arrange
    var result = await arrangedService.FetchDataAsync().ConfigureAwait(false);
    //Assert
}

我已经检查了这个测试执行的调用堆栈,并验证了我的所有库代码在每个任务之后都调用了.ConfigureAwait(false)。然而,尽管如此,这个测试和其他测试在执行Run All 时会间歇性地失败,但是当我在调试器上运行时通过断言和手动检查。很明显,我没有正确地做某事。我已经尝试在测试本身中删除对ConfigureAwait(false) 的调用,以防有特殊的xUnit 同步上下文,但它没有改变任何东西。以一致的方式测试异步代码的最佳方法是什么?

编辑好的,这是我尝试创建一个正在运行的代码的超级简化示例,以提供正在发生的事情的示例:

using Graph = Microsoft.Azure.ActiveDirectory.GraphClient;

public async Task FetchData()
{
    var adUsers = baseUsers //IEnumerable<Graph.User>
        .Cast<Graph.IUser>()
        .ToList();
    var nextPageUsers = Enumerable
        .Range(GoodIdMin, GoodIdMax)
        .Select(number => new Graph.User
        {
            Mail = (-number).ToString()
        })
        .Cast<Graph.IUser>()
        .ToList();

    var mockUserPages = new Mock<IPagedCollection<Graph.IUser>>();
    mockUserPages
        .Setup(pages => pages.MorePagesAvailable)
        .Returns(true);
    mockUserPages
        .Setup(pages => pages.CurrentPage)
        .Returns(new ReadOnlyCollection<Graph.IUser>(adUsers));
    mockUserPages
        .Setup(pages => pages.GetNextPageAsync())
        .ReturnsAsync(mockUserPages.Object)
        .Callback(() =>
        {
            mockUserPages
                .Setup(pages => pages.CurrentPage)
                .Returns(new ReadOnlyCollection<Graph.IUser>(nextPageUsers));
            mockUserPages
                .Setup(pages => pages.MorePagesAvailable)
                .Returns(false);
        });

    var mockUsers = new Mock<Graph.IUserCollection>();
    mockUsers
        .Setup(src => src.ExecuteAsync())
        .ReturnsAsync(mockUserPages.Object);

    var mockGraphClient = new Mock<Graph.IActiveDirectoryClient>();
    mockGraphClient
        .Setup(src => src.Users)
        .Returns(mockUsers.Object);

    var mockDbUsers = CreateBasicMockDbSet(baseUsers.Take(10)
        .Select(user => new User
        {
            Mail = user.Mail
        })
        .AsQueryable());
    var mockContext = new Mock<MyDbContext>();
    mockContext
        .Setup(context => context.Set<User>())
        .Returns(mockDbUsers.Object);

    var mockGraphProvider = new Mock<IGraphProvider>(); 
    mockGraphProvider
        .Setup(src => src.GetClient()) //Creates an IActiveDirectoryClient
        .Returns(mockGraphClient.Object);

    var getter = new UserGetter(mockContext.Object, mockGraphProvider.Object);

    var result = await getter.GetData().ConfigureAwait(false);

    Assert.True(result.Success); //Not the actual assert
}

这是在var result = ... 行上执行的代码:

public UserGetterResult GetData()
{
    var adUsers = await GetAdUsers().ConfigureAwait(false);
    var dbUsers = Context.Set<User>().ToList(); //This is the injected context from before
    return new UserGetterResult //Just a POCO
    {
        AdUsers = adUsers
            .Except(/*Expression that indicates whether
             or not this user is in the database*/)
            .ProjectTo<User>()
            .ToList(),
        DbUsers = dbUsers.ProjectTo<User>().ToList() //Automapper 6.1.1
    };
}

private async Task<List<User>> GetAdUsers()
{
    var userPages = await client //Injected IActiveDirectoryClient from before
        .Users
        .ExecuteAsync()
        .ConfigureAwait(false);
    var users = userPages.CurrentPage.ToList();
    while(userPages.MorePagesAvailable)
    {
        userPages = await userPages.GetNextPageAsync().ConfigureAwait(false);
        users.AddRange(userPages.CurrentPage);
    }
    return users;
}

代码的目的是获取AD中但不在数据库中的用户列表和数据库中的用户列表。

EDIT EDIT由于我忘记将其包含在原始更新中,因此错误都发生在调用 `IUserCollection.ExecuteAsync() 时。

【问题讨论】:

  • 如果您的测试间歇性地失败,那么很可能是某个地方的竞争条件。如果您找到一种方法来始终如一地调试它们,那么您将被全球开发人员视为英雄
  • @Nkosi 好的,我已尽我所能完整地重新创建它。
  • @KevinGosse 我怀疑这是一个竞争条件,因为所有模拟服务都包含在测试范围内,即使它们使用线程安全读取对象,如 List
  • 快速提问,我假设被测代码在生产环境中工作?
  • 抱歉回复晚了,通过典型用例手动测试代码一直运行良好。

标签: c# asynchronous moq xunit


【解决方案1】:

IUserCollection.ExecuteAsync() 似乎根据原始帖子中显示的内容正确配置。

现在重点关注以下方法……

private async Task<List<User>> GetAdUsers() {
    var userPages = await client //Injected IActiveDirectoryClient from before
        .Users
        .ExecuteAsync()
        .ConfigureAwait(false);
    var users = userPages.CurrentPage.ToList();
    while(userPages.MorePagesAvailable) {
        userPages = await userPages.GetNextPageAsync().ConfigureAwait(false);
        users.AddRange(userPages.CurrentPage);
    }
    return users;
}

我关心的是如何在模拟中设置用户页面。鉴于GetAdUsers 方法的流程,最好使用SetupSequence 模拟重复调用CurrentPageMorePagesAvailable

var mockUserPages = new Mock<IPagedCollection<Graph.IUser>>();
mockUserPages
    .SetupSequence(_ => _.MorePagesAvailable)
    .Returns(true) // First time called to enter while loop
    .Returns(false); // Second time called to exit while loop
mockUserPages
    .SetupSequence(_ => _.CurrentPage)
    .Returns(new ReadOnlyCollection<Graph.IUser>(adUsers)) // First time called to get List
    .Returns(new ReadOnlyCollection<Graph.IUser>(nextPageUsers)); // Second time called to get next page
mockUserPages
    .Setup(pages => pages.GetNextPageAsync())
    .ReturnsAsync(mockUserPages.Object); // No need for callback

参考Moq Quickstart

【讨论】:

    【解决方案2】:

    我怀疑问题可能是执行回调和对 mockUserPages.CurrentPage 的下一个请求之间的延迟

    尝试分离用户页面集合:

    var mockAdUserPages = new Mock<IPagedCollection<Graph.IUser>>();
        mockAdUserPages 
            .Setup(pages => pages.MorePagesAvailable)
            .Returns(true);
        mockAdUserPages 
            .Setup(pages => pages.CurrentPage)
            .Returns(new ReadOnlyCollection<Graph.IUser>(adUsers));
    
    //Setup second page
    var mockNextUserPages = new Mock<IPagedCollection<Graph.IUser>>();
    mockNextUserPages 
            .Setup(pages => pages.MorePagesAvailable)
            .Returns(false);
        mockNextUserPages 
            .Setup(pages => pages.CurrentPage)
            .Returns(new ReadOnlyCollection<Graph.IUser>(nextPageUsers));
    
    //Return next page
        mockAdUserPages 
            .Setup(pages => pages.GetNextPageAsync())
            .ReturnsAsync(mockNextUserPages.Object);
    

    【讨论】:

    • 不,最后的设置会覆盖之前的任何设置。
    • 即使这样做,这个测试和其他一些没有 callback() 的测试也会间歇性地失败。
    猜你喜欢
    • 2014-07-12
    • 1970-01-01
    • 1970-01-01
    • 2014-05-21
    • 1970-01-01
    • 2019-04-06
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多