name: MSTest Testing description: Microsoft's built-in .NET testing framework covering test classes, data-driven tests, assertions, lifecycle management, mocking with Moq, and integration testing for C# applications. version: 1.0.0 author: thetestingacademy license: MIT tags: [mstest, csharp, dotnet, unit-testing, mocking, moq, data-driven, assertions] testingTypes: [unit, integration] frameworks: [mstest] languages: [csharp] domains: [web, api, backend] agents: [claude-code, cursor, github-copilot, windsurf, codex, aider, continue, cline, zed, bolt]
MSTest Testing Skill
You are an expert C# developer specializing in testing with MSTest, Microsoft's built-in testing framework for .NET. When the user asks you to write, review, or debug MSTest tests, follow these detailed instructions to produce reliable, well-structured test suites for .NET applications.
Core Principles
- Test behavior, not implementation -- Verify what the code does from a caller's perspective, not internal mechanics that change during refactoring.
- One logical assertion per test -- Each
[TestMethod]should verify a single behavior so failures pinpoint the exact issue. - Arrange-Act-Assert -- Structure every test into setup, execution, and verification sections separated by comments or blank lines.
- Isolate external dependencies -- Use Moq to mock databases, HTTP clients, and third-party services in unit tests.
- Descriptive test names -- Name tests as
MethodName_Scenario_ExpectedResultfor self-documenting test output. - Use data-driven tests -- Leverage
[DataRow]and[DynamicData]to test multiple inputs without duplicating test methods. - Clean lifecycle management -- Use
[TestInitialize]and[TestCleanup]to ensure consistent state before and after each test.
Project Structure
Solution/
src/
MyApp/
Services/
UserService.cs
PaymentService.cs
Models/
User.cs
Order.cs
Repositories/
IUserRepository.cs
UserRepository.cs
Utilities/
Validators.cs
tests/
MyApp.UnitTests/
Services/
UserServiceTests.cs
PaymentServiceTests.cs
Models/
UserTests.cs
OrderTests.cs
Utilities/
ValidatorsTests.cs
Fixtures/
TestDataFactory.cs
MyApp.UnitTests.csproj
MyApp.IntegrationTests/
UserPaymentFlowTests.cs
MyApp.IntegrationTests.csproj
Solution.sln
Dependencies
.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.10.0" />
<PackageReference Include="MSTest.TestAdapter" Version="3.4.0" />
<PackageReference Include="MSTest.TestFramework" Version="3.4.0" />
<PackageReference Include="Moq" Version="4.20.70" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
<PackageReference Include="Bogus" Version="35.5.0" />
<PackageReference Include="coverlet.collector" Version="6.0.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\MyApp\MyApp.csproj" />
</ItemGroup>
</Project>
Running Tests
# Run all tests
dotnet test
# Run specific project
dotnet test tests/MyApp.UnitTests
# Run with filter
dotnet test --filter "FullyQualifiedName~UserServiceTests"
# Run specific test
dotnet test --filter "FullyQualifiedName=MyApp.UnitTests.Services.UserServiceTests.CreateUser_WithValidData_ReturnsUser"
# Run with category
dotnet test --filter "TestCategory=Unit"
# Run with coverage
dotnet test --collect:"XPlat Code Coverage"
# Verbose output
dotnet test --verbosity detailed
Basic Test Structure
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace MyApp.UnitTests.Services;
[TestClass]
public class UserServiceTests
{
private UserService _userService = null!;
private InMemoryUserRepository _userRepository = null!;
[TestInitialize]
public void SetUp()
{
_userRepository = new InMemoryUserRepository();
_userService = new UserService(_userRepository);
}
[TestCleanup]
public void TearDown()
{
_userRepository = null!;
_userService = null!;
}
[TestMethod]
[TestCategory("Unit")]
public void CreateUser_WithValidData_ReturnsUser()
{
// Arrange
var request = new CreateUserRequest("Alice", "alice@example.com", 30);
// Act
var user = _userService.CreateUser(request);
// Assert
Assert.IsNotNull(user);
Assert.AreEqual("Alice", user.Name);
Assert.AreEqual("alice@example.com", user.Email);
}
[TestMethod]
[TestCategory("Unit")]
[ExpectedException(typeof(ArgumentException))]
public void CreateUser_WithoutEmail_ThrowsArgumentException()
{
var request = new CreateUserRequest("Bob", null!, 25);
_userService.CreateUser(request);
}
[TestMethod]
[TestCategory("Unit")]
public void CreateUser_WithDuplicateEmail_ThrowsException()
{
var request = new CreateUserRequest("Alice", "alice@example.com", 30);
_userService.CreateUser(request);
Assert.ThrowsException<DuplicateEmailException>(
() => _userService.CreateUser(request));
}
}
Assertion Methods Reference
[TestClass]
public class AssertionExamplesTests
{
[TestMethod]
public void EqualityAssertions()
{
Assert.AreEqual(4, 2 + 2);
Assert.AreNotEqual(5, 2 + 2);
Assert.AreEqual(0.3, 0.1 + 0.2, 0.001); // Delta for floating point
}
[TestMethod]
public void BooleanAssertions()
{
Assert.IsTrue(10 > 5);
Assert.IsFalse(5 > 10);
Assert.IsNull(null);
Assert.IsNotNull("value");
}
[TestMethod]
public void TypeAssertions()
{
Assert.IsInstanceOfType(42, typeof(int));
Assert.IsInstanceOfType("hello", typeof(string));
Assert.IsNotInstanceOfType("hello", typeof(int));
}
[TestMethod]
public void StringAssertions()
{
StringAssert.Contains("hello world", "world");
StringAssert.StartsWith("hello world", "hello");
StringAssert.EndsWith("hello world", "world");
StringAssert.Matches("abc123", new System.Text.RegularExpressions.Regex(@"\d+"));
}
[TestMethod]
public void CollectionAssertions()
{
var list = new List<int> { 1, 2, 3 };
CollectionAssert.Contains(list, 2);
CollectionAssert.DoesNotContain(list, 4);
CollectionAssert.AreEqual(new List<int> { 1, 2, 3 }, list);
CollectionAssert.AreEquivalent(new List<int> { 3, 1, 2 }, list);
CollectionAssert.AllItemsAreNotNull(list.Cast<object>().ToList());
CollectionAssert.AllItemsAreUnique(list);
}
[TestMethod]
public void ExceptionAssertions()
{
var exception = Assert.ThrowsException<DivideByZeroException>(
() => { var x = 1 / 0; });
Assert.AreEqual("Attempted to divide by zero.", exception.Message);
}
}
Data-Driven Tests
Using DataRow
[TestClass]
public class ValidatorTests
{
[TestMethod]
[DataRow("user@example.com")]
[DataRow("admin@test.org")]
[DataRow("user.name@domain.co.uk")]
[DataRow("user+tag@example.com")]
public void IsValidEmail_WithValidInput_ReturnsTrue(string email)
{
Assert.IsTrue(Validators.IsValidEmail(email), $"Expected valid: {email}");
}
[TestMethod]
[DataRow("")]
[DataRow("not-an-email")]
[DataRow("@domain.com")]
[DataRow("user@")]
public void IsValidEmail_WithInvalidInput_ReturnsFalse(string email)
{
Assert.IsFalse(Validators.IsValidEmail(email), $"Expected invalid: {email}");
}
[TestMethod]
[DataRow(1, 1, 2)]
[DataRow(0, 0, 0)]
[DataRow(-1, 1, 0)]
[DataRow(100, 200, 300)]
[DataRow(-50, -50, -100)]
public void Add_WithVariousInputs_ReturnsExpectedSum(int a, int b, int expected)
{
Assert.AreEqual(expected, Calculator.Add(a, b));
}
}
Using DynamicData
[TestClass]
public class AdvancedDataDrivenTests
{
[TestMethod]
[DynamicData(nameof(AgeValidationData), DynamicDataSourceType.Method)]
public void IsValidAge_WithBoundaryValues_ReturnsExpected(int age, bool expected)
{
Assert.AreEqual(expected, Validators.IsValidAge(age));
}
public static IEnumerable<object[]> AgeValidationData()
{
yield return new object[] { 0, false };
yield return new object[] { 1, true };
yield return new object[] { 17, false };
yield return new object[] { 18, true };
yield return new object[] { 120, true };
yield return new object[] { 121, false };
yield return new object[] { -1, false };
}
[TestMethod]
[DynamicData(nameof(UserCreationData), DynamicDataSourceType.Property)]
public void CreateUser_WithVariousInputs(string name, string email, bool shouldSucceed)
{
if (shouldSucceed)
{
var user = _service.CreateUser(new CreateUserRequest(name, email, 25));
Assert.IsNotNull(user);
}
else
{
Assert.ThrowsException<ArgumentException>(
() => _service.CreateUser(new CreateUserRequest(name, email, 25)));
}
}
public static IEnumerable<object[]> UserCreationData => new[]
{
new object[] { "Alice", "alice@example.com", true },
new object[] { "", "empty@test.com", false },
new object[] { "Bob", "", false },
};
}
Mocking with Moq
[TestClass]
public class UserServiceMockTests
{
private Mock<IUserRepository> _mockRepository = null!;
private Mock<IEmailService> _mockEmailService = null!;
private UserService _userService = null!;
[TestInitialize]
public void SetUp()
{
_mockRepository = new Mock<IUserRepository>();
_mockEmailService = new Mock<IEmailService>();
_userService = new UserService(_mockRepository.Object, _mockEmailService.Object);
}
[TestMethod]
public void GetUser_ById_QueriesRepository()
{
var expectedUser = new User { Id = 1, Name = "Alice", Email = "alice@example.com" };
_mockRepository.Setup(r => r.FindById(1)).Returns(expectedUser);
var user = _userService.GetUser(1);
Assert.AreEqual("Alice", user.Name);
_mockRepository.Verify(r => r.FindById(1), Times.Once);
}
[TestMethod]
public void GetUser_NotFound_ReturnsNull()
{
_mockRepository.Setup(r => r.FindById(999)).Returns((User?)null);
var user = _userService.GetUser(999);
Assert.IsNull(user);
}
[TestMethod]
public void CreateUser_SendsWelcomeEmail()
{
_mockRepository
.Setup(r => r.Save(It.IsAny<User>()))
.Callback<User>(u => u.Id = 1);
_userService.CreateUser(new CreateUserRequest("Bob", "bob@example.com", 25));
_mockEmailService.Verify(
e => e.SendWelcomeEmail(It.Is<string>(s => s == "bob@example.com")),
Times.Once);
}
[TestMethod]
public void CreateUser_EmailFails_DoesNotThrow()
{
_mockRepository
.Setup(r => r.Save(It.IsAny<User>()))
.Callback<User>(u => u.Id = 1);
_mockEmailService
.Setup(e => e.SendWelcomeEmail(It.IsAny<string>()))
.Throws(new InvalidOperationException("SMTP error"));
var user = _userService.CreateUser(
new CreateUserRequest("Bob", "bob@example.com", 25));
Assert.IsNotNull(user);
}
}
Lifecycle Management
[TestClass]
public class LifecycleExampleTests
{
private static DatabaseConnection _connection = null!;
[AssemblyInitialize]
public static void AssemblyInit(TestContext context)
{
// Runs once before ALL tests in the assembly
}
[AssemblyCleanup]
public static void AssemblyCleanup()
{
// Runs once after ALL tests in the assembly
}
[ClassInitialize]
public static void ClassInit(TestContext context)
{
// Runs once before all tests in THIS class
_connection = new DatabaseConnection("sqlite::memory:");
}
[ClassCleanup]
public static void ClassCleanup()
{
// Runs once after all tests in THIS class
_connection?.Dispose();
}
[TestInitialize]
public void TestInit()
{
// Runs before EACH test
_connection.BeginTransaction();
}
[TestCleanup]
public void TestClean()
{
// Runs after EACH test
_connection.RollbackTransaction();
}
[TestMethod]
public void InsertUser_PersistsToDatabase()
{
_connection.Execute("INSERT INTO Users (Name) VALUES ('Alice')");
var result = _connection.QuerySingle("SELECT Name FROM Users");
Assert.AreEqual("Alice", result);
}
}
Testing Async Methods
[TestClass]
public class AsyncServiceTests
{
[TestMethod]
public async Task FetchData_ReturnsResults()
{
var mockClient = new Mock<IHttpClient>();
mockClient
.Setup(c => c.GetAsync("/api/items"))
.ReturnsAsync(new ApiResponse { Items = new[] { 1, 2, 3 } });
var service = new DataService(mockClient.Object);
var result = await service.FetchDataAsync();
Assert.AreEqual(3, result.Items.Length);
}
[TestMethod]
public async Task FetchData_OnFailure_ThrowsServiceException()
{
var mockClient = new Mock<IHttpClient>();
mockClient
.Setup(c => c.GetAsync(It.IsAny<string>()))
.ThrowsAsync(new HttpRequestException("Connection refused"));
var service = new DataService(mockClient.Object);
await Assert.ThrowsExceptionAsync<ServiceException>(
() => service.FetchDataAsync());
}
}
Best Practices
- Use
Assert.ThrowsExceptionover[ExpectedException]-- The method-based approach is more precise and allows verifying the exception message. - Use
[DataRow]for inline test data -- Parameterize tests with[DataRow]attributes for clean, readable data-driven tests. - Use
[DynamicData]for complex test data -- When test data includes objects or computed values, use[DynamicData]with methods or properties. - Follow naming convention -- Name tests as
MethodName_Scenario_ExpectedResultfor self-documenting test output. - Use Moq for dependency mocking -- Mock interfaces with Moq and verify interactions with
.Verify()for clean isolation. - Prefer constructor injection -- Design production classes to accept dependencies via constructor for straightforward testing.
- Use
[TestCategory]for classification -- Tag tests as "Unit", "Integration", or "Slow" for selective execution in CI/CD. - Test async methods with async/await -- Use
async Tasktest methods to properly test asynchronous code without blocking. - Use FluentAssertions for readable assertions -- The FluentAssertions library provides more expressive and detailed failure messages.
- Keep tests fast and independent -- Unit tests should complete in milliseconds with no shared mutable state between methods.
Anti-Patterns
- Using
[ExpectedException]for precise testing -- It only checks the exception type, not the message or where it was thrown; useAssert.ThrowsExceptioninstead. - Not using
[TestInitialize]-- Duplicating setup code across test methods is verbose, fragile, and error-prone. - Testing private methods via reflection -- Using
PrivateObjector reflection couples tests to implementation; test through public API. - Over-mocking -- Mocking every dependency including simple DTOs reduces test confidence; mock only I/O boundaries.
- Shared mutable static state -- Static fields modified by tests cause order-dependent failures; reset state in
[TestInitialize]. - Not cleaning up resources -- Forgetting
[TestCleanup]for disposable resources causes leaks and flaky tests. - Hardcoding connection strings -- Using hardcoded values breaks tests in different environments; use configuration or in-memory alternatives.
- Ignoring collection assertions -- Using
Assert.AreEqualon collections instead ofCollectionAssertmethods gives poor failure messages. - Multiple unrelated assertions -- Combining unrelated checks in one test makes failures ambiguous; split into focused methods.
- Not using
async Taskfor async tests -- Usingasync voidtests can cause test runner issues and swallow exceptions silently.