MSTest 为测试类和测试方法提供了定义完善的生命周期,使你可以在测试执行的各个阶段执行设置和拆解作。 了解生命周期有助于编写高效的测试并避免常见的陷阱。
生命周期概述
生命周期分为四个阶段,从最高级别(程序集)执行到最低级别(测试方法):
- 程序集级别:在测试程序集加载和卸载时运行一次
- 类级别:每个测试类运行一次
- 全局测试级别:在程序集中的每个测试方法之前和之后运行
- 测试级别:针对每个测试方法运行(包括参数化测试中的每个数据行)
程序集级生命周期
当测试程序集加载和卸载时,程序集生命周期方法运行一次。 将这些设置用于昂贵的一次性设置,例如数据库初始化或服务启动。
AssemblyInitialize 和 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!);
}
}
要求
- 方法必须是
public static - 返回类型:
void、Task或ValueTask(MSTest v3.3+) -
AssemblyInitialize需要一个TestContext参数 -
AssemblyCleanup接受零个参数或一个TestContext参数(MSTest 3.8+) - 每个程序集仅允许一个属性
- 必须位于标记为
[TestClass]的类中
类级生命周期
类生命周期方法在每个测试类中运行一次,在该类中的所有测试方法之前和之后运行一次。 使用这些设置可在类中的测试之间共享。
ClassInitialize 和 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);
}
}
要求
- 方法必须是
public static - 返回类型:
void、Task或ValueTask(MSTest v3.3+) -
ClassInitialize需要一个TestContext参数 -
ClassCleanup接受零个参数或一个TestContext参数(MSTest 3.8+) - 每个类只允许每个属性之一
继承行为
控制 ClassInitialize 是否对派生类使用 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
}
}
| 继承行为 | Description |
|---|---|
None(默认值) |
仅为声明类专门进行初始化运行 |
BeforeEachDerivedClass |
在每个派生类之前初始化运行 |
小窍门
相关分析器:
-
MSTEST0010 - 验证
ClassInitialize签名。 -
MSTEST0011 - 验证
ClassCleanup签名。 -
MSTEST0034 - 建议使用
ClassCleanupBehavior.EndOfClass。
全局测试级生命周期
注释
MSTest 3.10.0 中引入了全局测试生命周期属性。
全局测试生命周期方法在整个程序集 的每个 测试方法之前和之后运行,而无需向每个测试类添加代码。
GlobalTestInitialize 和 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}");
}
}
要求
- 方法必须是
public static - 返回类型:
void、或TaskValueTask - 必须只有一个
TestContext参数 - 必须位于标记为
的类中 - 允许跨程序集使用具有这些属性的多个方法
注释
如果存在多个 GlobalTestInitialize 或 GlobalTestCleanup 方法,则不能保证执行顺序。 方法中的TimeoutAttribute不受支持GlobalTestInitialize。
小窍门
相关分析器: MSTEST0050 - 验证全局测试装置方法。
测试阶段生命周期
每个测试方法都运行测试级生命周期。 对于参数化测试,生命周期针对每个数据行运行。
安装阶段
使用 TestInitialize 或构造函数进行每次测试的设置:
[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);
}
}
构造函数与 TestInitialize:
| 方面 | 构造函数 | TestInitialize |
|---|---|---|
| 异步支持 | 否 | 是的 |
| 超时支持 | 否 | 是(具有 [Timeout] 属性) |
| 执行顺序 | First | 构造函数之后 |
| 继承 | 基础然后派生 | 基类然后派生类 |
| 异常行为 | 清理和处理操作不会运行(因为没有实例存在) | 清理和处理仍然在运行 |
小窍门
我应该使用哪种方法? 构造函数通常是首选的,因为它们允许你使用 readonly 字段,从而强制实施不可变性,并让你的测试类更易于理解和分析。 需要异步初始化或超时支持时使用 TestInitialize 。
还可以结合这两种方法:使用构造函数对 readonly 字段进行简单的同步初始化,然后使用 TestInitialize 进行依赖于这些字段的额外异步设置。
可以选择性地启用代码分析器以强制实施一致的方法:
- MSTEST0019 - 首选 TestInitialize 方法而不是构造函数
- MSTEST0020 - 首选构造函数而不是 TestInitialize 方法
执行阶段
测试方法在安装完成后执行。 对于 async 测试方法,MSTest 等待返回的 Task 或 ValueTask。
警告
默认情况下,异步测试方法没有 SynchronizationContext 。 这不适用于在 UWP 和 WinUI 中 UI 线程上运行的 UITestMethod 测试。
清理阶段
使用 TestCleanup 或 IDisposable/IAsyncDisposable 来进行每次测试后的清理:
[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);
}
}
清理执行顺序(从派生类到基类):
-
TestCleanup(派生类) -
TestCleanup(基类) -
DisposeAsync(如果实现) -
Dispose(如果实现)
小窍门
可以选择性地启用代码分析器以强制实施一致的清理方法:
- MSTEST0021 - 首选 Dispose 而不是 TestCleanup 方法
- MSTEST0022 - 首选 TestCleanup 而不是 Dispose 方法
如果启用了非 MSTest 分析器(如 .NET 代码分析规则),则可能会看到 CA1001 建议在测试类拥有可释放资源时实现释放模式。 这是预期行为,应遵循分析器的指南。
完成测试层级顺序
- 创建测试类的实例(构造函数)
- 设置
TestContext属性(如果存在) - 运行
GlobalTestInitialize方法 - 执行
TestInitialize方法(从基类到派生类) - 执行测试方法
- 更新
TestContext为结果值(例如Outcome属性) - 运行
TestCleanup方法(派生到基) - 运行
GlobalTestCleanup方法 - 运行
DisposeAsync(如果已实现) - 运行
Dispose(如果已实现)
小窍门
相关分析器:
-
MSTEST0008 - 验证
TestInitialize签名。 -
MSTEST0009 - 验证
TestCleanup签名。 - MSTEST0063 - 验证测试类构造函数。
最佳做法
使用适当的范围:将设置放在最高级别,以避免冗余工作。
使设置保持快速:长时间运行的安装程序会影响所有测试。 考虑延迟初始化昂贵的资源。
正确清理:始终清理资源,以防止测试干扰和内存泄漏。
正确处理异步:对于异步生命周期方法,使用
async Task返回类型,而无需async void返回类型。考虑测试隔离:每个测试都应是独立的。 避免在测试之间共享可变状态。
请谨慎使用 GlobalTest:全局生命周期方法会运行于每个测试上,因此应保持简洁。