writing-mstest-tests

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Writing MSTest Tests

编写MSTest测试

Help users write effective, modern unit tests with MSTest 3.x/4.x using current APIs and best practices.
帮助用户使用当前API和最佳实践,借助MSTest 3.x/4.x编写高效、现代化的单元测试。

When to Use

适用场景

  • User wants to write new MSTest unit tests
  • User wants to improve or modernize existing MSTest tests
  • User asks about MSTest assertion APIs, data-driven patterns, or test lifecycle
  • User needs to review MSTest test code for anti-patterns
  • 用户希望编写新的MSTest单元测试
  • 用户希望改进或现代化现有的MSTest测试
  • 用户咨询MSTest断言API、数据驱动模式或测试生命周期相关问题
  • 用户需要评审MSTest测试代码中的反模式

When Not to Use

不适用场景

  • User needs to run or execute tests (use the
    run-tests
    skill)
  • User needs CI/CD pipeline configuration
  • User is using xUnit, NUnit, or TUnit (not MSTest)
  • 用户需要运行或执行测试(请使用
    run-tests
    技能)
  • 用户需要CI/CD流水线配置
  • 用户使用的是xUnit、NUnit或TUnit(而非MSTest)

Inputs

输入项

InputRequiredDescription
Code under testNoThe production code to be tested
Existing test codeNoCurrent tests to review or improve
Test scenario descriptionNoWhat behavior the user wants to test
输入项是否必填描述
被测代码待测试的生产代码
现有测试代码待评审或改进的当前测试代码
测试场景描述用户希望测试的行为说明

Workflow

工作流程

Step 1: Determine project setup

步骤1:确定项目配置

Check the test project for MSTest version and configuration:
  • If using
    MSTest.Sdk
    (
    <Sdk Name="MSTest.Sdk">
    ): modern setup, all features available
  • If using
    MSTest
    metapackage: modern setup (MSTest 3.x+)
  • If using
    MSTest.TestFramework
    +
    MSTest.TestAdapter
    : check version for feature availability
Recommend MSTest.Sdk or the MSTest metapackage for new projects:
xml
<!-- Option 1: MSTest SDK (simplest, recommended for new projects) -->
<Project Sdk="MSTest.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
  </PropertyGroup>
</Project>
When using
MSTest.Sdk
, put the version in
global.json
instead of the project file so all test projects get bumped together:
json
{
  "msbuild-sdks": {
    "MSTest.Sdk": "3.8.2"
  }
}
xml
<!-- Option 2: MSTest metapackage -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="MSTest" Version="3.8.2" />
  </ItemGroup>
</Project>
检查测试项目的MSTest版本和配置:
  • 如果使用
    MSTest.Sdk
    <Sdk Name="MSTest.Sdk">
    ):现代化配置,支持所有功能
  • 如果使用
    MSTest
    元包:现代化配置(MSTest 3.x+)
  • 如果使用
    MSTest.TestFramework
    +
    MSTest.TestAdapter
    :检查版本以确认功能可用性
为新项目推荐使用MSTest.Sdk或MSTest元包:
xml
<!-- 选项1:MSTest SDK(最简单,推荐用于新项目) -->
<Project Sdk="MSTest.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
  </PropertyGroup>
</Project>
使用
MSTest.Sdk
时,将版本配置在
global.json
而非项目文件中,这样所有测试项目可同步更新版本:
json
{
  "msbuild-sdks": {
    "MSTest.Sdk": "3.8.2"
  }
}
xml
<!-- 选项2:MSTest元包 -->
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>net9.0</TargetFramework>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="MSTest" Version="3.8.2" />
  </ItemGroup>
</Project>

Step 2: Write test classes following conventions

步骤2:遵循约定编写测试类

Apply these structural conventions:
  • Seal test classes with
    sealed
    for performance and design clarity
  • Use
    [TestClass]
    on the class and
    [TestMethod]
    on test methods
  • Follow the Arrange-Act-Assert (AAA) pattern
  • Name tests using
    MethodName_Scenario_ExpectedBehavior
  • Use separate test projects with naming convention
    [ProjectName].Tests
