| name | csharp-mstest |
| description | Get best practices for MSTest 3.x/4.x unit testing, including modern assertion APIs and data-driven tests |
MSTest Best Practices (MSTest 3.x/4.x)
Your goal is to help me write effective unit tests with modern MSTest, using current APIs and best practices.
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
Test Class Structure
- Use
[TestClass] attribute for test classes
- Seal test classes by default for performance and design clarity
- Use
[TestMethod] for test methods (prefer over [DataTestMethod])
- Follow Arrange-Act-Assert (AAA) pattern
- Name tests using pattern
MethodName_Scenario_ExpectedBehavior
[TestClass]
public sealed class CalculatorTests
{
[TestMethod]
public void Add_TwoPositiveNumbers_ReturnsSum()
{
var calculator = new Calculator();
var result = calculator.Add(2, 3);
Assert.AreEqual(5, result);
}
}
Test Lifecycle
- Prefer constructors over
[TestInitialize] - enables readonly fields and follows standard C# patterns
- Use
[TestCleanup] for cleanup that must run even if test fails
- Combine constructor with async
[TestInitialize] when async setup is needed
[TestClass]
public sealed class ServiceTests
{
private readonly MyService _service;
public ServiceTests()
{
_service = new MyService();
}
[TestInitialize]
public async Task InitAsync()
{
await _service.WarmupAsync();
}
[TestCleanup]
public void Cleanup() => _service.Reset();
}
Execution Order
- Assembly Initialization -
[AssemblyInitialize] (once per test assembly)
- Class Initialization -
[ClassInitialize] (once per test class)
- Test Initialization (for every test method):
- Constructor
- Set
TestContext property
[TestInitialize]
- Test Execution - test method runs
- Test Cleanup (for every test method):
[TestCleanup]
DisposeAsync (if implemented)
Dispose (if implemented)
- Class Cleanup -
[ClassCleanup] (once per test class)
- Assembly Cleanup -
[AssemblyCleanup] (once per test assembly)
Modern Assertion APIs
MSTest provides three assertion classes: Assert, StringAssert, and CollectionAssert.
Assert Class - Core Assertions
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("Test failed due to...");
Assert.Inconclusive("Test cannot be completed because...");
Exception Testing (Prefer over [ExpectedException])
var ex = Assert.Throws<ArgumentException>(() => Method(null));
Assert.AreEqual("Value cannot be null.", ex.Message);
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.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.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);
Comparison Assertions
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
Assert.IsInstanceOfType<MyClass>(obj, out var typed);
typed.DoSomething();
var typed = Assert.IsInstanceOfType<MyClass>(obj);
typed.DoSomething();
Assert.IsNotInstanceOfType<WrongType>(obj);
Assert.That (MSTest 4.0+)
Assert.That(result.Count > 0);
StringAssert Class
Note: Prefer Assert class equivalents when available (e.g., Assert.Contains("expected", actual) over StringAssert.Contains(actual, "expected")).
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+"));
CollectionAssert Class
Note: Prefer Assert class equivalents when available (e.g., Assert.Contains).
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
[TestMethod]
[DataRow(1, 2, 3)]
[DataRow(0, 0, 0, DisplayName = "Zeros")]
[DataRow(-1, 1, 0, IgnoreMessage = "Known issue #123")]
public void Add_ReturnsSum(int a, int b, int expected)
{
Assert.AreEqual(expected, Calculator.Add(a, b));
}
DynamicData
The data source can return any of the following types:
IEnumerable<(T1, T2, ...)> (ValueTuple) - preferred, provides type safety (MSTest 3.7+)
IEnumerable<Tuple<T1, T2, ...>> - provides type safety
IEnumerable<TestDataRow> - provides type safety plus control over test metadata (display name, categories)
IEnumerable<object[]> - least preferred, no type safety
Note: When creating new test data methods, prefer ValueTuple or TestDataRow over IEnumerable<object[]>. The object[] approach provides no compile-time type checking and can lead to runtime errors from type mismatches.
[TestMethod]
[DynamicData(nameof(TestData))]
public void DynamicTest(int a, int b, int expected)
{
Assert.AreEqual(expected, Calculator.Add(a, b));
}
public static IEnumerable<(int a, int b, int expected)> TestData =>
[
(1, 2, 3),
(0, 0, 0),
];
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" },
];
public static IEnumerable<object[]> LegacyTestData =>
[
[1, 2, 3],
[0, 0, 0],
];
TestContext
The TestContext class provides test run information, cancellation support, and output methods.
See TestContext documentation for complete reference.
Accessing TestContext
public TestContext TestContext { get; set; }
[TestClass]
public sealed class MyTests
{
private readonly TestContext _testContext;
public MyTests(TestContext testContext)
{
_testContext = testContext;
}
}
[ClassInitialize]
public static void ClassInit(TestContext context) { }
[ClassCleanup]
public static void ClassCleanup(TestContext context) { }
[AssemblyCleanup]
public static void AssemblyCleanup(TestContext context) { }
Cancellation Token
Always use TestContext.CancellationToken for cooperative cancellation with [Timeout]:
[TestMethod]
[Timeout(5000)]
public async Task LongRunningTest()
{
await _httpClient.GetAsync(url, TestContext.CancellationToken);
}
Test Run Properties
TestContext.TestName
TestContext.TestDisplayName
TestContext.CurrentTestOutcome
TestContext.TestData
TestContext.TestException
TestContext.DeploymentDirectory
Output and Result Files
TestContext.WriteLine("Processing item {0}", itemId);
TestContext.AddResultFile(screenshotPath);
TestContext.Properties["SharedKey"] = computedValue;
Advanced Features
Retry for Flaky Tests (MSTest 3.9+)
[TestMethod]
[Retry(3)]
public void FlakyTest() { }
Conditional Execution (MSTest 3.10+)
Skip or run tests based on OS or CI environment:
[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() { }
[TestMethod]
[CICondition]
public void CIOnlyTest() { }
[TestMethod]
[CICondition(ConditionMode.Exclude)]
public void LocalOnlyTest() { }
Parallelization
[assembly: Parallelize(Workers = 4, Scope = ExecutionScope.MethodLevel)]
[TestClass]
[DoNotParallelize]
public sealed class SequentialTests { }
Work Item Traceability (MSTest 3.8+)
Link tests to work items for traceability in test reports:
[TestMethod]
[WorkItem(12345)]
public void Feature_Scenario_ExpectedBehavior() { }
[TestMethod]
[WorkItem(12345)]
[WorkItem(67890)]
public void Feature_CoversMultipleRequirements() { }
[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
Common Mistakes to Avoid
Assert.AreEqual(actual, expected);
Assert.AreEqual(expected, actual);
[ExpectedException(typeof(ArgumentException))]
Assert.Throws<ArgumentException>(() => Method());
var item = items.Single();
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);
public TestContext? TestContext { get; set; }
public TestContext TestContext { get; set; } = null!;
public TestContext TestContext { get; set; }
Test Organization
- Group tests by feature or component
- Use
[TestCategory("Category")] for filtering
- Use
[TestProperty("Name", "Value")] for custom metadata (e.g., [TestProperty("Bug", "12345")])
- Use
[Priority(1)] for critical tests
- Enable relevant MSTest analyzers (MSTEST0020 for constructor preference)
Mocking and Isolation
- Use Moq or NSubstitute for mocking dependencies
- Use interfaces to facilitate mocking
- Mock dependencies to isolate units under test
MCP Interactions
- GitHub MCP: Search repository for previous similar actions or related PRs before executing this skill.
- Brave Search MCP: Validate current patterns against the live internet if the context is sparse.
- Context7 MCP: Extract deep architectural/domain context relevant to the skill's execution.