Edit

Share via


Test execution and control in MSTest

MSTest provides attributes to control how tests execute, including parallelization, threading models, timeouts, retries, and conditional execution based on platform or environment.

Threading attributes

Threading attributes control which thread model test methods use. These attributes are essential when testing COM components, UI elements, or code with specific threading requirements.

STATestClassAttribute

The STATestClassAttribute runs all test methods in a class (including ClassInitialize and ClassCleanup) in a single-threaded apartment (STA). Use this attribute when testing COM objects that require STA.

[STATestClass]
public class ComInteropTests
{
    [TestMethod]
    public void TestComComponent()
    {
        // This test runs in an STA thread
        var comObject = new SomeComObject();
        // Test COM interactions
    }
}

Note

This attribute is only supported on Windows in MSTest v3.6 and later.

STATestMethodAttribute

The STATestMethodAttribute runs a specific test method in a single-threaded apartment. Use this attribute for individual tests that need STA while other tests in the class don't.

[TestClass]
public class MixedThreadingTests
{
    [STATestMethod]
    public void TestRequiringSTA()
    {
        // This test runs in an STA thread
    }

    [TestMethod]
    public void RegularTest()
    {
        // This test uses default threading
    }
}

Note

This attribute is only supported on Windows in MSTest v3.6 and later.

Preserve STA context for async continuations

Starting with MSTest 4.1, the STATestMethodAttribute includes a UseSTASynchronizationContext property that ensures async continuations run on the same STA thread. When enabled, the attribute creates a custom SynchronizationContext that posts continuations back to the STA thread, which is essential for testing UI components that require STA threading throughout their async operations.

[TestClass]
public class UIComponentTests
{
    [STATestMethod(UseSTASynchronizationContext = true)]
    public async Task TestAsyncUIOperation()
    {
        // Initial code runs on STA thread
        var control = new MyControl();
        
        await control.LoadDataAsync();
        
        // Continuation also runs on STA thread,
        // ensuring UI operations remain valid
        Assert.IsTrue(control.IsDataLoaded);
    }
}

Tip

Use UseSTASynchronizationContext = true when testing Windows Forms or WPF components that perform async operations and expect their continuations to run on the same thread.

UITestMethodAttribute

The UITestMethod attribute schedules test execution on the UI thread. This attribute is designed for testing UWP and WinUI applications that require UI thread access.

[TestClass]
public class WinUITests
{
    [UITestMethod]
    public void TestUIComponent()
    {
        // This test runs on the UI thread
        var button = new Button();
        button.Content = "Click me";
        Assert.IsNotNull(button.Content);
    }
}

Note

This attribute requires the appropriate MSTest adapter for UWP or WinUI platforms. For more information, see the platform support section.

Parallelization attributes

Parallelization attributes control whether and how tests run concurrently, improving test execution time.

ParallelizeAttribute

By default, MSTest runs tests sequentially. The ParallelizeAttribute assembly-level attribute enables parallel test execution.

using Microsoft.VisualStudio.TestTools.UnitTesting;

[assembly: Parallelize(Workers = 0, Scope = ExecutionScope.MethodLevel)]

Parallelization scope

Scope Behavior
ClassLevel Multiple test classes run in parallel, but tests within a class run sequentially
MethodLevel Individual test methods can run in parallel, regardless of their class

Worker threads

The Workers property specifies the maximum number of threads for parallel execution:

  • 0 (default): Use the number of logical processors on the machine
  • Any positive integer: Use that specific number of threads
// Parallelize at class level with 2 worker threads
[assembly: Parallelize(Workers = 2, Scope = ExecutionScope.ClassLevel)]

Tip

You can also configure parallelization through runsettings or testconfig.json without modifying code.

Tip

Enable parallelization at the assembly level by default, even if many tests currently require sequential execution. This approach encourages writing new tests that support parallel execution from the start. Use the MSTEST0001 analyzer to ensure that every test class explicitly declares its parallelization intent, which forces you to review whether each class safely supports concurrent execution. Often, excluding just a few classes or methods with DoNotParallelize is sufficient, allowing the majority of your tests to run in parallel for significantly faster test execution.

DoNotParallelizeAttribute

The DoNotParallelizeAttribute prevents parallel execution for specific assemblies, classes, or methods. Use this attribute when tests share state or resources that can't be safely accessed concurrently.

[assembly: Parallelize(Scope = ExecutionScope.MethodLevel)]

[TestClass]
public class ParallelTests
{
    [TestMethod]
    public void CanRunInParallel()
    {
        // This test can run with others
    }
}

[TestClass]
[DoNotParallelize]
public class SequentialTests
{
    [TestMethod]
    public void MustRunSequentially()
    {
        // This class's tests run sequentially
    }
}

[TestClass]
public class MixedTests
{
    [TestMethod]
    public void CanRunInParallel()
    {
        // This test can run with others
    }

    [TestMethod]
    [DoNotParallelize]
    public void MustBeIsolated()
    {
        // This specific test doesn't run in parallel
    }
}

