Skill v1.0.1
currentAutomated scan100/1001 files
version: "1.0.1" 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(){// Arrangevar calculator = new Calculator();// Actvar result = calculator.Add(2, 3);// AssertAssert.AreEqual(5, result);}}
Test Lifecycle
- Prefer constructors over `[TestInitialize]` - enables
readonlyfields 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; // readonly enabled by constructorpublic ServiceTests(){_service = new MyService();}[TestInitialize]public async Task InitAsync(){// Use for async initialization onlyawait _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
TestContextproperty [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
// EqualityAssert.AreEqual(expected, actual);Assert.AreNotEqual(notExpected, actual);Assert.AreSame(expectedObject, actualObject); // Reference equalityAssert.AreNotSame(notExpectedObject, actualObject);// Null checksAssert.IsNull(value);Assert.IsNotNull(value);// BooleanAssert.IsTrue(condition);Assert.IsFalse(condition);// Fail/InconclusiveAssert.Fail("Test failed due to...");Assert.Inconclusive("Test cannot be completed because...");
Exception Testing (Prefer over [ExpectedException])
// Assert.Throws - matches TException or derived typesvar ex = Assert.Throws<ArgumentException>(() => Method(null));Assert.AreEqual("Value cannot be null.", ex.Message);// Assert.ThrowsExactly - matches exact type onlyvar ex = Assert.ThrowsExactly<InvalidOperationException>(() => Method());// Async versionsvar 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); // exactly one elementAssert.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
// MSTest 3.x - uses out parameterAssert.IsInstanceOfType<MyClass>(obj, out var typed);typed.DoSomething();// MSTest 4.x - returns typed result directlyvar typed = Assert.IsInstanceOfType<MyClass>(obj);typed.DoSomething();Assert.IsNotInstanceOfType<WrongType>(obj);
Assert.That (MSTest 4.0+)
Assert.That(result.Count > 0); // Auto-captures expression in failure message
StringAssert Class
Note: PreferAssertclass equivalents when available (e.g.,Assert.Contains("expected", actual)overStringAssert.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: PreferAssertclass equivalents when available (e.g.,Assert.Contains).
// ContainmentCollectionAssert.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 checksCollectionAssert.IsSubsetOf(subset, superset);CollectionAssert.IsNotSubsetOf(notSubset, collection);// Element validationCollectionAssert.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")] // MSTest 3.8+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 safetyIEnumerable<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, preferValueTupleorTestDataRowoverIEnumerable<object[]>. Theobject[]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));}// 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 metadatapublic 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],];
TestContext
The TestContext class provides test run information, cancellation support, and output methods. See TestContext documentation for complete reference.
Accessing TestContext
// 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) { }
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 // Current test method nameTestContext.TestDisplayName // Display name (3.7+)TestContext.CurrentTestOutcome // Pass/Fail/InProgressTestContext.TestData // Parameterized test data (3.7+, in TestInitialize/Cleanup)TestContext.TestException // Exception if test failed (3.7+, in TestCleanup)TestContext.DeploymentDirectory // Directory with deployment items
Output and Result Files
// 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 methodsTestContext.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:
// 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 locallypublic void LocalOnlyTest() { }
Parallelization
// Assembly level[assembly: Parallelize(Workers = 4, Scope = ExecutionScope.MethodLevel)]// Disable for specific class[TestClass][DoNotParallelize]public sealed class SequentialTests { }
Work Item Traceability (MSTest 3.8+)
Link tests to work items for traceability in test reports:
// Azure DevOps work items[TestMethod][WorkItem(12345)] // Links to work item #12345public 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
Common Mistakes to Avoid
// ❌ Wrong argument orderAssert.AreEqual(actual, expected);// ✅ CorrectAssert.AreEqual(expected, actual);// ❌ Using ExpectedException (obsolete)[ExpectedException(typeof(ArgumentException))]// ✅ Use Assert.ThrowsAssert.Throws<ArgumentException>(() => Method());// ❌ Using LINQ Single() - unclear exceptionvar item = items.Single();// ✅ Use ContainsSingle - better failure messagevar item = Assert.ContainsSingle(items);// ❌ Hard cast - unclear exceptionvar handler = (MyHandler)result;// ✅ Type assertion - shows actual type on failurevar handler = Assert.IsInstanceOfType<MyHandler>(result);// ❌ Ignoring cancellation tokenawait client.GetAsync(url, CancellationToken.None);// ✅ Flow test cancellationawait client.GetAsync(url, TestContext.CancellationToken);// ❌ Making TestContext nullable - leads to unnecessary null checkspublic TestContext? TestContext { get; set; }// ❌ Using null! - MSTest already suppresses CS8618 for this propertypublic TestContext TestContext { get; set; } = null!;// ✅ Declare without nullable or initializer - MSTest handles the warningpublic 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