writing-mstest-tests
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseWriting 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 skill)
run-tests - User needs CI/CD pipeline configuration
- User is using xUnit, NUnit, or TUnit (not MSTest)
- 用户需要运行或执行测试(请使用技能)
run-tests - 用户需要CI/CD流水线配置
- 用户使用的是xUnit、NUnit或TUnit(而非MSTest)
Inputs
输入项
| Input | Required | Description |
|---|---|---|
| Code under test | No | The production code to be tested |
| Existing test code | No | Current tests to review or improve |
| Test scenario description | No | What 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): modern setup, all features available<Sdk Name="MSTest.Sdk"> - If using metapackage: modern setup (MSTest 3.x+)
MSTest - If using +
MSTest.TestFramework: check version for feature availabilityMSTest.TestAdapter
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 , put the version in instead of the project file so all test projects get bumped together:
MSTest.Sdkglobal.jsonjson
{
"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 3.x+)
MSTest - 如果使用+
MSTest.TestFramework:检查版本以确认功能可用性MSTest.TestAdapter
为新项目推荐使用MSTest.Sdk或MSTest元包:
xml
<!-- 选项1:MSTest SDK(最简单,推荐用于新项目) -->
<Project Sdk="MSTest.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
</Project>使用时,将版本配置在而非项目文件中,这样所有测试项目可同步更新版本:
MSTest.Sdkglobal.jsonjson
{
"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 for performance and design clarity
sealed - Use on the class and
[TestClass]on test methods[TestMethod] - 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 class methods over or where both exist.
AssertStringAssertCollectionAssert针对不同场景选择正确的断言方式。当类方法与或功能重叠时,优先使用类方法。
AssertStringAssertCollectionAssertAssertEquality 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]异常测试 — 使用Assert.Throws
替代[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));- matches
Assert.Throws<T>or any derived typeT - matches only the exact type
Assert.ThrowsExactly<T>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 return types over for type safety:
ValueTupleIEnumerable<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 — 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" },
];为保证类型安全,优先使用返回类型而非:
ValueTupleIEnumerable<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 fields and works correctly with nullability analyzers (fields are guaranteed non-null after construction)
readonly - Use only for async initialization, combined with the constructor for sync parts
[TestInitialize] - Use for cleanup that must run even on failure
[TestCleanup] - Inject via constructor (MSTest 3.6+)
TestContext
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] - 通过构造函数注入(MSTest 3.6+)
TestContext
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
执行顺序
- — once per assembly
[AssemblyInitialize] - — once per class
[ClassInitialize] - Per test:
- With property injection: Constructor → set
TestContextproperty →TestContext[TestInitialize] - With constructor injection of : Constructor (receives
TestContext) →TestContext[TestInitialize]
- With
- Test method
- →
[TestCleanup]→DisposeAsync— per testDispose - — once per class
[ClassCleanup] - — once per assembly
[AssemblyCleanup]
- — 每个程序集执行一次
[AssemblyInitialize] - — 每个类执行一次
[ClassInitialize] - 每个测试流程:
- 采用TestContext属性注入:构造函数 → 设置TestContext属性 →
[TestInitialize] - 采用TestContext构造函数注入:构造函数(接收TestContext) →
[TestInitialize]
- 采用TestContext属性注入:构造函数 → 设置TestContext属性 →
- 测试方法
- →
[TestCleanup]→DisposeAsync— 每个测试执行一次Dispose - — 每个类执行一次
[ClassCleanup] - — 每个程序集执行一次
[AssemblyCleanup]
Step 6: Apply cancellation and timeout patterns
步骤6:应用取消与超时模式
Always use with :
TestContext.CancellationToken[Timeout]csharp
[TestMethod]
[Timeout(5000)]
public async Task FetchData_ReturnsWithinTimeout()
{
var result = await _client.GetDataAsync(_testContext.CancellationToken);
Assert.IsNotNull(result);
}始终结合使用:
[Timeout]TestContext.CancellationTokencsharp
[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
常见陷阱
| Pitfall | Solution |
|---|---|
| Always put expected first: |
| Use |
| Use |
Hard cast | Use |
| Use |
Sync setup in | Initialize in the constructor instead — enables |
| Use |
| Drop the |
| Remove |
| Non-sealed test classes | Seal test classes by default for performance |
| 陷阱 | 解决方案 |
|---|---|
| 始终将预期值放在前面: |
| 使用 |
| 使用 |
硬转换 | 使用 |
DynamicData使用 | 使用 |
在 | 改为在构造函数中初始化 — 支持 |
异步测试中使用 | 使用 |
| 移除 |
| 移除 |
| 测试类未密封 | 默认密封测试类以提升性能 |