Edit

Share via


MSTest lifecycle

MSTest provides a well-defined lifecycle for test classes and test methods, allowing you to perform setup and teardown operations at various stages of test execution. Understanding the lifecycle helps you write efficient tests and avoid common pitfalls.

Lifecycle overview

The lifecycle groups into four stages, executed from highest level (assembly) to lowest level (test method):

  1. Assembly-level: Runs once when the test assembly loads and unloads
  2. Class-level: Runs once per test class
  3. Global test-level: Runs before and after every test method in the assembly
  4. Test-level: Runs for each test method (including each data row in parameterized tests)

Assembly-level lifecycle

Assembly lifecycle methods run once when the test assembly loads and unloads. Use these for expensive one-time setup like database initialization or service startup.

AssemblyInitialize and AssemblyCleanup

[TestClass]
public class AssemblyLifecycleExample
{
    private static IHost? _host;

    [AssemblyInitialize]
    public static async Task AssemblyInit(TestContext context)
    {
        // Runs once before any tests in the assembly
        _host = await StartTestServerAsync();
        context.WriteLine("Test server started");
    }

    [AssemblyCleanup]
    public static async Task AssemblyCleanup(TestContext context)
    {
        // Runs once after all tests complete
        // TestContext parameter available in MSTest 3.8+
        if (_host != null)
        {
            await _host.StopAsync();
        }
    }

    private static Task<IHost> StartTestServerAsync()
    {
        // Server initialization
        return Task.FromResult<IHost>(null!);
    }
}

Requirements

  • Methods must be public static
  • Return type: void, Task, or ValueTask (MSTest v3.3+)
  • AssemblyInitialize requires one TestContext parameter
  • AssemblyCleanup accepts zero parameters, or one TestContext parameter (MSTest 3.8+)
  • Only one of each attribute allowed per assembly
  • Must be in a class marked with [TestClass]

Tip

Related analyzers:

  • MSTEST0012 - validates AssemblyInitialize signature.
  • MSTEST0013 - validates AssemblyCleanup signature.

Class-level lifecycle

Class lifecycle methods run once per test class, before and after all test methods in that class. Use these for setup shared across tests in a class.

ClassInitialize and ClassCleanup

[TestClass]
public class ClassLifecycleExample
{
    private static HttpClient? _client;

    [ClassInitialize]
    public static void ClassInit(TestContext context)
    {
        // Runs once before any tests in this class
        _client = new HttpClient
        {
            BaseAddress = new Uri("https://api.example.com")
        };
    }

    [ClassCleanup]
    public static void ClassCleanup()
    {
        // Runs after all tests in this class complete
        _client?.Dispose();
    }

    [TestMethod]
    public async Task GetUsers_ReturnsSuccess()
    {
        var response = await _client!.GetAsync("/users");
        Assert.IsTrue(response.IsSuccessStatusCode);
    }
}

Requirements

  • Methods must be public static
  • Return type: void, Task, or ValueTask (MSTest v3.3+)
  • ClassInitialize requires one TestContext parameter
  • ClassCleanup accepts zero parameters, or one TestContext parameter (MSTest 3.8+)
  • Only one of each attribute allowed per class

Inheritance behavior

Control whether ClassInitialize runs for derived classes using InheritanceBehavior:

[TestClass]
public class BaseTestClass
{
    [ClassInitialize(InheritanceBehavior.BeforeEachDerivedClass)]
    public static void BaseClassInit(TestContext context)
    {
        // Runs before each derived class's tests
    }
}

[TestClass]
public class DerivedTestClass : BaseTestClass
{
    [TestMethod]
    public void DerivedTest()
    {
        // BaseClassInit runs before this class's tests
    }
}
InheritanceBehavior Description
None (default) Initialize runs only for the declaring class
BeforeEachDerivedClass Initialize runs before each derived class

Tip

Related analyzers:

  • MSTEST0010 - validates ClassInitialize signature.
  • MSTEST0011 - validates ClassCleanup signature.
  • MSTEST0034 - recommends using ClassCleanupBehavior.EndOfClass.

Global test-level lifecycle

Note

Global test lifecycle attributes were introduced in MSTest 3.10.0.

Global test lifecycle methods run before and after every test method across the entire assembly, without needing to add code to each test class.

GlobalTestInitialize and GlobalTestCleanup

[TestClass]
public class GlobalTestLifecycleExample
{
    [GlobalTestInitialize]
    public static void GlobalTestInit(TestContext context)
    {
        // Runs before every test method in the assembly
        context.WriteLine($"Starting test: {context.TestName}");
    }

    [GlobalTestCleanup]
    public static void GlobalTestCleanup(TestContext context)
    {
        // Runs after every test method in the assembly
        context.WriteLine($"Finished test: {context.TestName}");
    }
}

