通过


MSTest 生命周期

MSTest 为测试类和测试方法提供了定义完善的生命周期,使你可以在测试执行的各个阶段执行设置和拆解作。 了解生命周期有助于编写高效的测试并避免常见的陷阱。

生命周期概述

生命周期分为四个阶段,从最高级别(程序集)执行到最低级别(测试方法):

  1. 程序集级别:在测试程序集加载和卸载时运行一次
  2. 类级别:每个测试类运行一次
  3. 全局测试级别:在程序集中的每个测试方法之前和之后运行
  4. 测试级别:针对每个测试方法运行(包括参数化测试中的每个数据行)

程序集级生命周期

当测试程序集加载和卸载时,程序集生命周期方法运行一次。 将这些设置用于昂贵的一次性设置,例如数据库初始化或服务启动。

AssemblyInitializeAssemblyCleanup

[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
  • 返回类型: voidTaskValueTask (MSTest v3.3+)
  • AssemblyInitialize 需要一个 TestContext 参数
  • AssemblyCleanup 接受零个参数或一个 TestContext 参数(MSTest 3.8+)
  • 每个程序集仅允许一个属性
  • 必须位于标记为 [TestClass] 的类中

小窍门

相关分析器:

类级生命周期

类生命周期方法在每个测试类中运行一次,在该类中的所有测试方法之前和之后运行一次。 使用这些设置可在类中的测试之间共享。

ClassInitializeClassCleanup

[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
  • 返回类型: voidTaskValueTask (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 中引入了全局测试生命周期属性。

全局测试生命周期方法在整个程序集 的每个 测试方法之前和之后运行,而无需向每个测试类添加代码。

GlobalTestInitializeGlobalTestCleanup

[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 参数
  • 必须位于标记为 的类中
  • 允许跨程序集使用具有这些属性的多个方法

注释

如果存在多个 GlobalTestInitializeGlobalTestCleanup 方法,则不能保证执行顺序。 方法中的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 等待返回的 TaskValueTask

警告

默认情况下,异步测试方法没有 SynchronizationContext 。 这不适用于在 UWP 和 WinUI 中 UI 线程上运行的 UITestMethod 测试。

清理阶段

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

清理执行顺序(从派生类到基类):

  1. TestCleanup (派生类)
  2. TestCleanup (基类)
  3. DisposeAsync (如果实现)
  4. Dispose (如果实现)

小窍门

可以选择性地启用代码分析器以强制实施一致的清理方法:

  • MSTEST0021 - 首选 Dispose 而不是 TestCleanup 方法
  • MSTEST0022 - 首选 TestCleanup 而不是 Dispose 方法

如果启用了非 MSTest 分析器(如 .NET 代码分析规则),则可能会看到 CA1001 建议在测试类拥有可释放资源时实现释放模式。 这是预期行为,应遵循分析器的指南。

完成测试层级顺序

  1. 创建测试类的实例(构造函数)
  2. 设置TestContext属性(如果存在)
  3. 运行 GlobalTestInitialize 方法
  4. 执行 TestInitialize 方法(从基类到派生类)
  5. 执行测试方法
  6. 更新 TestContext 为结果值(例如 Outcome 属性)
  7. 运行 TestCleanup 方法(派生到基)
  8. 运行 GlobalTestCleanup 方法
  9. 运行 DisposeAsync (如果已实现)
  10. 运行 Dispose (如果已实现)

小窍门

相关分析器:

最佳做法

  1. 使用适当的范围:将设置放在最高级别,以避免冗余工作。

  2. 使设置保持快速:长时间运行的安装程序会影响所有测试。 考虑延迟初始化昂贵的资源。

  3. 正确清理:始终清理资源,以防止测试干扰和内存泄漏。

  4. 正确处理异步:对于异步生命周期方法,使用 async Task 返回类型,而无需 async void 返回类型。

  5. 考虑测试隔离:每个测试都应是独立的。 避免在测试之间共享可变状态。

  6. 请谨慎使用 GlobalTest:全局生命周期方法会运行于每个测试上,因此应保持简洁。

另请参阅