$npx -y skills add github/awesome-copilot --skill csharp-mstestGet best practices for MSTest 3.x/4.x unit testing, including modern assertion APIs and data-driven tests
| 1 | # MSTest Best Practices (MSTest 3.x/4.x) |
| 2 | |
| 3 | Your goal is to help me write effective unit tests with modern MSTest, using current APIs and best practices. |
| 4 | |
| 5 | ## Project Setup |
| 6 | |
| 7 | - Use a separate test project with naming convention `[ProjectName].Tests` |
| 8 | - Reference MSTest 3.x+ NuGet packages (includes analyzers) |
| 9 | - Consider using MSTest.Sdk for simplified project setup |
| 10 | - Run tests with `dotnet test` |
| 11 | |
| 12 | ## Test Class Structure |
| 13 | |
| 14 | - Use `[TestClass]` attribute for test classes |
| 15 | - **Seal test classes by default** for performance and design clarity |
| 16 | - Use `[TestMethod]` for test methods (prefer over `[DataTestMethod]`) |
| 17 | - Follow Arrange-Act-Assert (AAA) pattern |
| 18 | - Name tests using pattern `MethodName_Scenario_ExpectedBehavior` |
| 19 | |
| 20 | ```csharp |
| 21 | [TestClass] |
| 22 | public sealed class CalculatorTests |
| 23 | { |
| 24 | [TestMethod] |
| 25 | public void Add_TwoPositiveNumbers_ReturnsSum() |
| 26 | { |
| 27 | // Arrange |
| 28 | var calculator = new Calculator(); |
| 29 | |
| 30 | // Act |
| 31 | var result = calculator.Add(2, 3); |
| 32 | |
| 33 | // Assert |
| 34 | Assert.AreEqual(5, result); |
| 35 | } |
| 36 | } |
| 37 | ``` |
| 38 | |
| 39 | ## Test Lifecycle |
| 40 | |
| 41 | - **Prefer constructors over `[TestInitialize]`** - enables `readonly` fields and follows standard C# patterns |
| 42 | - Use `[TestCleanup]` for cleanup that must run even if test fails |
| 43 | - Combine constructor with async `[TestInitialize]` when async setup is needed |
| 44 | |
| 45 | ```csharp |
| 46 | [TestClass] |
| 47 | public sealed class ServiceTests |
| 48 | { |
| 49 | private readonly MyService _service; // readonly enabled by constructor |
| 50 | |
| 51 | public ServiceTests() |
| 52 | { |
| 53 | _service = new MyService(); |
| 54 | } |
| 55 | |
| 56 | [TestInitialize] |
| 57 | public async Task InitAsync() |
| 58 | { |
| 59 | // Use for async initialization only |
| 60 | await _service.WarmupAsync(); |
| 61 | } |
| 62 | |
| 63 | [TestCleanup] |
| 64 | public void Cleanup() => _service.Reset(); |
| 65 | } |
| 66 | ``` |
| 67 | |
| 68 | ### Execution Order |
| 69 | |
| 70 | 1. **Assembly Initialization** - `[AssemblyInitialize]` (once per test assembly) |
| 71 | 2. **Class Initialization** - `[ClassInitialize]` (once per test class) |
| 72 | 3. **Test Initialization** (for every test method): |
| 73 | 1. Constructor |
| 74 | 2. Set `TestContext` property |
| 75 | 3. `[TestInitialize]` |
| 76 | 4. **Test Execution** - test method runs |
| 77 | 5. **Test Cleanup** (for every test method): |
| 78 | 1. `[TestCleanup]` |
| 79 | 2. `DisposeAsync` (if implemented) |
| 80 | 3. `Dispose` (if implemented) |
| 81 | 6. **Class Cleanup** - `[ClassCleanup]` (once per test class) |
| 82 | 7. **Assembly Cleanup** - `[AssemblyCleanup]` (once per test assembly) |
| 83 | |
| 84 | ## Modern Assertion APIs |
| 85 | |
| 86 | MSTest provides three assertion classes: `Assert`, `StringAssert`, and `CollectionAssert`. |
| 87 | |
| 88 | ### Assert Class - Core Assertions |
| 89 | |
| 90 | ```csharp |
| 91 | // Equality |
| 92 | Assert.AreEqual(expected, actual); |
| 93 | Assert.AreNotEqual(notExpected, actual); |
| 94 | Assert.AreSame(expectedObject, actualObject); // Reference equality |
| 95 | Assert.AreNotSame(notExpectedObject, actualObject); |
| 96 | |
| 97 | // Null checks |
| 98 | Assert.IsNull(value); |
| 99 | Assert.IsNotNull(value); |
| 100 | |
| 101 | // Boolean |
| 102 | Assert.IsTrue(condition); |
| 103 | Assert.IsFalse(condition); |
| 104 | |
| 105 | // Fail/Inconclusive |
| 106 | Assert.Fail("Test failed due to..."); |
| 107 | Assert.Inconclusive("Test cannot be completed because..."); |
| 108 | ``` |
| 109 | |
| 110 | ### Exception Testing (Prefer over `[ExpectedException]`) |
| 111 | |
| 112 | ```csharp |
| 113 | // Assert.Throws - matches TException or derived types |
| 114 | var ex = Assert.Throws<ArgumentException>(() => Method(null)); |
| 115 | Assert.AreEqual("Value cannot be null.", ex.Message); |
| 116 | |
| 117 | // Assert.ThrowsExactly - matches exact type only |
| 118 | var ex = Assert.ThrowsExactly<InvalidOperationException>(() => Method()); |
| 119 | |
| 120 | // Async versions |
| 121 | var ex = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetAsync(url)); |
| 122 | var ex = await Assert.ThrowsExactlyAsync<InvalidOperationException>(async () => await Method()); |
| 123 | ``` |
| 124 | |
| 125 | ### Collection Assertions (Assert class) |
| 126 | |
| 127 | ```csharp |
| 128 | Assert.Contains(expectedItem, collection); |
| 129 | Assert.DoesNotContain(unexpectedItem, collection); |
| 130 | Assert.ContainsSingle(collection); // exactly one element |
| 131 | Assert.HasCount(5, collection); |
| 132 | Assert.IsEmpty(collection); |
| 133 | Assert.IsNotEmpty(collection); |
| 134 | ``` |
| 135 | |
| 136 | ### String Assertions (Assert class) |
| 137 | |
| 138 | ```csharp |
| 139 | Assert.Contains("expected", actualString); |
| 140 | Assert.StartsWith("prefix", actualString); |
| 141 | Assert.EndsWith("suffix", actualString); |
| 142 | Assert.DoesNotStartWith("prefix", actualString); |
| 143 | Assert.DoesNotEndWith("suffix", actualString); |
| 144 | Assert.MatchesRegex(@"\d{3}-\d{4}", phoneNumber); |
| 145 | Assert.DoesNotMatchRegex(@"\d+", textOnly); |
| 146 | ``` |
| 147 | |
| 148 | ### Comparison Assertions |
| 149 | |
| 150 | ```csharp |
| 151 | Assert.IsGreaterThan(lowerBound, actual); |
| 152 | Assert.IsGreaterThanOrEqualTo(lowerBound, actual); |
| 153 | Assert.IsLessThan(upperBound, actual); |
| 154 | Assert.IsLessThanOrEqualTo(upperBound, actual); |
| 155 | Assert.IsInRange(actual, low, high); |
| 156 | Assert.IsPositive(number); |
| 157 | Assert.IsNegative(number); |
| 158 | ``` |
| 159 | |
| 160 | ### Type Assertions |
| 161 | |
| 162 | ```csharp |
| 163 | // MSTest 3.x - uses out parameter |
| 164 | Assert.IsInstanceOfType<MyClass>(obj, out var typed); |
| 165 | typed.DoSomething(); |
| 166 | |
| 167 | // MSTest 4.x - returns typed result directly |
| 168 | var typed = Assert.IsInstanceOfType<MyClass>(obj); |
| 169 | typed.DoSomething(); |
| 170 | |
| 171 | Assert.IsNotInstanceOfTyp |