csharp-mstest
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMSTest Best Practices (MSTest 3.x/4.x)
MSTest 最佳实践(MSTest 3.x/4.x)
Your goal is to help me write effective unit tests with modern MSTest, using current APIs and best practices.
本文目标是帮助你使用现代MSTest及当前API,编写高效的单元测试并遵循最佳实践。
Project Setup
项目设置
- Use a separate test project with naming convention
[ProjectName].Tests - Reference MSTest 3.x+ NuGet packages (includes analyzers)
- Consider using MSTest.Sdk for simplified project setup
- Run tests with
dotnet test
- 使用独立的测试项目,命名遵循规范
[ProjectName].Tests - 引用MSTest 3.x+ NuGet包(包含代码分析器)
- 可考虑使用MSTest.Sdk简化项目设置
- 使用命令运行测试
dotnet test
Test Class Structure
测试类结构
- Use attribute for test classes
[TestClass] - Seal test classes by default for performance and design clarity
- Use for test methods (prefer over
[TestMethod])[DataTestMethod] - Follow Arrange-Act-Assert (AAA) pattern
- Name tests using pattern
MethodName_Scenario_ExpectedBehavior
csharp
[TestClass]
public sealed class CalculatorTests
{
[TestMethod]
public void Add_TwoPositiveNumbers_ReturnsSum()
{
// Arrange
var calculator = new Calculator();
// Act
var result = calculator.Add(2, 3);
// Assert
Assert.AreEqual(5, result);
}
}- 测试类使用特性
[TestClass] - 默认将测试类设为密封类,以提升性能并明确设计意图
- 测试方法使用(优先于
[TestMethod])[DataTestMethod] - 遵循Arrange-Act-Assert(AAA)模式
- 测试方法命名遵循的格式
方法名_场景_预期行为
csharp
[TestClass]
public sealed class CalculatorTests
{
[TestMethod]
public void Add_TwoPositiveNumbers_ReturnsSum()
{
// Arrange
var calculator = new Calculator();
// Act
var result = calculator.Add(2, 3);
// Assert
Assert.AreEqual(5, result);
}
}Test Lifecycle
测试生命周期
- Prefer constructors over - enables
[TestInitialize]fields and follows standard C# patternsreadonly - Use for cleanup that must run even if test fails
[TestCleanup] - Combine constructor with async when async setup is needed
[TestInitialize]
csharp
[TestClass]
public sealed class ServiceTests
{
private readonly MyService _service; // readonly enabled by constructor
public ServiceTests()
{
_service = new MyService();
}
[TestInitialize]
public async Task InitAsync()
{
// Use for async initialization only
await _service.WarmupAsync();
}
[TestCleanup]
public void Cleanup() => _service.Reset();
}- 优先使用构造函数而非- 支持
[TestInitialize]字段,符合标准C#模式readonly - 若需要在测试失败时仍执行清理操作,使用
[TestCleanup] - 当需要异步初始化时,将构造函数与异步结合使用
[TestInitialize]
csharp
[TestClass]
public sealed class ServiceTests
{
private readonly MyService _service; // 构造函数支持readonly字段
public ServiceTests()
{
_service = new MyService();
}
[TestInitialize]
public async Task InitAsync()
{
// 仅用于异步初始化
await _service.WarmupAsync();
}
[TestCleanup]
public void Cleanup() => _service.Reset();
}Execution Order
执行顺序
- Assembly Initialization - (once per test assembly)
[AssemblyInitialize] - Class Initialization - (once per test class)
[ClassInitialize] - Test Initialization (for every test method):
- Constructor
- Set property
TestContext [TestInitialize]
- Test Execution - test method runs
- Test Cleanup (for every test method):
[TestCleanup]- (if implemented)
DisposeAsync - (if implemented)
Dispose
- Class Cleanup - (once per test class)
[ClassCleanup] - Assembly Cleanup - (once per test assembly)
[AssemblyCleanup]
- 程序集初始化 - (每个测试程序集执行一次)
[AssemblyInitialize] - 类初始化 - (每个测试类执行一次)
[ClassInitialize] - 测试初始化(每个测试方法执行一次):
- 构造函数
- 设置属性
TestContext [TestInitialize]
- 测试执行 - 运行测试方法
- 测试清理(每个测试方法执行一次):
[TestCleanup]- (若已实现)
DisposeAsync - (若已实现)
Dispose
- 类清理 - (每个测试类执行一次)
[ClassCleanup] - 程序集清理 - (每个测试程序集执行一次)
[AssemblyCleanup]
Modern Assertion APIs
现代断言API
MSTest provides three assertion classes: , , and .
AssertStringAssertCollectionAssertMSTest提供三个断言类:、和。
AssertStringAssertCollectionAssertAssert Class - Core Assertions
Assert类 - 核心断言
csharp
// Equality
Assert.AreEqual(expected, actual);
Assert.AreNotEqual(notExpected, actual);
Assert.AreSame(expectedObject, actualObject); // Reference equality
Assert.AreNotSame(notExpectedObject, actualObject);
// Null checks
Assert.IsNull(value);
Assert.IsNotNull(value);
// Boolean
Assert.IsTrue(condition);
Assert.IsFalse(condition);
// Fail/Inconclusive
Assert.Fail("Test failed due to...");
Assert.Inconclusive("Test cannot be completed because...");csharp
// 相等性检查
Assert.AreEqual(expected, actual);
Assert.AreNotEqual(notExpected, actual);
Assert.AreSame(expectedObject, actualObject); // 引用相等
Assert.AreNotSame(notExpectedObject, actualObject);
// 空值检查
Assert.IsNull(value);
Assert.IsNotNull(value);
// 布尔值检查
Assert.IsTrue(condition);
Assert.IsFalse(condition);
// 失败/不确定结果
Assert.Fail("测试失败原因:...");
Assert.Inconclusive("测试无法完成,原因:...");Exception Testing (Prefer over [ExpectedException]
)
[ExpectedException]异常测试(优先于[ExpectedException]
)
[ExpectedException]csharp
// Assert.Throws - matches TException or derived types
var ex = Assert.Throws<ArgumentException>(() => Method(null));
Assert.AreEqual("Value cannot be null.", ex.Message);
// Assert.ThrowsExactly - matches exact type only
var ex = Assert.ThrowsExactly<InvalidOperationException>(() => Method());
// Async versions
var ex = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetAsync(url));
var ex = await Assert.ThrowsExactlyAsync<InvalidOperationException>(async () => await Method());csharp
// Assert.Throws - 匹配TException或其派生类型
var ex = Assert.Throws<ArgumentException>(() => Method(null));
Assert.AreEqual("值不能为空。", ex.Message);
// Assert.ThrowsExactly - 仅匹配精确类型
var ex = Assert.ThrowsExactly<InvalidOperationException>(() => Method());
// 异步版本
var ex = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetAsync(url));
var ex = await Assert.ThrowsExactlyAsync<InvalidOperationException>(async () => await Method());Collection Assertions (Assert class)
集合断言(Assert类)
csharp
Assert.Contains(expectedItem, collection);
Assert.DoesNotContain(unexpectedItem, collection);
Assert.ContainsSingle(collection); // exactly one element
Assert.HasCount(5, collection);
Assert.IsEmpty(collection);
Assert.IsNotEmpty(collection);csharp
Assert.Contains(expectedItem, collection);
Assert.DoesNotContain(unexpectedItem, collection);
Assert.ContainsSingle(collection); // 集合恰好包含一个元素
Assert.HasCount(5, collection);
Assert.IsEmpty(collection);
Assert.IsNotEmpty(collection);String Assertions (Assert class)
字符串断言(Assert类)
csharp
Assert.Contains("expected", actualString);
Assert.StartsWith("prefix", actualString);
Assert.EndsWith("suffix", actualString);
Assert.DoesNotStartWith("prefix", actualString);
Assert.DoesNotEndWith("suffix", actualString);
Assert.MatchesRegex(@"\d{3}-\d{4}", phoneNumber);
Assert.DoesNotMatchRegex(@"\d+", textOnly);csharp
Assert.Contains("预期内容", actualString);
Assert.StartsWith("前缀", actualString);
Assert.EndsWith("后缀", actualString);
Assert.DoesNotStartWith("前缀", actualString);
Assert.DoesNotEndWith("后缀", actualString);
Assert.MatchesRegex(@"\d{3}-\d{4}", phoneNumber);
Assert.DoesNotMatchRegex(@"\d+", textOnly);Comparison Assertions
比较断言
csharp
Assert.IsGreaterThan(lowerBound, actual);
Assert.IsGreaterThanOrEqualTo(lowerBound, actual);
Assert.IsLessThan(upperBound, actual);
Assert.IsLessThanOrEqualTo(upperBound, actual);
Assert.IsInRange(actual, low, high);
Assert.IsPositive(number);
Assert.IsNegative(number);csharp
Assert.IsGreaterThan(lowerBound, actual);
Assert.IsGreaterThanOrEqualTo(lowerBound, actual);
Assert.IsLessThan(upperBound, actual);
Assert.IsLessThanOrEqualTo(upperBound, actual);
Assert.IsInRange(actual, low, high);
Assert.IsPositive(number);
Assert.IsNegative(number);Type Assertions
类型断言
csharp
// MSTest 3.x - uses out parameter
Assert.IsInstanceOfType<MyClass>(obj, out var typed);
typed.DoSomething();
// MSTest 4.x - returns typed result directly
var typed = Assert.IsInstanceOfType<MyClass>(obj);
typed.DoSomething();
Assert.IsNotInstanceOfType<WrongType>(obj);csharp
// MSTest 3.x - 使用out参数
Assert.IsInstanceOfType<MyClass>(obj, out var typed);
typed.DoSomething();
// MSTest 4.x - 直接返回强类型结果
var typed = Assert.IsInstanceOfType<MyClass>(obj);
typed.DoSomething();
Assert.IsNotInstanceOfType<WrongType>(obj);Assert.That (MSTest 4.0+)
Assert.That(MSTest 4.0+)
csharp
Assert.That(result.Count > 0); // Auto-captures expression in failure messagecsharp
Assert.That(result.Count > 0); // 自动在失败消息中捕获表达式StringAssert Class
StringAssert类
Note: Preferclass equivalents when available (e.g.,AssertoverAssert.Contains("expected", actual)).StringAssert.Contains(actual, "expected")
csharp
StringAssert.Contains(actualString, "expected");
StringAssert.StartsWith(actualString, "prefix");
StringAssert.EndsWith(actualString, "suffix");
StringAssert.Matches(actualString, new Regex(@"\d{3}-\d{4}"));
StringAssert.DoesNotMatch(actualString, new Regex(@"\d+"));注意: 当Assert类有等效方法时,优先使用Assert类(例如,优先使用而非Assert.Contains("预期内容", actual))。StringAssert.Contains(actual, "预期内容")
csharp
StringAssert.Contains(actualString, "预期内容");
StringAssert.StartsWith(actualString, "前缀");
StringAssert.EndsWith(actualString, "后缀");
StringAssert.Matches(actualString, new Regex(@"\d{3}-\d{4}"));
StringAssert.DoesNotMatch(actualString, new Regex(@"\d+"));CollectionAssert Class
CollectionAssert类
Note: Preferclass equivalents when available (e.g.,Assert).Assert.Contains
csharp
// Containment
CollectionAssert.Contains(collection, expectedItem);
CollectionAssert.DoesNotContain(collection, unexpectedItem);
// Equality (same elements, same order)
CollectionAssert.AreEqual(expectedCollection, actualCollection);
CollectionAssert.AreNotEqual(unexpectedCollection, actualCollection);
// Equivalence (same elements, any order)
CollectionAssert.AreEquivalent(expectedCollection, actualCollection);
CollectionAssert.AreNotEquivalent(unexpectedCollection, actualCollection);
// Subset checks
CollectionAssert.IsSubsetOf(subset, superset);
CollectionAssert.IsNotSubsetOf(notSubset, collection);
// Element validation
CollectionAssert.AllItemsAreInstancesOfType(collection, typeof(MyClass));
CollectionAssert.AllItemsAreNotNull(collection);
CollectionAssert.AllItemsAreUnique(collection);注意: 当Assert类有等效方法时,优先使用Assert类(例如,优先使用)。Assert.Contains
csharp
// 包含性检查
CollectionAssert.Contains(collection, expectedItem);
CollectionAssert.DoesNotContain(collection, unexpectedItem);
// 相等性(元素相同且顺序一致)
CollectionAssert.AreEqual(expectedCollection, actualCollection);
CollectionAssert.AreNotEqual(unexpectedCollection, actualCollection);
// 等效性(元素相同,顺序不限)
CollectionAssert.AreEquivalent(expectedCollection, actualCollection);
CollectionAssert.AreNotEquivalent(unexpectedCollection, actualCollection);
// 子集检查
CollectionAssert.IsSubsetOf(subset, superset);
CollectionAssert.IsNotSubsetOf(notSubset, collection);
// 元素验证
CollectionAssert.AllItemsAreInstancesOfType(collection, typeof(MyClass));
CollectionAssert.AllItemsAreNotNull(collection);
CollectionAssert.AllItemsAreUnique(collection);Data-Driven Tests
数据驱动测试
DataRow
DataRow
csharp
[TestMethod]
[DataRow(1, 2, 3)]
[DataRow(0, 0, 0, DisplayName = "Zeros")]
[DataRow(-1, 1, 0, IgnoreMessage = "Known issue #123")] // MSTest 3.8+
public void Add_ReturnsSum(int a, int b, int expected)
{
Assert.AreEqual(expected, Calculator.Add(a, b));
}csharp
[TestMethod]
[DataRow(1, 2, 3)]
[DataRow(0, 0, 0, DisplayName = "零值测试")]
[DataRow(-1, 1, 0, IgnoreMessage = "已知问题 #123")] // MSTest 3.8+
public void Add_ReturnsSum(int a, int b, int expected)
{
Assert.AreEqual(expected, Calculator.Add(a, b));
}DynamicData
DynamicData
The data source can return any of the following types:
- (ValueTuple) - preferred, provides type safety (MSTest 3.7+)
IEnumerable<(T1, T2, ...)> - - provides type safety
IEnumerable<Tuple<T1, T2, ...>> - - provides type safety plus control over test metadata (display name, categories)
IEnumerable<TestDataRow> - - least preferred, no type safety
IEnumerable<object[]>
Note: When creating new test data methods, preferorValueTupleoverTestDataRow. TheIEnumerable<object[]>approach provides no compile-time type checking and can lead to runtime errors from type mismatches.object[]
csharp
[TestMethod]
[DynamicData(nameof(TestData))]
public void DynamicTest(int a, int b, int expected)
{
Assert.AreEqual(expected, Calculator.Add(a, b));
}
// ValueTuple - preferred (MSTest 3.7+)
public static IEnumerable<(int a, int b, int expected)> TestData =>
[
(1, 2, 3),
(0, 0, 0),
];
// TestDataRow - when you need custom display names or metadata
public static IEnumerable<TestDataRow<(int a, int b, int expected)>> TestDataWithMetadata =>
[
new((1, 2, 3)) { DisplayName = "Positive numbers" },
new((0, 0, 0)) { DisplayName = "Zeros" },
new((-1, 1, 0)) { DisplayName = "Mixed signs", IgnoreMessage = "Known issue #123" },
];
// IEnumerable<object[]> - avoid for new code (no type safety)
public static IEnumerable<object[]> LegacyTestData =>
[
[1, 2, 3],
[0, 0, 0],
];数据源可返回以下任意类型:
- (值元组)- 优先选择,提供类型安全性(MSTest 3.7+)
IEnumerable<(T1, T2, ...)> - - 提供类型安全性
IEnumerable<Tuple<T1, T2, ...>> - - 提供类型安全性,且可控制测试元数据(显示名称、分类)
IEnumerable<TestDataRow> - - 最不推荐,无类型安全性
IEnumerable<object[]>
注意: 创建新的测试数据方法时,优先选择或值元组而非TestDataRow。IEnumerable<object[]>方式无编译时类型检查,可能因类型不匹配导致运行时错误。object[]
csharp
[TestMethod]
[DynamicData(nameof(TestData))]
public void DynamicTest(int a, int b, int expected)
{
Assert.AreEqual(expected, Calculator.Add(a, b));
}
// 值元组 - 优先选择(MSTest 3.7+)
public static IEnumerable<(int a, int b, int expected)> TestData =>
[
(1, 2, 3),
(0, 0, 0),
];
// TestDataRow - 当需要自定义显示名称或元数据时使用
public static IEnumerable<TestDataRow<(int a, int b, int expected)>> TestDataWithMetadata =>
[
new((1, 2, 3)) { DisplayName = "正数相加" },
new((0, 0, 0)) { DisplayName = "零值相加" },
new((-1, 1, 0)) { DisplayName = "正负相加", IgnoreMessage = "已知问题 #123" },
];
// IEnumerable<object[]> - 新代码避免使用(无类型安全)
public static IEnumerable<object[]> LegacyTestData =>
[
[1, 2, 3],
[0, 0, 0],
];TestContext
TestContext
The class provides test run information, cancellation support, and output methods.
See TestContext documentation for complete reference.
TestContextTestContextAccessing TestContext
访问TestContext
csharp
// Property (MSTest suppresses CS8618 - don't use nullable or = null!)
public TestContext TestContext { get; set; }
// Constructor injection (MSTest 3.6+) - preferred for immutability
[TestClass]
public sealed class MyTests
{
private readonly TestContext _testContext;
public MyTests(TestContext testContext)
{
_testContext = testContext;
}
}
// Static methods receive it as parameter
[ClassInitialize]
public static void ClassInit(TestContext context) { }
// Optional for cleanup methods (MSTest 3.6+)
[ClassCleanup]
public static void ClassCleanup(TestContext context) { }
[AssemblyCleanup]
public static void AssemblyCleanup(TestContext context) { }csharp
// 属性方式(MSTest会抑制CS8618警告 - 不要设为可空或赋值为null!)
public TestContext TestContext { get; set; }
// 构造函数注入(MSTest 3.6+)- 优先选择,保证不可变性
[TestClass]
public sealed class MyTests
{
private readonly TestContext _testContext;
public MyTests(TestContext testContext)
{
_testContext = testContext;
}
}
// 静态方法通过参数接收
[ClassInitialize]
public static void ClassInit(TestContext context) { }
// 清理方法可选接收(MSTest 3.6+)
[ClassCleanup]
public static void ClassCleanup(TestContext context) { }
[AssemblyCleanup]
public static void AssemblyCleanup(TestContext context) { }Cancellation Token
取消令牌
Always use for cooperative cancellation with :
TestContext.CancellationToken[Timeout]csharp
[TestMethod]
[Timeout(5000)]
public async Task LongRunningTest()
{
await _httpClient.GetAsync(url, TestContext.CancellationToken);
}始终使用配合实现协作式取消:
TestContext.CancellationToken[Timeout]csharp
[TestMethod]
[Timeout(5000)]
public async Task LongRunningTest()
{
await _httpClient.GetAsync(url, TestContext.CancellationToken);
}Test Run Properties
测试运行属性
csharp
TestContext.TestName // Current test method name
TestContext.TestDisplayName // Display name (3.7+)
TestContext.CurrentTestOutcome // Pass/Fail/InProgress
TestContext.TestData // Parameterized test data (3.7+, in TestInitialize/Cleanup)
TestContext.TestException // Exception if test failed (3.7+, in TestCleanup)
TestContext.DeploymentDirectory // Directory with deployment itemscsharp
TestContext.TestName // 当前测试方法名称
TestContext.TestDisplayName // 显示名称(3.7+)
TestContext.CurrentTestOutcome // 测试结果:通过/失败/进行中
TestContext.TestData // 参数化测试数据(3.7+,在TestInitialize/Cleanup中可用)
TestContext.TestException // 测试失败时的异常(3.7+,在TestCleanup中可用)
TestContext.DeploymentDirectory // 部署项所在目录Output and Result Files
输出与结果文件
csharp
// Write to test output (useful for debugging)
TestContext.WriteLine("Processing item {0}", itemId);
// Attach files to test results (logs, screenshots)
TestContext.AddResultFile(screenshotPath);
// Store/retrieve data across test methods
TestContext.Properties["SharedKey"] = computedValue;csharp
// 写入测试输出(用于调试)
TestContext.WriteLine("正在处理项 {0}", itemId);
// 将文件附加到测试结果(日志、截图等)
TestContext.AddResultFile(screenshotPath);
// 在测试方法间存储/检索数据
TestContext.Properties["SharedKey"] = computedValue;Advanced Features
高级特性
Retry for Flaky Tests (MSTest 3.9+)
不稳定测试重试(MSTest 3.9+)
csharp
[TestMethod]
[Retry(3)]
public void FlakyTest() { }csharp
[TestMethod]
[Retry(3)]
public void FlakyTest() { }Conditional Execution (MSTest 3.10+)
条件执行(MSTest 3.10+)
Skip or run tests based on OS or CI environment:
csharp
// OS-specific tests
[TestMethod]
[OSCondition(OperatingSystems.Windows)]
public void WindowsOnlyTest() { }
[TestMethod]
[OSCondition(OperatingSystems.Linux | OperatingSystems.MacOS)]
public void UnixOnlyTest() { }
[TestMethod]
[OSCondition(ConditionMode.Exclude, OperatingSystems.Windows)]
public void SkipOnWindowsTest() { }
// CI environment tests
[TestMethod]
[CICondition] // Runs only in CI (default: ConditionMode.Include)
public void CIOnlyTest() { }
[TestMethod]
[CICondition(ConditionMode.Exclude)] // Skips in CI, runs locally
public void LocalOnlyTest() { }根据操作系统或CI环境跳过或运行测试:
csharp
// 特定操作系统测试
[TestMethod]
[OSCondition(OperatingSystems.Windows)]
public void WindowsOnlyTest() { }
[TestMethod]
[OSCondition(OperatingSystems.Linux | OperatingSystems.MacOS)]
public void UnixOnlyTest() { }
[TestMethod]
[OSCondition(ConditionMode.Exclude, OperatingSystems.Windows)]
public void SkipOnWindowsTest() { }
// CI环境专属测试
[TestMethod]
[CICondition] // 仅在CI环境运行(默认:ConditionMode.Include)
public void CIOnlyTest() { }
[TestMethod]
[CICondition(ConditionMode.Exclude)] // CI环境跳过,本地运行
public void LocalOnlyTest() { }Parallelization
并行化
csharp
// Assembly level
[assembly: Parallelize(Workers = 4, Scope = ExecutionScope.MethodLevel)]
// Disable for specific class
[TestClass]
[DoNotParallelize]
public sealed class SequentialTests { }csharp
// 程序集级别
[assembly: Parallelize(Workers = 4, Scope = ExecutionScope.MethodLevel)]
// 特定类禁用并行
[TestClass]
[DoNotParallelize]
public sealed class SequentialTests { }Work Item Traceability (MSTest 3.8+)
工作项可追溯性(MSTest 3.8+)
Link tests to work items for traceability in test reports:
csharp
// Azure DevOps work items
[TestMethod]
[WorkItem(12345)] // Links to work item #12345
public void Feature_Scenario_ExpectedBehavior() { }
// Multiple work items
[TestMethod]
[WorkItem(12345)]
[WorkItem(67890)]
public void Feature_CoversMultipleRequirements() { }
// GitHub issues (MSTest 3.8+)
[TestMethod]
[GitHubWorkItem("https://github.com/owner/repo/issues/42")]
public void BugFix_Issue42_IsResolved() { }Work item associations appear in test results and can be used for:
- Tracing test coverage to requirements
- Linking bug fixes to regression tests
- Generating traceability reports in CI/CD pipelines
将测试与工作项关联,实现测试报告中的可追溯性:
csharp
// Azure DevOps工作项
[TestMethod]
[WorkItem(12345)] // 关联工作项#12345
public void Feature_Scenario_ExpectedBehavior() { }
// 多个工作项
[TestMethod]
[WorkItem(12345)]
[WorkItem(67890)]
public void Feature_CoversMultipleRequirements() { }
// GitHub问题(MSTest 3.8+)
[TestMethod]
[GitHubWorkItem("https://github.com/owner/repo/issues/42")]
public void BugFix_Issue42_IsResolved() { }工作项关联会显示在测试结果中,可用于:
- 跟踪测试对需求的覆盖情况
- 将Bug修复与回归测试关联
- 在CI/CD流水线中生成可追溯性报告
Common Mistakes to Avoid
常见错误规避
csharp
// ❌ Wrong argument order
Assert.AreEqual(actual, expected);
// ✅ Correct
Assert.AreEqual(expected, actual);
// ❌ Using ExpectedException (obsolete)
[ExpectedException(typeof(ArgumentException))]
// ✅ Use Assert.Throws
Assert.Throws<ArgumentException>(() => Method());
// ❌ Using LINQ Single() - unclear exception
var item = items.Single();
// ✅ Use ContainsSingle - better failure message
var item = Assert.ContainsSingle(items);
// ❌ Hard cast - unclear exception
var handler = (MyHandler)result;
// ✅ Type assertion - shows actual type on failure
var handler = Assert.IsInstanceOfType<MyHandler>(result);
// ❌ Ignoring cancellation token
await client.GetAsync(url, CancellationToken.None);
// ✅ Flow test cancellation
await client.GetAsync(url, TestContext.CancellationToken);
// ❌ Making TestContext nullable - leads to unnecessary null checks
public TestContext? TestContext { get; set; }
// ❌ Using null! - MSTest already suppresses CS8618 for this property
public TestContext TestContext { get; set; } = null!;
// ✅ Declare without nullable or initializer - MSTest handles the warning
public TestContext TestContext { get; set; }csharp
// ❌ 参数顺序错误
Assert.AreEqual(actual, expected);
// ✅ 正确写法
Assert.AreEqual(expected, actual);
// ❌ 使用已过时的ExpectedException
[ExpectedException(typeof(ArgumentException))]
// ✅ 使用Assert.Throws
Assert.Throws<ArgumentException>(() => Method());
// ❌ 使用LINQ Single() - 异常信息不清晰
var item = items.Single();
// ✅ 使用ContainsSingle - 失败信息更友好
var item = Assert.ContainsSingle(items);
// ❌ 强制类型转换 - 异常信息不清晰
var handler = (MyHandler)result;
// ✅ 类型断言 - 失败时显示实际类型
var handler = Assert.IsInstanceOfType<MyHandler>(result);
// ❌ 忽略取消令牌
await client.GetAsync(url, CancellationToken.None);
// ✅ 传递测试取消令牌
await client.GetAsync(url, TestContext.CancellationToken);
// ❌ 将TestContext设为可空 - 导致不必要的空值检查
public TestContext? TestContext { get; set; }
// ❌ 使用null! - MSTest已针对该属性抑制CS8618警告
public TestContext TestContext { get; set; } = null!;
// ✅ 不设为可空也不初始化 - MSTest会处理警告
public TestContext TestContext { get; set; }Test Organization
测试组织
- Group tests by feature or component
- Use for filtering
[TestCategory("Category")] - Use for custom metadata (e.g.,
[TestProperty("Name", "Value")])[TestProperty("Bug", "12345")] - Use for critical tests
[Priority(1)] - Enable relevant MSTest analyzers (MSTEST0020 for constructor preference)
- 按功能或组件分组测试
- 使用进行过滤
[TestCategory("分类名")] - 使用添加自定义元数据(例如
[TestProperty("名称", "值")])[TestProperty("Bug", "12345")] - 使用标记关键测试
[Priority(1)] - 启用相关的MSTest代码分析器(如MSTEST0020,用于优先推荐构造函数写法)
Mocking and Isolation
模拟与隔离
- Use Moq or NSubstitute for mocking dependencies
- Use interfaces to facilitate mocking
- Mock dependencies to isolate units under test
- 使用Moq或NSubstitute模拟依赖项
- 使用接口以方便模拟
- 模拟依赖项以隔离被测单元