Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to correctly write async XUnit test?

I am using async xUnit tests and I am noticing inconsistent passing behavior:

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

I have gone through the call stack executed by this test and have verified that all of my library code is calling .ConfigureAwait(false) after each task. However, in spite of this, this test and others will intermittently fail when performing a Run All, but pass the asserts and manual inspection when I walk through on the debugger. So clearly I am not doing something correctly. I have tried removing the call to ConfigureAwait(false) in the test itself in case there is a special xUnit synchronization context, but it did not change anything. What is the best way to test asynchronous code in a consistent way?

EDIT Okay here is my attempt to create a super-simplified example of the code that is running to provide an example of what is happening:

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
}

And here is the code being executed on the var result = ... line:

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;
}

The purpose of the code is to get a list of users who are in AD but not the database and a list of users who are in the database.

EDIT EDIT Since I forgot to include this in the original update, the errors are all occurring on calls to `IUserCollection.ExecuteAsync().

like image 929
jokulmorder Avatar asked Jan 29 '18 15:01

jokulmorder


People also ask

How do you test asynchronous methods?

This illustrates the first lesson from the async/await conceptual model: To test an asynchronous method's behavior, you must observe the task it returns. The best way to do this is to await the task returned from the method under test.

Should unit tests be async?

It is recommended that programmers use async Task unit test methods in lieu of async void. The reason is that it is difficult to retrieve the test results from the async void unit test methods in C#. It should be noted that not all unit test frameworks support async unit tests that return void.

Do xUnit tests run in order?

Each test class is a unique test collection and tests under it will run in sequence, so if you put all of your tests in the same collection then it will run sequentially.


1 Answers

IUserCollection.ExecuteAsync() appears to be configured correctly based on what was shown in the original post.

Now focusing on the following method...

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;
}

I am concerned with how user pages was setup in the mock. Given the flow of the GetAdUsers method it would be better to use SetupSequence to mock the repeated calls CurrentPage and 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

Reference Moq Quickstart

like image 126
Nkosi Avatar answered Oct 01 '22 13:10

Nkosi