Getting Started
Installation
Consume this library via its NuGet Package. Click on the badge to find its latest version and the instructions for consuming it that best apply to your project.
Default Fact behavior
/// <summary>
/// Demonstrates that xunit [Fact] behavior is to invoke the test on an MTA thread.
/// </summary>
[Fact]
public async Task Fact_OnMTAThread()
{
ApartmentState expectedApartment = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? ApartmentState.MTA
: ApartmentState.Unknown;
Assert.Equal(expectedApartment, Thread.CurrentThread.GetApartmentState());
await Task.Yield();
Assert.Equal(expectedApartment, Thread.CurrentThread.GetApartmentState());
}
Usage
Portable UI
Best when you need basic UI thread semantics for tests that may run on any OS. You'll get an STA thread on Windows.
/// <summary>
/// Demonstrates that the <see cref="UIFactAttribute"/> can run on all platforms,
/// and emulates a UI thread (not specific to WPF or WinForms) with a <see cref="SynchronizationContext"/>
/// that keeps yielding awaits on the original thread.
/// </summary>
[UIFact]
public async Task UIFact_OnSTAThread()
{
int initialThread = Environment.CurrentManagedThreadId;
Assert.NotNull(SynchronizationContext.Current);
ApartmentState expectedApartment = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? ApartmentState.STA
: ApartmentState.Unknown;
Assert.Equal(expectedApartment, Thread.CurrentThread.GetApartmentState());
await Task.Yield();
Assert.Equal(initialThread, Environment.CurrentManagedThreadId);
Assert.NotNull(SynchronizationContext.Current);
}
WPF
More closely resembles WPF-specific semantics including a WPF-specific SynchronizationContext.
/// <summary>
/// Demonstrates that <see cref="WpfFactAttribute"/> invokes tests expecting an STA thread
/// specifically with a WPF SynchronizationContext.
/// </summary>
[WpfFact]
public async Task WpfFact_OnSTAThread()
{
Assert.Equal(ApartmentState.STA, Thread.CurrentThread.GetApartmentState());
Assert.IsType<DispatcherSynchronizationContext>(SynchronizationContext.Current);
await Task.Yield();
Assert.Equal(ApartmentState.STA, Thread.CurrentThread.GetApartmentState()); // still there
Assert.IsType<DispatcherSynchronizationContext>(SynchronizationContext.Current);
}
WinForms
More closely resembles WinForms-specific semantics including a WinForms-specific SynchronizationContext.
/// <summary>
/// Demonstrates that <see cref="WinFormsFactAttribute"/> invokes tests expecting an STA thread
/// specifically with a WinForms SynchronizationContext.
/// </summary>
[WinFormsFact]
public async Task WinFormsFact_OnSTAThread()
{
Assert.Equal(ApartmentState.STA, Thread.CurrentThread.GetApartmentState());
Assert.IsType<WindowsFormsSynchronizationContext>(SynchronizationContext.Current);
await Task.Yield();
Assert.Equal(ApartmentState.STA, Thread.CurrentThread.GetApartmentState()); // still there
Assert.IsType<WindowsFormsSynchronizationContext>(SynchronizationContext.Current);
}
WinUI
Use @Xunit.WinUIFactAttribute or @Xunit.WinUITheoryAttribute to initialize WinUI XAML and run a test with a
@Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext. The test project must use a Windows-versioned
target framework such as net8.0-windows10.0.17763.0.
An unpackaged test executable that uses these attributes must also enable Windows App SDK bootstrap initialization:
<PropertyGroup>
<TargetFramework>net8.0-windows10.0.17763.0</TargetFramework>
<WindowsPackageType>None</WindowsPackageType>
<WindowsAppSdkBootstrapInitialize>false</WindowsAppSdkBootstrapInitialize>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
</PropertyGroup>
WindowsAppSDKSelfContained allows tests to run on machines that do not have the Windows App SDK runtime installed.
Choose a runtime identifier that matches the test process architecture. For a framework-dependent test host instead,
omit WindowsAppSDKSelfContained and RuntimeIdentifier, and set WindowsAppSdkBootstrapInitialize to true.
These properties are not required for projects that use only the portable UI, STA, WPF, or WinForms attributes.
[WinUIFact]
public async Task ControlStaysOnDispatcherQueue()
{
var button = new Microsoft.UI.Xaml.Controls.Button();
Assert.True(button.DispatcherQueue.HasThreadAccess);
await Task.Yield();
Assert.True(button.DispatcherQueue.HasThreadAccess);
}
STA thread
Guarantees the test to run on an STA thread. Applicable only on Windows. Because no SynchronizationContext is applied by default, an async test will resume on a threadpool thread instead of the test thread after a yielding await.
/// <summary>
/// Demonstrates that <see cref="StaFactAttribute"/> invokes tests expecting an STA thread
/// but with no <see cref="SynchronizationContext"/> at all, so any yielding await will
/// return on the threadpool (an MTA thread).
/// </summary>
[StaFact]
public async Task StaWithoutSyncContext()
{
Assert.Equal(ApartmentState.STA, Thread.CurrentThread.GetApartmentState());
Assert.Null(SynchronizationContext.Current);
await Task.Yield();
// Without a single-threaded SynchronizationContext, we won't come back to the STA thread.
Assert.Equal(ApartmentState.MTA, Thread.CurrentThread.GetApartmentState());
Assert.Null(SynchronizationContext.Current);
}
Sharing a UI thread between tests
By default, each UI fact or theory gets a fresh thread. When thread-affine state must outlive one test, register a shared-thread fixture and receive it in the test class constructor:
/// <summary>
/// Every UI fact or theory in this class uses one shared thread.
/// </summary>
public class ClassScopedTests : IClassFixture<UIThreadFixture>
{
/// <summary>
/// Initializes a new instance of the <see cref="ClassScopedTests"/> class.
/// </summary>
/// <param name="fixture">The shared thread fixture.</param>
public ClassScopedTests(UIThreadFixture fixture)
{
}
/// <summary>
/// Runs on the shared thread.
/// </summary>
[UIFact]
public void FirstTest()
{
Assert.NotNull(SynchronizationContext.Current);
}
/// <summary>
/// Runs on the same thread as <see cref="FirstTest"/>.
/// </summary>
[UIFact]
public void SecondTest()
{
Assert.NotNull(SynchronizationContext.Current);
}
}
See Shared UI thread fixtures for collection scope, fixture lifecycle hooks, supported fixture types, and concurrency behavior.