Table of Contents

Features

The following test attributes are supported:

Xunit test attributes Supported OS's SynchronizationContext STA thread?
UIFactAttribute, UITheoryAttribute All Yes1 yes2
WpfFactAttribute, WpfTheoryAttribute Windows only3 DispatcherSynchronizationContext yes
WinFormsFactAttribute, WinFormsTheoryAttribute Windows only3 WindowsFormsSynchronizationContext yes
@Xunit.WinUIFactAttribute, @Xunit.WinUITheoryAttribute Windows 10 1809+4 @Microsoft.UI.Dispatching.DispatcherQueueSynchronizationContext yes
StaFactAttribute, StaTheoryAttribute Windows only3 No yes
@Xunit.CocoaFactAttribute, @Xunit.CocoaTheoryAttribute Mac OSX only3 Yes1 no

We also offer a UISettingsAttribute that can be applied to individual test methods or test classes to control the behavior of the various UI test attributes. This attribute offers a means to add automated retries to a test's execution for unstable tests.

Shared UI thread fixtures

By default, every fact or theory in this package runs on a newly created thread that is disposed when that test finishes. This avoids sharing thread-affine state and allows unrelated tests to run concurrently. WinUI tests still get a fresh thread, but their XAML lifetimes are serialized within the process because overlapping WinUI XAML managers can terminate the test process.

Some UI frameworks retain thread-affine objects in static caches, or a suite may intentionally host an application-level object for several tests. In those cases, opt in to a shared thread with an xUnit class or collection fixture. The fixture owns the thread, and its xUnit lifetime determines how long the thread is shared.

Test attribute Compatible fixture
UIFactAttribute, UITheoryAttribute UIThreadFixture
StaFactAttribute, StaTheoryAttribute StaThreadFixture
WpfFactAttribute, WpfTheoryAttribute WpfThreadFixture
WinFormsFactAttribute, WinFormsTheoryAttribute WinFormsThreadFixture
@Xunit.WinUIFactAttribute, @Xunit.WinUITheoryAttribute @Xunit.WinUIThreadFixture
@Xunit.CocoaFactAttribute, @Xunit.CocoaTheoryAttribute @Xunit.CocoaThreadFixture

Class scope

Implement IClassFixture<TFixture> and receive that fixture in the test class constructor. All compatible UI facts and theories in the class then execute on the fixture's thread.

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

The fixture must be present in the constructor arguments so the test runner can identify it. Merely declaring IClassFixture<TFixture> without receiving the fixture does not opt the tests into its thread.

Collection scope

Use an ICollectionFixture<TFixture> when multiple test classes must share the same thread. Each participating class must belong to that collection and receive the collection fixture in its constructor.

/// <summary>
/// Defines a collection whose test classes share one UI thread.
/// </summary>
[CollectionDefinition(nameof(SharedUIThreadCollection))]
public class SharedUIThreadCollection : ICollectionFixture<UIThreadFixture>
{
}

/// <summary>
/// Tests in any class in this collection can use the same fixture thread.
/// </summary>
[Collection(nameof(SharedUIThreadCollection))]
public class CollectionScopedTests
{
    /// <summary>
    /// Initializes a new instance of the <see cref="CollectionScopedTests"/> class.
    /// </summary>
    /// <param name="fixture">The collection's shared thread fixture.</param>
    public CollectionScopedTests(UIThreadFixture fixture)
    {
    }

    /// <summary>
    /// Runs on the collection fixture thread.
    /// </summary>
    [UIFact]
    public void UsesCollectionThread()
    {
        Assert.NotNull(SynchronizationContext.Current);
    }
}

xUnit does not run tests in one collection concurrently, so tests borrowing one collection fixture do not overlap. Classes using different fixture instances remain eligible for parallel execution. The xUnit execution throttle remains in effect while a test is running on a fixture thread.

Fixture initialization and cleanup

xUnit constructs fixtures on its own worker thread, before Xunit.StaFact participates in test execution. Consequently, a fixture's CLR constructor is not guaranteed to run on an STA or UI thread.

The simplest usage requires no custom fixture type: register and inject one of the library-provided fixtures directly, as shown in the class and collection examples above.

For thread-affine setup and cleanup, derive from the compatible fixture type and override InitializeOnUIThreadAsync and DisposeOnUIThreadAsync. These hooks are dispatched to the owned thread, and cleanup runs before that thread is shut down.

/// <summary>
/// Owns thread-affine state and initializes and disposes it on the shared UI thread.
/// </summary>
public class ThreadAffineFixture : UIThreadFixture
{
    /// <summary>
    /// Gets the thread that owns the fixture state.
    /// </summary>
    public int ThreadId { get; private set; }

    /// <inheritdoc/>
    protected override ValueTask InitializeOnUIThreadAsync()
    {
        this.ThreadId = Environment.CurrentManagedThreadId;
        return default;
    }

    /// <inheritdoc/>
    protected override ValueTask DisposeOnUIThreadAsync()
    {
        Assert.Equal(this.ThreadId, Environment.CurrentManagedThreadId);
        return default;
    }
}

The portable UI, WPF, WinForms, WinUI, and Cocoa fixtures install their corresponding synchronization context. The STA fixture intentionally does not install one, matching StaFactAttribute behavior; after a yielding await, code may resume on a thread-pool thread.

Compatibility and ownership rules

  • A test class may receive at most one @Xunit.UIThreadFixtureBase-derived fixture.
  • The fixture must match the fact or theory attribute. For example, a WpfFactAttribute cannot borrow an UIThreadFixture.
  • Tests that do not receive a shared-thread fixture retain the default fresh-thread-per-test behavior.
  • Do not dispose a fixture directly when xUnit owns it. xUnit disposes it at the end of its class or collection lifetime.
  • Put only state that is intentionally shared into the fixture. Test class instances are still created separately for each test.

  1. This is a private SynchronizationContext that works cross-platform and effectively keeps code running on the test's starting thread the way a GUI application's main thread would do.

  2. STA thread only applies on Windows. On other operating systems, an MTA thread is used.

  3. Windows-only attributes result in the test to result in "Skipped" on other operating systems.

  4. WinUI attributes require a Windows-versioned target framework such as net8.0-windows10.0.17763.0.