csharp
[TestClass]
public sealed class OrderServiceTests
{
    [TestMethod]
    public void CalculateTotal_WithDiscount_ReturnsReducedPrice()
    {
        // Arrange
        var service = new OrderService();
        var order = new Order { Price = 100m, DiscountPercent = 10 };

        // Act
        var total = service.CalculateTotal(order);

        // Assert
        Assert.AreEqual(90m, total);
    }
}
应用以下结构约定:
  • 使用
    sealed
    修饰测试类以提升性能并明确设计意图
  • 在类上使用
    [TestClass]
    特性,在测试方法上使用
    [TestMethod]
    特性
  • 遵循Arrange-Act-Assert(AAA)模式
  • 测试方法命名采用
    MethodName_Scenario_ExpectedBehavior
    格式
  • 使用独立的测试项目,命名遵循
    [ProjectName].Tests
    约定
csharp
[TestClass]
public sealed class OrderServiceTests
{
    [TestMethod]
    public void CalculateTotal_WithDiscount_ReturnsReducedPrice()
    {
        // Arrange(准备)
        var service = new OrderService();
        var order = new Order { Price = 100m, DiscountPercent = 10 };

        // Act(执行)
        var total = service.CalculateTotal(order);

        // Assert(断言)
        Assert.AreEqual(90m, total);
    }
}

Step 3: Use modern assertion APIs

步骤3:使用现代化断言API

Use the correct assertion for each scenario. Prefer
Assert
class methods over
StringAssert
or
CollectionAssert
where both exist.
针对不同场景选择正确的断言方式。当
Assert
类方法与
StringAssert
CollectionAssert
功能重叠时,优先使用
Assert
类方法。

Equality and null checks

相等性与空值检查

csharp
Assert.AreEqual(expected, actual);      // Value equality
Assert.AreSame(expected, actual);       // Reference equality
Assert.IsNull(value);
Assert.IsNotNull(value);
csharp
Assert.AreEqual(expected, actual);      // 值相等
Assert.AreSame(expected, actual);       // 引用相等
Assert.IsNull(value);
Assert.IsNotNull(value);

Exception testing — use
Assert.Throws
instead of
[ExpectedException]

异常测试 — 使用
Assert.Throws
替代
[ExpectedException]

csharp
// Synchronous
var ex = Assert.ThrowsExactly<ArgumentNullException>(() => service.Process(null));
Assert.AreEqual("input", ex.ParamName);

// Async
var ex = await Assert.ThrowsExactlyAsync<InvalidOperationException>(
    async () => await service.ProcessAsync(null));
  • Assert.Throws<T>
    matches
    T
    or any derived type
  • Assert.ThrowsExactly<T>
    matches only the exact type
    T
csharp
// 同步方法
var ex = Assert.ThrowsExactly<ArgumentNullException>(() => service.Process(null));
Assert.AreEqual("input", ex.ParamName);

// 异步方法
var ex = await Assert.ThrowsExactlyAsync<InvalidOperationException>(
    async () => await service.ProcessAsync(null));
  • Assert.Throws<T>
    匹配
    T
    或其任何派生类型
  • Assert.ThrowsExactly<T>
    仅匹配确切的
    T
    类型

Collection assertions

集合断言

csharp
Assert.Contains(expectedItem, collection);
Assert.DoesNotContain(unexpectedItem, collection);
var single = Assert.ContainsSingle(collection);  // Returns the single element
Assert.HasCount(3, collection);
Assert.IsEmpty(collection);
csharp
Assert.Contains(expectedItem, collection);
Assert.DoesNotContain(unexpectedItem, collection);
var single = Assert.ContainsSingle(collection);  // 返回集合中的唯一元素
Assert.HasCount(3, collection);
Assert.IsEmpty(collection);

String assertions

字符串断言

