Как правильно написать асинхронный тест XUnit?

Я использую асинхронные тесты 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, но не базу данных, и список пользователей, которые находятся в базе данных.

РЕДАКТИРОВАТЬ РЕДАКТИРОВАТЬ Так как я забыл включить это в исходное обновление, все ошибки происходят при вызовах `IUserCollection.ExecuteAsync().

2 ответа

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 высмеивать повторяющиеся звонки CurrentPage а также MorePagesAvailable,

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

Я подозреваю, что проблема может быть задержка между выполнением обратного вызова и следующего запроса к 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);
Другие вопросы по тегам