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?
AsyncLocal<T>is designed for code-scoped locals. I.e., ifA()sets anAsyncLocal<T>.Valueand then callsB(), thenB()should see that value.In this case, the
TestInitializemethod does not call theTestMethodmethods, so attempting to set anAsyncLocal<T>inTestInitializeis 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.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 closestasyncmethod further up the call stack (more details on my blog). I recommend not depending on this behavior, since it's often surprising.It's expected behavior. Specifically, expected behavior for
AsyncLocal<T>; it doesn't have anything to do with MSTest.