csharp
Assert.Contains("expected", actualString);
Assert.StartsWith("prefix", actualString);
Assert.EndsWith("suffix", actualString);
Assert.MatchesRegex(@"\d{3}-\d{4}", phoneNumber);
csharp
Assert.Contains("expected", actualString);
Assert.StartsWith("prefix", actualString);
Assert.EndsWith("suffix", actualString);
Assert.MatchesRegex(@"\d{3}-\d{4}", phoneNumber);

Type assertions

类型断言

csharp
// MSTest 3.x — out parameter
Assert.IsInstanceOfType<MyHandler>(result, out var typed);
typed.Handle();

// MSTest 4.x — returns directly
var typed = Assert.IsInstanceOfType<MyHandler>(result);
csharp
// MSTest 3.x — out参数
Assert.IsInstanceOfType<MyHandler>(result, out var typed);
typed.Handle();

// MSTest 4.x — 直接返回
var typed = Assert.IsInstanceOfType<MyHandler>(result);

Comparison assertions

比较断言

csharp
Assert.IsGreaterThan(lowerBound, actual);
Assert.IsLessThan(upperBound, actual);
Assert.IsInRange(actual, low, high);
csharp
Assert.IsGreaterThan(lowerBound, actual);
Assert.IsLessThan(upperBound, actual);
Assert.IsInRange(actual, low, high);

Step 4: Use data-driven tests for multiple inputs

步骤4:使用数据驱动测试处理多输入场景

DataRow for inline values

内联值使用DataRow

csharp
[TestMethod]
[DataRow(1, 2, 3)]
[DataRow(0, 0, 0, DisplayName = "Zeros")]
[DataRow(-1, 1, 0)]
public void Add_ReturnsExpectedSum(int a, int b, int expected)
{
    Assert.AreEqual(expected, Calculator.Add(a, b));
}
csharp
[TestMethod]
[DataRow(1, 2, 3)]
[DataRow(0, 0, 0, DisplayName = "Zeros")]
[DataRow(-1, 1, 0)]
public void Add_ReturnsExpectedSum(int a, int b, int expected)
{
    Assert.AreEqual(expected, Calculator.Add(a, b));
}

DynamicData with ValueTuples (preferred for complex data)

复杂数据优先使用结合ValueTuples的DynamicData

Prefer
ValueTuple
return types over
IEnumerable<object[]>
for type safety:
csharp
[TestMethod]
[DynamicData(nameof(DiscountTestData))]
public void ApplyDiscount_ReturnsExpectedPrice(decimal price, int percent, decimal expected)
{
    var result = PriceCalculator.ApplyDiscount(price, percent);
    Assert.AreEqual(expected, result);
}

// ValueTuple — preferred (MSTest 3.7+)
public static IEnumerable<(decimal price, int percent, decimal expected)> DiscountTestData =>
[
    (100m, 10, 90m),
    (200m, 25, 150m),
    (50m, 0, 50m),
];
When you need metadata per test case, use
TestDataRow<T>
:
csharp
public static IEnumerable<TestDataRow<(decimal price, int percent, decimal expected)>> DiscountTestDataWithMetadata =>
[
    new((100m, 10, 90m)) { DisplayName = "10% discount" },
    new((200m, 25, 150m)) { DisplayName = "25% discount" },
    new((50m, 0, 50m)) { DisplayName = "No discount" },
];
为保证类型安全,优先使用
ValueTuple
返回类型而非
IEnumerable<object[]>
csharp
[TestMethod]
[DynamicData(nameof(DiscountTestData))]
public void ApplyDiscount_ReturnsExpectedPrice(decimal price, int percent, decimal expected)
{
    var result = PriceCalculator.ApplyDiscount(price, percent);
    Assert.AreEqual(expected, result);
}

