Skip to main content

writing-mstest-tests

Best practices for writing new MSTest 3.x/4.x unit tests and implementing concrete fixes in existing MSTest code. Use when the user asks to write, create, implement, repair, or modernize tests (including fix-it prompts such as 'something seems off, fix issues'). Primary fit for direct code changes like correcting swapped Assert.AreEqual argument order, replacing outdated assertion patterns, and converting DynamicData from IEnumerable<object[]> to ValueTuple-based data sets. Covers modern assertions, data-driven tests, test lifecycle, MSTest.Sdk, sealed classes, Assert.Throws, DynamicData with ValueTuples, TestContext, and conditional execution. Do NOT use for broad test quality audits, flaky-test investigations, or test smell detection reports — use test-anti-patterns instead.

Source facts

Repository
runceel/ReactiveProperty
Last source activity
June 20, 2026 at 11:21
Detected SKILL.md language
English
Stars
944
Forks
105

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
description
Best practices for writing new MSTest 3.x/4.x unit tests and implementing concrete fixes in existing MSTest code. Use when the user asks to write, create, implement, repair, or modernize tests (including fix-it prompts such as 'something seems off, fix issues'). Primary fit for direct code changes like correcting swapped Assert.AreEqual argument order, replacing outdated assertion patterns, and converting DynamicData from IEnumerable<object[]> to ValueTuple-based data sets. Covers modern assertions, data-driven tests, test lifecycle, MSTest.Sdk, sealed classes, Assert.Throws, DynamicData with ValueTuples, TestContext, and conditional execution. Do NOT use for broad test quality audits, flaky-test investigations, or test smell detection reports — use test-anti-patterns instead.
metadata
{"github-path":"plugins/dotnet-test/skills/writing-mstest-tests","github-pinned":"v1.0.0","github-ref":"refs/tags/v1.0.0","github-repo":"https://github.com/dotnet/skills","github-tree-sha":"02ca0ad8517efa911ef1748d70b6942bcbaff7d2"}
name
writing-mstest-tests
# Writing MSTest Tests Help users write effective, modern unit tests with MSTest 3.x/4.x using current APIs and best practices. ## When to Use - User wants to write new MSTest unit tests - User wants to improve or modernize existing MSTest tests by implementing concrete fixes - User asks about MSTest assertion APIs, data-driven patterns, or test lifecycle - User needs help fixing a specific MSTest test bug or failing assertion - User asks to fix swapped `Assert.AreEqual` argument order (expected first, actual second) - User asks to convert `DynamicData` from `IEnumerable<object[]>` to ValueTuple-based data ## When Not to Use - User needs a test quality audit, anti-pattern detection, or flaky-test investigation (use `test-anti-patterns`) - User needs to run or execute tests (use the `run-tests` skill) - User needs to upgrade from MSTest v1/v2 to v3 (use `migrate-mstest-v1v2-to-v3`) - User needs to upgrade from MSTest v3 to v4 (use `migrate-mstest-v3-to-v4`) - User needs CI/CD pipeline configuration - User is using xUnit, NUnit, or TUnit (not MSTest) ## Inputs | Input | Required | Description | |-------|----------|-------------| | Code under test | No | The production code to be tested | | Existing test code | No | Current tests to fix, update, or modernize | | Test scenario description | No | What behavior the user wants to test | ## Workflow ### Step 1: Determine project setup 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> ``` ### Step 2: Write test classes following conventions 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); } } ``` ### Step 3: Use modern assertion APIs Use the correct assertion for each scenario. Prefer `Assert` class methods over `StringAssert` or `CollectionAssert` where both exist. #### Equality and null checks ```csharp Assert.AreEqual(expected, actual); // Value equality Assert.AreSame(expected, actual); // Reference equality Assert.IsNull(value); Assert.IsNotNull(value); ``` #### Exception testing -- use `Assert.Throws` instead of `[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` #### 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); Assert.IsNotEmpty(collection); ``` Replace generic `Assert.IsTrue` with specialized assertions -- they give better failure messages: | Instead of | Use | |---|---| | `Assert.IsTrue(list.Count > 0)` | `Assert.IsNotEmpty(list)` | | `Assert.IsTrue(list.Count() == 3)` | `Assert.HasCount(3, list)` | | `Assert.IsTrue(x != null)` | `Assert.IsNotNull(x)` | | `list.Single(predicate)` + `Assert.IsNotNull` | `Assert.ContainsSingle(list)` | | `Assert.IsTrue(list.Contains(item))` | `Assert.Contains(item, list)` | #### String assertions ```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); ``` #### Comparison assertions ```csharp Assert.IsGreaterThan(lowerBound, actual); Assert.IsLessThan(upperBound, actual); Assert.IsInRange(actual, low, high); ``` ### Step 4: Use data-driven tests for multiple inputs #### DataRow for inline values ```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) 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" }, ]; ``` ### Step 5: Handle test lifecycle correctly - **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(); } ``` #### 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 ### Step 6: Apply cancellation and timeout patterns 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); } ``` ### Step 7: Use advanced features where appropriate #### Retry flaky tests (MSTest 3.9+) Use only for genuinely flaky external dependencies (network, file system), not to paper over race conditions or shared state issues. ```csharp [TestMethod] [Retry(3)] public void ExternalService_EventuallyResponds() { } ``` #### Conditional execution (MSTest 3.10+) ```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 { } ``` ## Validation - [ ] Test classes are `sealed` - [ ] Test methods follow `MethodName_Scenario_ExpectedBehavior` naming - [ ] `Assert.ThrowsExactly<T>` used instead of `[ExpectedException]` - [ ] Specialized assertions used instead of `Assert.IsTrue` (e.g., `Assert.IsNotNull`, `Assert.AreEqual`) - [ ] DynamicData uses ValueTuple return types instead of `IEnumerable<object[]>` - [ ] Sync initialization done in the constructor, not `[TestInitialize]` - [ ] `TestContext.CancellationToken` passed to async calls in tests with `[Timeout]` - [ ] Project builds with zero errors and all tests pass ## Common Pitfalls | Pitfall | Solution | |---------|----------| | `Assert.AreEqual(actual, expected)` -- swapped arguments | Always put expected first: `Assert.AreEqual(expected, actual)`. Failure messages show "Expected: X, Actual: Y" so wrong order makes messages confusing | | `[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 classes | Seal test classes by default for performance |
View on GitHub