Requirements

  • Methods must be public static
  • Return type: void, Task, or ValueTask
  • Must have exactly one TestContext parameter
  • Must be in a class marked with [TestClass]
  • Multiple methods with these attributes are allowed across the assembly

Note

When multiple GlobalTestInitialize or GlobalTestCleanup methods exist, the execution order isn't guaranteed. The TimeoutAttribute isn't supported on GlobalTestInitialize methods.

Tip

Related analyzer: MSTEST0050 - validates global test fixture methods.

Test-level lifecycle

Test-level lifecycle runs for every test method. For parameterized tests, the lifecycle runs for each data row.

Setup phase

Use TestInitialize or a constructor for per-test setup:

[TestClass]
public class TestLevelSetupExample
{
    private Calculator? _calculator;

    public TestLevelSetupExample()
    {
        // Constructor runs before TestInitialize
        // Use for simple synchronous initialization
    }

    [TestInitialize]
    public async Task TestInit()
    {
        // Runs before each test method
        // Supports async, attributes like Timeout
        _calculator = new Calculator();
        await _calculator.InitializeAsync();
    }

    [TestMethod]
    public void Add_TwoNumbers_ReturnsSum()
    {
        var result = _calculator!.Add(2, 3);
        Assert.AreEqual(5, result);
    }
}

Constructor vs. TestInitialize:

Aspect Constructor TestInitialize
Async support No Yes
Timeout support No Yes (with [Timeout] attribute)
Execution order First After constructor
Inheritance Base then derived Base then derived
Exception behavior Cleanup and Dispose don't run (no instance exists) Cleanup and Dispose still run

Tip

Which approach should I use? Constructors are generally preferred because they allow you to use readonly fields, which enforces immutability and makes your test class easier to reason about. Use TestInitialize when you need async initialization or timeout support.

You can also combine both approaches: use the constructor for simple synchronous initialization of readonly fields, and TestInitialize for additional async setup that depends on those fields.

You can optionally enable code analyzers to enforce a consistent approach:

  • MSTEST0019 - Prefer TestInitialize methods over constructors
  • MSTEST0020 - Prefer constructors over TestInitialize methods

Execution phase

The test method executes after setup completes. For async test methods, MSTest awaits the returned Task or ValueTask.

Warning

Asynchronous test methods don't have a SynchronizationContext by default. This doesn't apply to UITestMethod tests in UWP and WinUI, which run on the UI thread.

Cleanup phase

Use TestCleanup or IDisposable/IAsyncDisposable for per-test cleanup:

[TestClass]
public class TestLevelCleanupExample
{
    private HttpClient? _client;

    [TestInitialize]
    public void TestInit()
    {
        _client = new HttpClient();
    }

    [TestCleanup]
    public void TestCleanup()
    {
        if (_client != null)
        {
            _client.Dispose();
        }
    }

    [TestMethod]
    public async Task GetData_ReturnsSuccess()
    {
        var response = await _client!.GetAsync("https://example.com");
        Assert.IsTrue(response.IsSuccessStatusCode);
    }
}

Cleanup execution order (derived to base):

  1. TestCleanup (derived class)
  2. TestCleanup (base class)
  3. DisposeAsync (if implemented)
  4. Dispose (if implemented)

Tip

You can optionally enable code analyzers to enforce a consistent cleanup approach:

  • MSTEST0021 - Prefer Dispose over TestCleanup methods
  • MSTEST0022 - Prefer TestCleanup over Dispose methods

If you have non-MSTest analyzers enabled, such as .NET code analysis rules, you might see CA1001 suggesting that you implement the dispose pattern when your test class owns disposable resources. This is expected behavior and you should follow the analyzer's guidance.

Complete test-level order

  1. Create instance of test class (constructor)
  2. Set TestContext property (if present)
  3. Run GlobalTestInitialize methods
  4. Run TestInitialize methods (base to derived)
  5. Execute test method
  6. Update TestContext with results (for example, Outcome property)
  7. Run TestCleanup methods (derived to base)
  8. Run GlobalTestCleanup methods
  9. Run DisposeAsync (if implemented)
  10. Run Dispose (if implemented)

Tip

Related analyzers:

Best practices

  1. Use appropriate scope: Put setup at the highest level that makes sense to avoid redundant work.

  2. Keep setup fast: Long-running setup affects all tests. Consider lazy initialization for expensive resources.

  3. Clean up properly: Always clean up resources to prevent test interference and memory leaks.

  4. Handle async correctly: Use async Task return types, not async void, for async lifecycle methods.

  5. Consider test isolation: Each test should be independent. Avoid shared mutable state between tests.

  6. Use GlobalTest sparingly: Global lifecycle methods run for every test, so keep them lightweight.

See also