Note

You only need DoNotParallelize when you've enabled parallel execution with the Parallelize attribute.

Timeout attributes

Timeout attributes prevent tests from running indefinitely and help identify performance issues.

TimeoutAttribute

The TimeoutAttribute specifies the maximum time (in milliseconds) a test or fixture method can run. If execution exceeds this time, the test fails.

[TestClass]
public class TimeoutTests
{
    [TestMethod]
    [Timeout(5000)] // 5 seconds
    public void TestWithTimeout()
    {
        // Test must complete within 5 seconds
    }
}

Tip

You can configure a global test timeout through runsettings (TestTimeout) or testconfig.json (timeout.test) without modifying code.

Apply timeout to fixture methods

You can also apply timeouts to initialization and cleanup methods:

[TestClass]
public class FixtureTimeoutTests
{
    [ClassInitialize]
    [Timeout(10000)]
    public static void ClassInit(TestContext context)
    {
        // Must complete within 10 seconds
    }

    [TestInitialize]
    [Timeout(2000)]
    public void TestInit()
    {
        // Must complete within 2 seconds
    }
}

Tip

Every fixture method that accepts a [Timeout] attribute has an equivalent global configuration setting. Configure timeouts globally through runsettings or testconfig.json using settings like TestInitializeTimeout, ClassInitializeTimeout, AssemblyInitializeTimeout, and their cleanup counterparts.

Note

Timeouts aren't guaranteed to be precisely accurate. The test aborts after the specified time passes, but the actual cancellation might take slightly longer.

Cooperative cancellation

By default, MSTest wraps each timed test method in a separate task or thread. When the timeout is reached, the framework stops observing the test, but the underlying task continues running in the background. This behavior can cause problems:

  • The test method continues to access resources and mutate state even after timeout.
  • Background execution can lead to race conditions affecting subsequent tests.
  • Each timed method incurs additional overhead from the task/thread wrapper.

Starting with MSTest 3.6, use the CooperativeCancellation property to avoid these issues. In cooperative mode, MSTest doesn't wrap your test in an extra task. Instead, when the timeout is reached, the framework signals the cancellation token. Your test code is responsible for checking the token regularly and terminating gracefully.

[TestClass]
public class CooperativeTimeoutTests
{
    [TestMethod]
    [Timeout(5000, CooperativeCancellation = true)]
    public async Task TestWithCooperativeCancellation(CancellationToken cancellationToken)
    {
        // Check the token periodically
        while (!cancellationToken.IsCancellationRequested)
        {
            await Task.Delay(100, cancellationToken);
            // Do work
        }
    }

    [TestMethod]
    [Timeout(5000, CooperativeCancellation = true)]
    public void SyncTestWithCooperativeCancellation(CancellationToken cancellationToken)
    {
        // Works with sync methods too
        for (int i = 0; i < 1000; i++)
        {
            cancellationToken.ThrowIfCancellationRequested();
            // Do work
        }
    }
}

Benefits of cooperative cancellation:

  • Lower performance overhead (no extra task/thread wrapper per test).
  • Cleaner resource cleanup since your code handles cancellation explicitly.
  • Aligns with standard .NET cancellation patterns.
  • Deterministic behavior by avoiding race conditions between test code and unobserved background execution.

Note

Cooperative cancellation requires your test code to check the cancellation token regularly. If your code doesn't check the token, the test won't actually stop when timeout is reached.

Tip

You can enable cooperative cancellation globally for all timeout attributes through runsettings or testconfig.json instead of setting it on each attribute individually.

Tip

Related analyzers:

  • MSTEST0045 - recommends using cooperative cancellation for timeout attributes.

Retry attributes

Retry attributes help handle flaky tests by automatically re-running failed tests.

RetryAttribute

The RetryAttribute, introduced in MSTest 3.8, automatically retries test methods that fail or time out. Configure the maximum retry attempts, delay between retries, and backoff strategy.

[TestClass]
public class RetryTests
{
    [TestMethod]
    [Retry(3)] // Retry up to 3 times if the test fails
    public void FlakeyNetworkTest()
    {
        // Test that might occasionally fail due to network issues
    }

    [TestMethod]
    [Retry(3, MillisecondsDelayBetweenRetries = 1000, BackoffType = DelayBackoffType.Exponential)]
    public void TestWithExponentialBackoff()
    {
        // Retries with increasing delays: 1s, 2s, 4s
    }

    [TestMethod]
    [Retry(5, MillisecondsDelayBetweenRetries = 500, BackoffType = DelayBackoffType.Constant)]
    public void TestWithConstantDelay()
    {
        // Retries with constant 500ms delay between attempts
    }
}

Configuration options

Property Description Default
MaxRetryAttempts Maximum number of retry attempts (read-only, set via constructor) Required
MillisecondsDelayBetweenRetries Base delay between retries (in ms) 0
BackoffType Constant or Exponential delay Constant

Note

