Execution context (AsyncLocal) loses values if TestInitialize is an async method

577 Views Asked by At

Consider the following minimal repro example (.NET 7, MSTest 2.2.10):

[TestClass]
public class UnitTest1
{
    private AsyncLocal<string> _local = new AsyncLocal<string>();

    [TestInitialize]
    public async Task Init()
    {
        _local.Value = "SomeValue";
        Console.WriteLine(_local.Value != null); // yields True

        await Task.FromResult(0);
        Console.WriteLine(_local.Value != null); // yields True
    }

    [TestMethod]
    public void TestMethod1()
    {
        Console.WriteLine(_local.Value != null); // expected True, actual False

    }

    [TestCleanup]
    public void Cleanup()
    {
        Console.WriteLine(_local.Value != null); // expected True, actual False
    }
}

If I remove the line await Task.FromResult(0) from the test initializer and declare Init as public void Init(), everything works as expected (all WriteLines output True).

In other words: AsyncLocal values set in the test initializer are lost if (and only if) the test initializer is async. This is inconvenient for my use case, since the class I am testing internally uses an AsyncLocal and, thus, I cannot use TestInitialize to initialize it.

I know that I can work around this issue by making the test initializer synchronous (and wrap all asynchronous operations in Task.Run(...).Result). Still, I'm wondering: Is this expected behavior or did I find a bug in MSTest?

1

There are 1 best solutions below

0
Stephen Cleary On BEST ANSWER

AsyncLocal<T> is designed for code-scoped locals. I.e., if A() sets an AsyncLocal<T>.Value and then calls B(), then B() should see that value.

In this case, the TestInitialize method does not call the TestMethod methods, so attempting to set an AsyncLocal<T> in TestInitialize is incorrect. Instead, you should set it in the arrange part of each unit test. If the setup is complex, then use a helper method to build the value, and then set it in the arrange part of each unit test.

If I remove the line await Task.FromResult(0) from the test initializer and declare Init as public void Init(), everything works as expected (all WriteLines output True).

This happens to work, but is not guaranteed. What is happening is this: setting an AsyncLocal<T> from a synchronous method modifies the logical call context in the closest async method further up the call stack (more details on my blog). I recommend not depending on this behavior, since it's often surprising.

Is this expected behavior or did I find a bug in MSTest?

It's expected behavior. Specifically, expected behavior for AsyncLocal<T>; it doesn't have anything to do with MSTest.