
Mstest
- 17 installs
- 466 repo stars
- Updated July 25, 2026
- managedcode/dotnet-skills
Helps with testing & qa tasks.
About
mstest is a Claude Code skill for testing & qa. It helps solo builders move faster with AI-assisted coding.
- mstest
- Testing & QA
- AI-coding skill
Mstest 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 mstestAdd 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
MSTest
Trigger On
- the repo uses MSTest
- you need to add, run, debug, or repair MSTest tests
- the repo is moving between VSTest and Microsoft.Testing.Platform
Value
- produce a concrete project delta: code, docs, config, tests, CI, or review artifact
- reduce ambiguity through explicit planning, verification, and final validation skills
- leave reusable project context so future tasks are faster and safer
Do Not Use For
- xUnit projects
- TUnit projects
- generic test strategy with no MSTest-specific mechanics
Inputs
- the nearest
AGENTS.md - the test project file and package references
- the active MSTest runner model
Quick Start
1. Read the nearest AGENTS.md and confirm scope and constraints. 2. Run this skill's Workflow through the Ralph Loop until outcomes are acceptable. 3. Return the Required Result Format with concrete artifacts and verification evidence.
Workflow
1. Detect the MSTest project style first:
MSTest.Sdkproject SDKMSTestmeta-package- legacy package set with explicit
Microsoft.NET.Test.Sdk
2. Read the repo's real test command from AGENTS.md. If the repo has no explicit command yet, start with dotnet test PROJECT_OR_SOLUTION. 3. Keep the runner model consistent:
MSTest.Sdkdefaults to the MSTest runner on Microsoft.Testing.Platform- VSTest is opt-in with
UseVSTest=trueor legacy package choices - do not pass VSTest-only switches or assume legacy
.runsettingsbehavior on Microsoft.Testing.Platform jobs
4. Prefer [DataRow] or DynamicData for stable data-driven coverage. Keep test lifecycle hooks minimal and deterministic. 5. Keep MSTest analyzers enabled and fix findings instead of muting them casually. 6. Align coverage/reporting packages with the active runner.
Bootstrap When Missing
If MSTest is requested but not configured:
1. Detect current framework first:
rg -n "MSTest\\.Sdk|PackageReference Include=\"MSTest\"|xunit|TUnit|UseVSTest|TestingPlatformDotnetTestSupport" -g '*.csproj' .
2. If the repo currently uses xUnit or TUnit, do not auto-migrate. Return status: not_applicable unless migration is explicitly requested. 3. For explicit MSTest adoption, add package(s) to target test project:
dotnet add TEST_PROJECT.csproj package MSTest
4. Document runner model (MSTest.Sdk default MTP vs UseVSTest) in AGENTS.md. 5. Run dotnet test TEST_PROJECT.csproj and return status: configured or status: improved.
Deliver
- MSTest tests that match the repo's runner model
- commands that work in local and CI runs
- explicit guidance for VSTest versus Microsoft.Testing.Platform usage
Validate
- the runner model is documented and consistent
- test commands match that runner
- data-driven tests stay deterministic
- analyzer, coverage, and reporting packages align with the chosen runner
Ralph Loop
Use the Ralph Loop for every task, including docs, architecture, testing, and tooling work.
1. Plan first (mandatory):
- analyze current state
- define target outcome, constraints, and risks
- write a detailed execution plan
- list final validation skills to run at the end, with order and reason
2. Execute one planned step and produce a concrete delta. 3. Review the result and capture findings with actionable next fixes. 4. Apply fixes in small batches and rerun the relevant checks or review steps. 5. Update the plan after each iteration. 6. Repeat until outcomes are acceptable or only explicit exceptions remain. 7. If a dependency is missing, bootstrap it or return status: not_applicable with explicit reason and fallback path.
Required Result Format
status:complete|clean|improved|configured|not_applicable|blockedplan: concise plan and current iteration stepactions_taken: concrete changes madevalidation_skills: final skills run, or skipped with reasonsverification: commands, checks, or review evidence summaryremaining: top unresolved items ornone
For setup-only requests with no execution, return status: configured and exact next commands.
Load References
- references/mstest.md
- references/patterns.md
- references/anti-patterns.md
Example Requests
- "Fix our MSTest runner setup."
- "Add an MSTest regression test."
- "Move this MSTest project to Microsoft.Testing.Platform safely."
{
"version": "1.0.0",
"category": "Testing",
"packages": [
"MSTest",
"MSTest.TestFramework",
"MSTest.TestAdapter"
]
}
MSTest Anti-Patterns Reference
1. Duplicated Test Methods Instead of DataRow
Bad
[TestClass]
public class CalculatorTests
{
[TestMethod]
public void Add_TwoPositiveNumbers_ReturnsSum()
{
var calc = new Calculator();
Assert.AreEqual(5, calc.Add(2, 3));
}
[TestMethod]
public void Add_TwoNegativeNumbers_ReturnsSum()
{
var calc = new Calculator();
Assert.AreEqual(-5, calc.Add(-2, -3));
}
[TestMethod]
public void Add_PositiveAndNegative_ReturnsSum()
{
var calc = new Calculator();
Assert.AreEqual(1, calc.Add(3, -2));
}
[TestMethod]
public void Add_Zeros_ReturnsZero()
{
var calc = new Calculator();
Assert.AreEqual(0, calc.Add(0, 0));
}
}Good
[TestClass]
public class CalculatorTests
{
[TestMethod]
[DataRow(2, 3, 5, DisplayName = "Two positive numbers")]
[DataRow(-2, -3, -5, DisplayName = "Two negative numbers")]
[DataRow(3, -2, 1, DisplayName = "Positive and negative")]
[DataRow(0, 0, 0, DisplayName = "Zeros")]
public void Add_WithVariousInputs_ReturnsExpectedSum(int a, int b, int expected)
{
var calc = new Calculator();
Assert.AreEqual(expected, calc.Add(a, b));
}
}2. Heavy Work in Lifecycle Hooks
Bad
[TestClass]
public class IntegrationTests
{
private static SqlConnection _connection = null!;
private static List<TestUser> _testUsers = null!;
[ClassInitialize]
public static void ClassInit(TestContext context)
{
_connection = new SqlConnection("Server=...");
_connection.Open();
// Heavy seeding operation
_testUsers = [];
for (var i = 0; i < 10000; i++)
{
var user = new TestUser($"user{i}@test.com");
InsertUser(_connection, user);
_testUsers.Add(user);
}
}
[TestMethod]
public void GetUser_ReturnsUser()
{
// Simple test that doesn't need 10000 users
}
}Good
[TestClass]
public class IntegrationTests
{
private SqlConnection _connection = null!;
[TestInitialize]
public void Setup()
{
_connection = new SqlConnection("Server=...");
_connection.Open();
}
[TestCleanup]
public void Teardown()
{
_connection.Dispose();
}
[TestMethod]
public void GetUser_ReturnsUser()
{
// Create only what this test needs
var user = new TestUser("test@example.com");
InsertUser(_connection, user);
var result = GetUserById(_connection, user.Id);
Assert.IsNotNull(result);
}
}3. Test Order Dependencies
Bad
[TestClass]
public class OrderDependentTests
{
private static Order? _createdOrder;
[TestMethod]
[Priority(1)]
public void Test1_CreateOrder()
{
_createdOrder = new Order("ORD-001");
// Assumes this runs first
}
[TestMethod]
[Priority(2)]
public void Test2_UpdateOrder()
{
// Breaks if Test1 didn't run first
_createdOrder!.Status = OrderStatus.Processing;
}
[TestMethod]
[Priority(3)]
public void Test3_DeleteOrder()
{
// Breaks if Test1 and Test2 didn't run
Assert.IsNotNull(_createdOrder);
}
}Good
[TestClass]
public class IndependentOrderTests
{
[TestMethod]
public void CreateOrder_WithValidData_ReturnsOrder()
{
var order = new Order("ORD-001");
Assert.AreEqual("ORD-001", order.Id);
}
[TestMethod]
public void UpdateOrder_WithExistingOrder_UpdatesStatus()
{
var order = new Order("ORD-002"); // Each test creates its own
order.Status = OrderStatus.Processing;
Assert.AreEqual(OrderStatus.Processing, order.Status);
}
[TestMethod]
public void DeleteOrder_WithExistingOrder_RemovesOrder()
{
var order = new Order("ORD-003");
var repository = new InMemoryOrderRepository();
repository.Add(order);
repository.Delete(order.Id);
Assert.IsNull(repository.GetById(order.Id));
}
}4. Missing Assert Statements
Bad
[TestClass]
public class ServiceTests
{
[TestMethod]
public void ProcessData_DoesNotThrow()
{
var service = new DataService();
service.ProcessData("test"); // No assertion
}
[TestMethod]
public async Task LoadAsync_CompletesSuccessfully()
{
var loader = new DataLoader();
await loader.LoadAsync(); // No assertion
}
}Good
[TestClass]
public class ServiceTests
{
[TestMethod]
public void ProcessData_WithValidInput_ReturnsProcessedResult()
{
var service = new DataService();
var result = service.ProcessData("test");
Assert.IsNotNull(result);
Assert.AreEqual("PROCESSED: test", result);
}
[TestMethod]
public async Task LoadAsync_WithValidSource_ReturnsData()
{
var loader = new DataLoader();
var data = await loader.LoadAsync();
Assert.IsNotNull(data);
Assert.IsTrue(data.Count > 0);
}
}5. Catching Exceptions Incorrectly
Bad
[TestClass]
public class ExceptionTests
{
[TestMethod]
public void Validate_WithNull_ThrowsException()
{
try
{
var validator = new Validator();
validator.Validate(null!);
Assert.Fail("Expected exception was not thrown");
}
catch (Exception ex)
{
// Too broad, catches any exception
Assert.IsNotNull(ex);
}
}
}Good
[TestClass]
public class ExceptionTests
{
[TestMethod]
public void Validate_WithNull_ThrowsArgumentNullException()
{
var validator = new Validator();
var exception = Assert.ThrowsException<ArgumentNullException>(
() => validator.Validate(null!));
Assert.AreEqual("input", exception.ParamName);
}
[TestMethod]
public async Task ValidateAsync_WithNull_ThrowsArgumentNullException()
{
var validator = new AsyncValidator();
var exception = await Assert.ThrowsExceptionAsync<ArgumentNullException>(
async () => await validator.ValidateAsync(null!));
Assert.AreEqual("input", exception.ParamName);
}
}6. Shared Mutable State Between Tests
Bad
[TestClass]
public class SharedStateTests
{
private static List<string> _items = []; // Static mutable state
[TestMethod]
public void AddItem_IncreasesCount()
{
_items.Add("item1");
Assert.AreEqual(1, _items.Count); // May fail if other tests ran
}
[TestMethod]
public void AddTwoItems_CountIsTwo()
{
_items.Add("item2");
_items.Add("item3");
Assert.AreEqual(2, _items.Count); // Fails because state leaked
}
}Good
[TestClass]
public class IsolatedStateTests
{
private List<string> _items = null!;
[TestInitialize]
public void Setup()
{
_items = []; // Fresh instance per test
}
[TestMethod]
public void AddItem_IncreasesCount()
{
_items.Add("item1");
Assert.AreEqual(1, _items.Count);
}
[TestMethod]
public void AddTwoItems_CountIsTwo()
{
_items.Add("item1");
_items.Add("item2");
Assert.AreEqual(2, _items.Count);
}
}7. Ignoring Async/Await in Tests
Bad
[TestClass]
public class AsyncTests
{
[TestMethod]
public void LoadData_Completes() // Not async
{
var service = new DataService();
var task = service.LoadAsync(); // Fire and forget
// Test completes before async work finishes
}
[TestMethod]
public void ProcessAsync_WithResult()
{
var service = new DataService();
var result = service.ProcessAsync().Result; // Blocking call, can deadlock
Assert.IsNotNull(result);
}
}Good
[TestClass]
public class AsyncTests
{
[TestMethod]
public async Task LoadData_CompletesSuccessfully()
{
var service = new DataService();
var data = await service.LoadAsync();
Assert.IsNotNull(data);
}
[TestMethod]
public async Task ProcessAsync_WithValidInput_ReturnsResult()
{
var service = new DataService();
var result = await service.ProcessAsync();
Assert.IsNotNull(result);
}
}8. Hardcoded Test Data Paths
Bad
[TestClass]
public class FileTests
{
[TestMethod]
public void ReadConfig_ParsesCorrectly()
{
var config = ConfigReader.Read(@"C:\Users\john\project\test-data\config.json");
Assert.IsNotNull(config);
}
}Good
[TestClass]
public class FileTests
{
[TestMethod]
public void ReadConfig_ParsesCorrectly()
{
var testDataPath = Path.Combine(
AppContext.BaseDirectory,
"TestData",
"config.json");
var config = ConfigReader.Read(testDataPath);
Assert.IsNotNull(config);
}
[TestMethod]
public void ReadConfig_WithEmbeddedResource_ParsesCorrectly()
{
using var stream = GetType().Assembly
.GetManifestResourceStream("MyTests.TestData.config.json");
var config = ConfigReader.Read(stream!);
Assert.IsNotNull(config);
}
}9. Testing Implementation Details
Bad
[TestClass]
public class ImplementationTests
{
[TestMethod]
public void Cache_UsesCorrectInternalStructure()
{
var cache = new Cache<string>();
cache.Add("key", "value");
// Accessing private field through reflection
var dictionary = typeof(Cache<string>)
.GetField("_dictionary", BindingFlags.NonPublic | BindingFlags.Instance)!
.GetValue(cache) as Dictionary<string, string>;
Assert.AreEqual(1, dictionary!.Count);
}
}Good
[TestClass]
public class BehaviorTests
{
[TestMethod]
public void Cache_AfterAdd_ReturnsStoredValue()
{
var cache = new Cache<string>();
cache.Add("key", "value");
var result = cache.Get("key");
Assert.AreEqual("value", result);
}
[TestMethod]
public void Cache_AfterAdd_ContainsKey()
{
var cache = new Cache<string>();
cache.Add("key", "value");
Assert.IsTrue(cache.ContainsKey("key"));
}
}10. Overly Strict DateTime Assertions
Bad
[TestClass]
public class TimestampTests
{
[TestMethod]
public void CreateOrder_SetsCreatedAt()
{
var order = new Order();
Assert.AreEqual(DateTime.Now, order.CreatedAt); // Flaky, timing issue
}
}Good
[TestClass]
public class TimestampTests
{
[TestMethod]
public void CreateOrder_SetsCreatedAtToApproximatelyNow()
{
var before = DateTime.UtcNow;
var order = new Order();
var after = DateTime.UtcNow;
Assert.IsTrue(order.CreatedAt >= before && order.CreatedAt <= after);
}
[TestMethod]
public void CreateOrder_WithTimeProvider_SetsExpectedTime()
{
var fixedTime = new DateTime(2024, 1, 15, 10, 30, 0, DateTimeKind.Utc);
var timeProvider = new FakeTimeProvider(fixedTime);
var order = new Order(timeProvider);
Assert.AreEqual(fixedTime, order.CreatedAt);
}
}11. Missing Cleanup for External Resources
Bad
[TestClass]
public class ResourceLeakTests
{
[TestMethod]
public void WriteFile_CreatesFile()
{
var path = Path.GetTempFileName();
File.WriteAllText(path, "test content");
Assert.IsTrue(File.Exists(path));
// File never deleted, accumulates over test runs
}
[TestMethod]
public void OpenConnection_Succeeds()
{
var connection = new SqlConnection("...");
connection.Open();
Assert.AreEqual(ConnectionState.Open, connection.State);
// Connection never disposed
}
}Good
[TestClass]
public class ProperResourceTests
{
private readonly List<string> _tempFiles = [];
[TestCleanup]
public void Cleanup()
{
foreach (var file in _tempFiles)
{
if (File.Exists(file))
{
File.Delete(file);
}
}
}
[TestMethod]
public void WriteFile_CreatesFile()
{
var path = Path.GetTempFileName();
_tempFiles.Add(path);
File.WriteAllText(path, "test content");
Assert.IsTrue(File.Exists(path));
}
[TestMethod]
public void OpenConnection_Succeeds()
{
using var connection = new SqlConnection("...");
connection.Open();
Assert.AreEqual(ConnectionState.Open, connection.State);
}
}12. Mixing VSTest and Microsoft.Testing.Platform Patterns
Bad
// Project uses MSTest.Sdk (Microsoft.Testing.Platform by default)
// but test code uses VSTest-specific patterns
[TestClass]
public class MixedRunnerTests
{
[TestMethod]
[DeploymentItem("TestData/file.json")] // VSTest-specific, may not work
public void Test_WithDeploymentItem()
{
// ...
}
}Good
// For MSTest.Sdk projects, use runner-agnostic patterns
[TestClass]
public class RunnerAgnosticTests
{
[TestMethod]
public void Test_WithEmbeddedResource()
{
using var stream = GetType().Assembly
.GetManifestResourceStream("MyTests.TestData.file.json");
// ...
}
[TestMethod]
public void Test_WithTestDataFolder()
{
var path = Path.Combine(AppContext.BaseDirectory, "TestData", "file.json");
// ...
}
}Sources
MSTest in MCAF
Open/Free Status
- open source
- free to use
Install
Project template:
dotnet new mstestOr add the current MSTest meta-package to an existing test project:
dotnet add package MSTestVerify First
Before adding packages, check which MSTest model the repo already uses:
rg -n "MSTest\\.Sdk|UseVSTest|PackageReference Include=\"MSTest\"|Microsoft\\.NET\\.Test\\.Sdk|TestingPlatformDotnetTestSupport" -g '*.csproj' .Use this reference when the repository already chose MSTest and you need framework-specific commands, package checks, or CI guardrails.
Detect the MSTest Model
Use the project file as the source of truth:
rg -n "MSTest\\.Sdk|UseVSTest|PackageReference Include=\"MSTest\"|Microsoft\\.NET\\.Test\\.Sdk|TestingPlatformDotnetTestSupport" -g '*.csproj' .Typical markers:
MSTest.Sdk: modern MSTest project SDK, Microsoft.Testing.Platform by defaultUseVSTest=true: explicit VSTest fallbackMSTestmeta-package: framework, adapter, analyzers, and runner-related packages packaged together
Common Commands
Start with the repo's test command from AGENTS.md. If the repo has not documented one yet, these are the safe defaults:
dotnet test MySolution.sln
dotnet test tests/MyProject.Tests/MyProject.Tests.csproj
dotnet test tests/MyProject.Tests/MyProject.Tests.csproj --no-buildFocused VSTest-style run:
dotnet test tests/MyProject.Tests/MyProject.Tests.csproj --filter "FullyQualifiedName~Namespace.TypeName"New-project template:
The current dotnet new mstest template can target either VSTest or MSTest as the runner, and MSTest.Sdk uses Microsoft.Testing.Platform by default.
CI Notes
- Document the runner choice in
AGENTS.md. - If the project uses
MSTest.Sdk, assume Microsoft.Testing.Platform unless the project opts back into VSTest. - Keep coverage driver aligned with the runner:
- VSTest:
--collect:"XPlat Code Coverage"orcoverlet.collector - Microsoft.Testing.Platform: MSTest SDK coverage extension or
coverlet.MTP - Keep MSTest analyzers enabled.
Good Defaults
- prefer
[DataRow]orDynamicDataover duplicated test methods - keep
ClassInitialize,ClassCleanup, and similar hooks short and deterministic - avoid runner migrations and behavior changes in the same PR unless the current runner setup is already broken
Sources
MSTest Patterns Reference
Data-Driven Testing Patterns
DataRow for Inline Test Data
[TestClass]
public class CalculatorTests
{
[TestMethod]
[DataRow(2, 3, 5)]
[DataRow(0, 0, 0)]
[DataRow(-1, 1, 0)]
[DataRow(int.MaxValue, 0, int.MaxValue)]
public void Add_WithVariousInputs_ReturnsExpectedSum(int a, int b, int expected)
{
var calculator = new Calculator();
var result = calculator.Add(a, b);
Assert.AreEqual(expected, result);
}
}DataRow with Display Names
[TestClass]
public class ValidationTests
{
[TestMethod]
[DataRow("", false, DisplayName = "Empty string is invalid")]
[DataRow(" ", false, DisplayName = "Whitespace is invalid")]
[DataRow("valid@email.com", true, DisplayName = "Valid email passes")]
[DataRow("invalid", false, DisplayName = "Missing @ is invalid")]
public void IsValidEmail_WithVariousInputs_ReturnsExpectedResult(string email, bool expected)
{
var validator = new EmailValidator();
Assert.AreEqual(expected, validator.IsValid(email));
}
}DynamicData for Complex Test Data
[TestClass]
public class OrderProcessorTests
{
public static IEnumerable<object[]> OrderTestData =>
[
[new Order { Items = [], Total = 0 }, OrderStatus.Empty],
[new Order { Items = [new("Item1", 10)], Total = 10 }, OrderStatus.Valid],
[new Order { Items = [new("Item1", -5)], Total = -5 }, OrderStatus.Invalid]
];
[TestMethod]
[DynamicData(nameof(OrderTestData))]
public void ProcessOrder_WithVariousOrders_ReturnsCorrectStatus(Order order, OrderStatus expected)
{
var processor = new OrderProcessor();
var result = processor.Process(order);
Assert.AreEqual(expected, result);
}
}DynamicData from Method
[TestClass]
public class ParserTests
{
public static IEnumerable<object[]> GetParseTestCases()
{
yield return ["123", 123];
yield return ["456", 456];
yield return ["-789", -789];
}
[TestMethod]
[DynamicData(nameof(GetParseTestCases), DynamicDataSourceType.Method)]
public void Parse_WithValidInput_ReturnsExpectedValue(string input, int expected)
{
var result = int.Parse(input);
Assert.AreEqual(expected, result);
}
}Lifecycle Hook Patterns
TestInitialize and TestCleanup
[TestClass]
public class DatabaseTests
{
private TestDatabaseContext _context = null!;
[TestInitialize]
public void Setup()
{
_context = new TestDatabaseContext();
_context.Database.EnsureCreated();
}
[TestCleanup]
public void Teardown()
{
_context.Database.EnsureDeleted();
_context.Dispose();
}
[TestMethod]
public async Task CreateUser_WithValidData_PersistsToDatabase()
{
var user = new User("test@example.com", "TestUser");
_context.Users.Add(user);
await _context.SaveChangesAsync();
Assert.AreEqual(1, await _context.Users.CountAsync());
}
}ClassInitialize and ClassCleanup
[TestClass]
public class IntegrationTests
{
private static HttpClient _client = null!;
private static WebApplicationFactory<Program> _factory = null!;
[ClassInitialize]
public static void ClassSetup(TestContext context)
{
_factory = new WebApplicationFactory<Program>();
_client = _factory.CreateClient();
}
[ClassCleanup]
public static void ClassTeardown()
{
_client.Dispose();
_factory.Dispose();
}
[TestMethod]
public async Task GetEndpoint_ReturnsSuccess()
{
var response = await _client.GetAsync("/api/health");
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
}AssemblyInitialize and AssemblyCleanup
[TestClass]
public class GlobalTestSetup
{
[AssemblyInitialize]
public static void AssemblySetup(TestContext context)
{
// One-time setup for entire test assembly
Environment.SetEnvironmentVariable("TEST_MODE", "true");
}
[AssemblyCleanup]
public static void AssemblyTeardown()
{
Environment.SetEnvironmentVariable("TEST_MODE", null);
}
}Parallel Testing Patterns
Parallel Execution at Class Level
// Enable parallel execution in .runsettings or project file
// <Parallelize Workers="4" Scope="ClassLevel" />
[TestClass]
public class IndependentTestsA
{
[TestMethod]
public void TestA1() { /* ... */ }
[TestMethod]
public void TestA2() { /* ... */ }
}
[TestClass]
public class IndependentTestsB
{
[TestMethod]
public void TestB1() { /* ... */ }
}Disabling Parallelism for Specific Tests
[TestClass]
[DoNotParallelize]
public class SequentialDatabaseTests
{
// Tests in this class run sequentially
[TestMethod]
public void Test1_CreateRecord() { /* ... */ }
[TestMethod]
public void Test2_UpdateRecord() { /* ... */ }
}Thread-Safe Test Fixtures
[TestClass]
public class ThreadSafeTests
{
private static readonly Lock _lock = new();
private static int _sharedCounter;
[TestMethod]
public void IncrementCounter_ThreadSafe()
{
lock (_lock)
{
_sharedCounter++;
Assert.IsTrue(_sharedCounter > 0);
}
}
}Async Testing Patterns
Basic Async Test
[TestClass]
public class AsyncServiceTests
{
[TestMethod]
public async Task GetDataAsync_WhenCalled_ReturnsData()
{
var service = new DataService();
var result = await service.GetDataAsync();
Assert.IsNotNull(result);
}
}Testing Async Exceptions
[TestClass]
public class AsyncExceptionTests
{
[TestMethod]
public async Task ProcessAsync_WithInvalidInput_ThrowsArgumentException()
{
var service = new ValidationService();
await Assert.ThrowsExceptionAsync<ArgumentException>(
async () => await service.ProcessAsync(null!));
}
}Testing Cancellation
[TestClass]
public class CancellationTests
{
[TestMethod]
public async Task LongRunningOperation_WhenCancelled_ThrowsOperationCanceled()
{
using var cts = new CancellationTokenSource();
var service = new LongRunningService();
var task = service.ProcessAsync(cts.Token);
await cts.CancelAsync();
await Assert.ThrowsExceptionAsync<OperationCanceledException>(() => task);
}
}Dependency Injection Patterns
Test Class with Primary Constructor
[TestClass]
public class ServiceTests
{
private readonly Mock<IRepository> _mockRepository = new();
private readonly Mock<ILogger<OrderService>> _mockLogger = new();
private OrderService CreateService() =>
new(_mockRepository.Object, _mockLogger.Object);
[TestMethod]
public async Task CreateOrder_WithValidOrder_SavesAndReturnsOrder()
{
var order = new Order("ORD-001", 100m);
_mockRepository.Setup(r => r.SaveAsync(order))
.ReturnsAsync(order);
var service = CreateService();
var result = await service.CreateOrderAsync(order);
Assert.AreEqual("ORD-001", result.Id);
_mockRepository.Verify(r => r.SaveAsync(order), Times.Once);
}
}Test Base Class with Common Setup
public abstract class ServiceTestBase<TService> where TService : class
{
protected Mock<ILogger<TService>> MockLogger { get; } = new();
protected Mock<IConfiguration> MockConfiguration { get; } = new();
[TestInitialize]
public virtual void BaseSetup()
{
MockConfiguration.Setup(c => c["Environment"]).Returns("Test");
}
}
[TestClass]
public class UserServiceTests : ServiceTestBase<UserService>
{
private readonly Mock<IUserRepository> _mockRepo = new();
[TestMethod]
public async Task GetUser_ReturnsUser()
{
_mockRepo.Setup(r => r.GetByIdAsync(1))
.ReturnsAsync(new User(1, "Test"));
var service = new UserService(_mockRepo.Object, MockLogger.Object);
var user = await service.GetUserAsync(1);
Assert.AreEqual("Test", user.Name);
}
}Assertion Patterns
Collection Assertions
[TestClass]
public class CollectionAssertionTests
{
[TestMethod]
public void GetUsers_ReturnsExpectedCollection()
{
var service = new UserService();
var users = service.GetActiveUsers();
CollectionAssert.IsNotNull(users);
CollectionAssert.AllItemsAreNotNull(users);
CollectionAssert.AllItemsAreInstancesOfType(users, typeof(User));
CollectionAssert.AreEqual(new[] { "Alice", "Bob" }, users.Select(u => u.Name).ToList());
}
}String Assertions
[TestClass]
public class StringAssertionTests
{
[TestMethod]
public void FormatMessage_ContainsExpectedParts()
{
var formatter = new MessageFormatter();
var result = formatter.Format("Hello", "World");
StringAssert.Contains(result, "Hello");
StringAssert.StartsWith(result, "Message:");
StringAssert.EndsWith(result, ".");
StringAssert.Matches(result, new Regex(@"Message: .+ - .+\."));
}
}Custom Assert Extensions
public static class CustomAssert
{
public static void IsWithinRange<T>(T value, T min, T max) where T : IComparable<T>
{
if (value.CompareTo(min) < 0 || value.CompareTo(max) > 0)
{
throw new AssertFailedException(
$"Expected {value} to be between {min} and {max}");
}
}
}
[TestClass]
public class RangeTests
{
[TestMethod]
public void Calculate_ReturnsValueInExpectedRange()
{
var calculator = new Calculator();
var result = calculator.RandomInRange(1, 100);
CustomAssert.IsWithinRange(result, 1, 100);
}
}Test Organization Patterns
Arrange-Act-Assert Structure
[TestClass]
public class ShoppingCartTests
{
[TestMethod]
public void AddItem_WhenCartEmpty_IncreasesItemCount()
{
// Arrange
var cart = new ShoppingCart();
var item = new CartItem("SKU-001", "Widget", 9.99m);
// Act
cart.AddItem(item);
// Assert
Assert.AreEqual(1, cart.ItemCount);
Assert.AreEqual(9.99m, cart.Total);
}
}Test Categories
[TestClass]
public class MixedTests
{
[TestMethod]
[TestCategory("Unit")]
public void UnitTest_FastAndIsolated()
{
// Fast, no external dependencies
}
[TestMethod]
[TestCategory("Integration")]
public async Task IntegrationTest_UsesDatabase()
{
// Requires database
await Task.CompletedTask;
}
[TestMethod]
[TestCategory("Smoke")]
public void SmokeTest_BasicFunctionality()
{
// Quick sanity check
}
}Test Priority
[TestClass]
public class PrioritizedTests
{
[TestMethod]
[Priority(1)]
public void CriticalPath_Test()
{
// Most important, run first
}
[TestMethod]
[Priority(2)]
public void Secondary_Test()
{
// Run after priority 1
}
}