Only one RetryAttribute can be present on a test method. You can't use RetryAttribute on methods that aren't marked with TestMethod.

Tip

Related analyzers:

  • MSTEST0043 - recommends using RetryAttribute on test methods.

Custom retry implementations

Create custom retry logic by inheriting from RetryBaseAttribute:

public class CustomRetryAttribute : RetryBaseAttribute
{
    private readonly int _maxRetries;

    public CustomRetryAttribute(int maxRetries)
    {
        _maxRetries = maxRetries;
    }

    // Implement abstract members
    // Add custom logic for retry conditions
}

Conditional execution attributes

Conditional execution attributes control whether tests run based on specific conditions like operating system or CI environment.

ConditionBaseAttribute

The ConditionBaseAttribute is the abstract base class for conditional execution. MSTest provides several built-in implementations.

Note

By default, condition attributes aren't inherited. Applying them to a base class doesn't affect derived classes. Custom condition attributes can override this behavior by redefining AttributeUsage, but this isn't recommended to maintain consistency with the built-in condition attributes.

Tip

Related analyzers:

  • MSTEST0041 - recommends using condition-based attributes with test classes.

OSConditionAttribute

The OSConditionAttribute runs or skips tests based on the operating system. Use the OperatingSystems flags enum to specify which operating systems apply.

[TestClass]
public class OSSpecificTests
{
    [TestMethod]
    [OSCondition(OperatingSystems.Windows)]
    public void WindowsOnlyTest()
    {
        // Runs only on Windows
    }

    [TestMethod]
    [OSCondition(OperatingSystems.Linux | OperatingSystems.OSX)]
    public void UnixLikeOnlyTest()
    {
        // Runs on Linux or macOS
    }

    [TestMethod]
    [OSCondition(ConditionMode.Exclude, OperatingSystems.Windows)]
    public void SkipOnWindowsTest()
    {
        // Runs on any OS except Windows
    }
}

Supported operating systems

OS Description
Windows Microsoft Windows
Linux Linux distributions
OSX macOS
FreeBSD FreeBSD

Combine operating systems with the bitwise OR operator (|).

Tip

Related analyzers:

  • MSTEST0061 - recommends using OSCondition attribute instead of runtime checks.

CIConditionAttribute

The CIConditionAttribute runs or skips tests based on whether they're executing in a continuous integration environment.

[TestClass]
public class CIAwareTests
{
    [TestMethod]
    [CICondition] // Default: runs only in CI
    public void CIOnlyTest()
    {
        // Runs only in CI environments
    }

    [TestMethod]
    [CICondition(ConditionMode.Include)]
    public void ExplicitCIOnlyTest()
    {
        // Same as above, explicitly stated
    }

    [TestMethod]
    [CICondition(ConditionMode.Exclude)]
    public void LocalDevelopmentOnlyTest()
    {
        // Skipped in CI, runs during local development
    }
}

IgnoreAttribute

The IgnoreAttribute unconditionally skips a test class or method. Optionally provide a reason for ignoring.

Tip

Related analyzer: MSTEST0015 - Test method should not be ignored. Enable this analyzer to detect tests that are permanently ignored.

[TestClass]
public class IgnoreExamples
{
    [TestMethod]
    [Ignore]
    public void TemporarilyDisabled()
    {
        // This test is skipped
    }

    [TestMethod]
    [Ignore("Waiting for bug #123 to be fixed")]
    public void DisabledWithReason()
    {
        // This test is skipped with a documented reason
    }
}

[TestClass]
[Ignore("Entire class needs refactoring")]
public class IgnoredTestClass
{
    [TestMethod]
    public void Test1() { }  // Skipped

    [TestMethod]
    public void Test2() { }  // Skipped
}

When ignoring tests due to known issues, use WorkItemAttribute or GitHubWorkItemAttribute for traceability:

[TestClass]
public class TrackedIgnoreExamples
{
    [TestMethod]
    [Ignore("Waiting for fix")]
    [WorkItem(12345)]
    public void TestWithWorkItem()
    {
        // Linked to work item 12345
    }

    [TestMethod]
    [Ignore("Known issue")]
    [GitHubWorkItem("https://github.com/owner/repo/issues/42")]
    public void TestWithGitHubIssue()
    {
        // Linked to GitHub issue #42
    }
}

Best practices

  1. Use parallelization wisely: Enable parallelization for independent tests, but use DoNotParallelize for tests that share state.

  2. Set appropriate timeouts: Choose timeouts that allow normal execution but catch stuck tests. Consider slow CI environments.

  3. Prefer cooperative cancellation: Use cooperative cancellation to avoid the overhead of extra task wrappers and prevent background execution of timed-out tests. Enable the MSTEST0045 analyzer to enforce this practice.

  4. Document ignored tests: Always provide a reason and work item reference when ignoring tests.

  5. Use retries sparingly: Address the root cause of flaky tests rather than relying on retries.

  6. Test OS-specific code appropriately: Use OSCondition to run platform-specific tests only where they're applicable.

See also