
Writing Mstest Tests
- 17 installs
- 466 repo stars
- Updated July 25, 2026
- managedcode/dotnet-skills
Helps with testing & qa tasks.
About
writing-mstest-tests is a Claude Code skill for testing & qa. It helps solo builders move faster with AI-assisted coding.
- writing-mstest-tests
- Testing & QA
- AI-coding skill
Writing Mstest Tests by the numbers
- 17 all-time installs (skills.sh)
- +1 installs in the week ending Aug 2, 2026 (Skillselion tracking)
- Ranked #1,458 of 2,153 Testing & QA skills by installs in the Skillselion catalog
- Data as of Aug 3, 2026 (Skillselion catalog sync)
npx skills add https://github.com/managedcode/dotnet-skills --skill writing-mstest-testsAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 17 |
|---|---|
| repo stars | ★ 466 |
| Last updated | July 25, 2026 |
| Repository | managedcode/dotnet-skills ↗ |
What it does
Helps with testing & qa tasks.
Files
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 asks to replace
Assert.IsTruewith more specific assertions (collections, nulls, types, comparisons) - User asks to replace hard casts with type-checking assertions in tests
- User needs help fixing a specific MSTest test bug or failing assertion
- User asks to fix swapped
Assert.AreEqualargument order (expected first, actual second) - User asks to convert
DynamicDatafromIEnumerable<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-testsskill) - 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 |
Response Guidelines
- Specific API or pattern questions (assertions, data-driven, lifecycle): Jump directly to the relevant workflow step. Do not follow the full workflow.
- Write new tests from scratch: Follow the full workflow.
- Review and fix existing tests: Fix only the issues present. Do not add unrelated improvements.
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
MSTestmetapackage: 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:
<!-- 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:
{
"msbuild-sdks": {
"MSTest.Sdk": "3.8.2"
}
}<!-- 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
sealedfor 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
[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
Pick the most specific assertion for each test scenario. More specific assertions produce better failure messages and make the test's intent clear:
| What you are testing | Assertion |
|---|---|
| Two values are equal | Assert.AreEqual(expected, actual) |
| Same object instance (reference identity) | Assert.AreSame(expected, actual) |
| Value is null | Assert.IsNull(value) |
| Value is not null | Assert.IsNotNull(value) |
| Collection is empty | Assert.IsEmpty(collection) |
| Collection is not empty | Assert.IsNotEmpty(collection) |
| Collection has exactly N items | Assert.HasCount(N, collection) |
| Collection contains an item | Assert.Contains(item, collection) |
| Collection does not contain an item | Assert.DoesNotContain(item, collection) |
| Object is a specific type | Assert.IsInstanceOfType<T>(value) |
| Code throws an exception | Assert.ThrowsExactly<T>(() => ...) |
Prefer Assert class methods over StringAssert or CollectionAssert where both exist.
Equality, null, and reference checks
Assert.AreEqual(expected, actual); // Value equality
Assert.AreSame(expected, actual); // Reference equality -- same object instance
Assert.IsNull(value);
Assert.IsNotNull(value);Exception testing -- use Assert.Throws instead of [ExpectedException]
// 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>matchesTor any derived typeAssert.ThrowsExactly<T>matches only the exact typeT
Collection assertions
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 == 0) | Assert.IsEmpty(list) |
Assert.IsTrue(list.Count() == 3) | Assert.HasCount(3, list) |
Assert.IsTrue(x != null) | Assert.IsNotNull(x) |
Assert.IsTrue(x == null) | Assert.IsNull(x) |
Assert.AreEqual(a, b) for same instance | Assert.AreSame(a, b) -- reference identity |
Assert.IsTrue(!list.Contains(item)) | Assert.DoesNotContain(item, list) |
list.Single(predicate) + Assert.IsNotNull | Assert.ContainsSingle(list) |
Assert.IsTrue(list.Contains(item)) | Assert.Contains(item, list) |
String assertions
Assert.Contains("expected", actualString);
Assert.StartsWith("prefix", actualString);
Assert.EndsWith("suffix", actualString);
Assert.MatchesRegex(@"\d{3}-\d{4}", phoneNumber);Type assertions
// 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
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
[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:
[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>:
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
readonlyfields 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
TestContextvia constructor (MSTest 3.6+)
[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
TestContextproperty injection: Constructor -> setTestContextproperty ->[TestInitialize] - With constructor injection of
TestContext: Constructor (receivesTestContext) ->[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]:
[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.
[TestMethod]
[Retry(3)]
public void ExternalService_EventuallyResponds() { }Conditional execution (MSTest 3.10+)
[TestMethod]
[OSCondition(OperatingSystems.Windows)]
public void WindowsRegistry_ReadsValue() { }
[TestMethod]
[CICondition(ConditionMode.Exclude)]
public void LocalOnly_InteractiveTest() { }Parallelization
[assembly: Parallelize(Workers = 4, Scope = ExecutionScope.MethodLevel)]
[TestClass]
[DoNotParallelize] // Opt out specific classes
public sealed class DatabaseIntegrationTests { }{
"version": "0.1.0",
"category": "Testing",
"compatibility": "Requires a .NET test project or solution.",
"package_prefix": "MSTest"
}