// ValueTuple — 优先选择(MSTest 3.7+)
public static IEnumerable<(decimal price, int percent, decimal expected)> DiscountTestData =>
[
    (100m, 10, 90m),
    (200m, 25, 150m),
    (50m, 0, 50m),
];
当需要为每个测试用例添加元数据时,使用
TestDataRow<T>
csharp
public static IEnumerable<TestDataRow<(decimal price, int percent, decimal expected)>> DiscountTestDataWithMetadata =>
[
    new((100m, 10, 90m)) { DisplayName = "10% discount" },
    new((200m, 25, 150m)) { DisplayName = "25% discount" },
    new((50m, 0, 50m)) { DisplayName = "No discount" },
];

Step 5: Handle test lifecycle correctly

步骤5:正确处理测试生命周期

  • Always initialize in the constructor — this enables
    readonly
    fields and works correctly with nullability analyzers (fields are guaranteed non-null after construction)
  • Use
    [TestInitialize]
    only for async initialization, combined with the constructor for sync parts
  • Use
    [TestCleanup]
    for cleanup that must run even on failure
  • Inject
    TestContext
    via constructor (MSTest 3.6+)
csharp
[TestClass]
public sealed class RepositoryTests
{
    private readonly TestContext _testContext;
    private readonly FakeDatabase _db;  // readonly — guaranteed by constructor

    public RepositoryTests(TestContext testContext)
    {
        _testContext = testContext;
        _db = new FakeDatabase();  // sync init in ctor
    }

    [TestInitialize]
    public async Task InitAsync()
    {
        // Use TestInitialize ONLY for async setup
        await _db.SeedAsync();
    }

    [TestCleanup]
    public void Cleanup() => _db.Reset();
}
  • 始终在构造函数中初始化:这支持
    readonly
    字段,且能与空值分析器正确协作(构造完成后字段保证非空)
  • 仅在需要异步初始化时使用
    [TestInitialize]
    ,同步初始化部分仍放在构造函数中
  • 使用
    [TestCleanup]
    执行即使测试失败也必须运行的清理操作
  • 通过构造函数注入
    TestContext
    (MSTest 3.6+)
csharp
[TestClass]
public sealed class RepositoryTests
{
    private readonly TestContext _testContext;
    private readonly FakeDatabase _db;  // readonly — 由构造函数保证非空

    public RepositoryTests(TestContext testContext)
    {
        _testContext = testContext;
        _db = new FakeDatabase();  // 同步初始化放在构造函数中
    }

    [TestInitialize]
    public async Task InitAsync()
    {
        // 仅在需要异步设置时使用TestInitialize
        await _db.SeedAsync();
    }

    [TestCleanup]
    public void Cleanup() => _db.Reset();
}

Execution order

执行顺序

  1. [AssemblyInitialize]
    — once per assembly
  2. [ClassInitialize]
    — once per class
  3. Per test:
    • With
      TestContext
      property injection: Constructor → set
      TestContext
      property →
      [TestInitialize]
    • With constructor injection of
      TestContext
      : Constructor (receives
      TestContext
      ) →
      [TestInitialize]
  4. Test method
  5. [TestCleanup]
    DisposeAsync
    Dispose
    — per test
  6. [ClassCleanup]
    — once per class
  7. [AssemblyCleanup]
    — once per assembly
  1. [AssemblyInitialize]
    — 每个程序集执行一次
  2. [ClassInitialize]
    — 每个类执行一次
  3. 每个测试流程:
    • 采用TestContext属性注入:构造函数 → 设置TestContext属性 →
      [TestInitialize]
    • 采用TestContext构造函数注入:构造函数(接收TestContext) →
      [TestInitialize]
  4. 测试方法
  5. [TestCleanup]
    DisposeAsync
    Dispose
    — 每个测试执行一次
  6. [ClassCleanup]
    — 每个类执行一次
  7. [AssemblyCleanup]
    — 每个程序集执行一次

Step 6: Apply cancellation and timeout patterns

步骤6:应用取消与超时模式

Always use
TestContext.CancellationToken
with
[Timeout]
:
csharp
[TestMethod]
[Timeout(5000)]
public async Task FetchData_ReturnsWithinTimeout()
{
    var result = await _client.GetDataAsync(_testContext.CancellationToken);
    Assert.IsNotNull(result);
}
始终结合
[Timeout]
使用
TestContext.CancellationToken
csharp
[TestMethod]
[Timeout(5000)]
public async Task FetchData_ReturnsWithinTimeout()
{
    var result = await _client.GetDataAsync(_testContext.CancellationToken);
    Assert.IsNotNull(result);
}

