Note
Access to this page requires authorization. You can try signing in or changing directories.
Access to this page requires authorization. You can try changing directories.
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):
- Assembly-level: Runs once when the test assembly loads and unloads
- Class-level: Runs once per test class
- Global test-level: Runs before and after every test method in the assembly
- 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, orValueTask(MSTest v3.3+) AssemblyInitializerequires oneTestContextparameterAssemblyCleanupaccepts zero parameters, or oneTestContextparameter (MSTest 3.8+)- Only one of each attribute allowed per assembly
- Must be in a class marked with
[TestClass]
Tip
Related analyzers:
- MSTEST0012 - validates
AssemblyInitializesignature. - MSTEST0013 - validates
AssemblyCleanupsignature.
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, orValueTask(MSTest v3.3+) ClassInitializerequires oneTestContextparameterClassCleanupaccepts zero parameters, or oneTestContextparameter (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
ClassInitializesignature. - MSTEST0011 - validates
ClassCleanupsignature. - 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, orValueTask - Must have exactly one
TestContextparameter - 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):
TestCleanup(derived class)TestCleanup(base class)DisposeAsync(if implemented)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
- Create instance of test class (constructor)
- Set
TestContextproperty (if present) - Run
GlobalTestInitializemethods - Run
TestInitializemethods (base to derived) - Execute test method
- Update
TestContextwith results (for example,Outcomeproperty) - Run
TestCleanupmethods (derived to base) - Run
GlobalTestCleanupmethods - Run
DisposeAsync(if implemented) - Run
Dispose(if implemented)
Tip
Related analyzers:
- MSTEST0008 - validates
TestInitializesignature. - MSTEST0009 - validates
TestCleanupsignature. - MSTEST0063 - validates test class constructor.
Best practices
Use appropriate scope: Put setup at the highest level that makes sense to avoid redundant work.
Keep setup fast: Long-running setup affects all tests. Consider lazy initialization for expensive resources.
Clean up properly: Always clean up resources to prevent test interference and memory leaks.
Handle async correctly: Use
async Taskreturn types, notasync void, for async lifecycle methods.Consider test isolation: Each test should be independent. Avoid shared mutable state between tests.
Use GlobalTest sparingly: Global lifecycle methods run for every test, so keep them lightweight.