Step 7: Use advanced features where appropriate

步骤7:合理使用高级功能

Retry flaky tests (MSTest 3.9+)

重试不稳定测试(MSTest 3.9+)

csharp
[TestMethod]
[Retry(3)]
public void ExternalService_EventuallyResponds() { }
csharp
[TestMethod]
[Retry(3)]
public void ExternalService_EventuallyResponds() { }

Conditional execution (MSTest 3.10+)

条件执行(MSTest 3.10+)

csharp
[TestMethod]
[OSCondition(OperatingSystems.Windows)]
public void WindowsRegistry_ReadsValue() { }

[TestMethod]
[CICondition(ConditionMode.Exclude)]
public void LocalOnly_InteractiveTest() { }
csharp
[TestMethod]
[OSCondition(OperatingSystems.Windows)]
public void WindowsRegistry_ReadsValue() { }

[TestMethod]
[CICondition(ConditionMode.Exclude)]
public void LocalOnly_InteractiveTest() { }

Parallelization

并行化

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

[TestClass]
[DoNotParallelize]  // Opt out specific classes
public sealed class DatabaseIntegrationTests { }
csharp
[assembly: Parallelize(Workers = 4, Scope = ExecutionScope.MethodLevel)]

[TestClass]
[DoNotParallelize]  // 特定类退出并行
public sealed class DatabaseIntegrationTests { }

Common Pitfalls

常见陷阱

PitfallSolution
Assert.AreEqual(actual, expected)
— swapped arguments
Always put expected first:
Assert.AreEqual(expected, actual)
[ExpectedException]
— obsolete, cannot assert message
Use
Assert.Throws<T>
or
Assert.ThrowsExactly<T>
items.Single()
— unclear exception on failure
Use
Assert.ContainsSingle(items)
for better failure messages
Hard cast
(MyType)result
— unclear exception
Use
Assert.IsInstanceOfType<MyType>(result)
IEnumerable<object[]>
for DynamicData
Use
IEnumerable<(T1, T2, ...)>
ValueTuples for type safety
Sync setup in
[TestInitialize]
Initialize in the constructor instead — enables
readonly
fields and satisfies nullability analyzers
CancellationToken.None
in async tests
Use
TestContext.CancellationToken
for cooperative timeout
public TestContext? TestContext { get; set; }
Drop the
?
— MSTest suppresses CS8618 for this property
TestContext TestContext { get; set; } = null!
Remove
= null!
— unnecessary, MSTest handles assignment
Non-sealed test classesSeal test classes by default for performance
陷阱解决方案
Assert.AreEqual(actual, expected)
— 参数顺序颠倒
始终将预期值放在前面:
Assert.AreEqual(expected, actual)
[ExpectedException]
— 已过时,无法断言异常消息
使用
Assert.Throws<T>
Assert.ThrowsExactly<T>
items.Single()
— 失败时异常信息不清晰
使用
Assert.ContainsSingle(items)
以获得更明确的失败消息
硬转换
(MyType)result
— 失败时异常信息不清晰
使用
Assert.IsInstanceOfType<MyType>(result)
DynamicData使用
IEnumerable<object[]>
使用
IEnumerable<(T1, T2, ...)>
ValueTuples保证类型安全
[TestInitialize]
中执行同步初始化
改为在构造函数中初始化 — 支持
readonly
字段并满足空值分析器要求
异步测试中使用
CancellationToken.None
使用
TestContext.CancellationToken
实现协作式超时
public TestContext? TestContext { get; set; }
移除
?
— MSTest会抑制CS8618警告
TestContext TestContext { get; set; } = null!
移除
= null!
— 无需手动赋值,MSTest会处理
测试类未密封默认密封测试类以提升性能