MSTest
Microsoft's own .NET test framework — where it sits against xUnit and NUnit, what MSTest 3 improved, and when it is the right organisational choice.
1 min read · updated 19 September 2026
MSTest is Microsoft's own test framework, bundled with Visual Studio and supported first-party. It spent years behind xUnit and NUnit on capability; MSTest 3 largely closed that gap.
#The basics
[TestClass]
public class PricingTests
{
private Basket _basket = null!;
// Like NUnit, one instance per test is NOT guaranteed the way xUnit
// guarantees it — put per-test setup in TestInitialize.
[TestInitialize]
public void Setup() => _basket = new Basket();
[TestCleanup]
public void Cleanup() { }
[ClassInitialize]
public static void OnceForTheClass(TestContext context) { }
[TestMethod]
public void An_empty_basket_costs_nothing()
{
Assert.AreEqual(0, _basket.TotalCents);
}
[TestMethod]
[DataRow(12_000, 2_400)]
[DataRow(10_000, 2_000)]
[DataRow(9_999, 0)]
public void Discount_applies_at_or_above_the_threshold(int subtotal, int expected)
{
var result = Pricing.ApplyDiscount(new Order(subtotal), Policy);
Assert.AreEqual(expected, result.DiscountCents);
}
// DynamicData when the cases are not constants
public static IEnumerable<object[]> TierCases() =>
[
[CustomerTier.Standard, 395],
[CustomerTier.Gold, 0]
];
[TestMethod]
[DynamicData(nameof(TierCases), DynamicDataSourceType.Method)]
public void Shipping_depends_on_tier(CustomerTier tier, int expected) =>
Assert.AreEqual(expected, Shipping.For(tier).Cents);
}Note [TestMethod] plus [DataRow] — in older versions a parameterised
test needed [DataTestMethod], which still works and is no longer required.
#Parallelism
Off by default. Turn it on at the assembly level:
// AssemblyInfo.cs
[assembly: Parallelize(Workers = 4, Scope = ExecutionScope.ClassLevel)]
// Opt a class out
[TestClass]
[DoNotParallelize]
public class MigrationTests { }ExecutionScope.MethodLevel parallelises individual methods and is only
safe when the class holds no mutable state — the same caveat as
NUnit, for the same reason. See
parallel test execution.
#Assertions
Assert.AreEqual(2_400, result.DiscountCents);
Assert.IsTrue(order.Lines.Any(l => l.Sku == "book-1"));
CollectionAssert.AreEquivalent(expected, actual);
StringAssert.Contains(message, "declined");
Assert.ThrowsException<InsufficientStockException>(() => service.Reserve(order));
await Assert.ThrowsExceptionAsync<TimeoutException>(() => client.FetchAsync());The built-in set is functional and unlovely. As with xUnit, most teams add Shouldly or FluentAssertions for readable failure messages.
#What MSTest 3 brought
- A source-generated runner (
MSTest.Sdk), so tests can run as a plain executable with no VSTest host — much faster startup, and it works well in containers. Assert.That-style improvements and better async support throughout.- Trimming and AOT compatibility, which matters if you test AOT-compiled code.
<!-- The modern minimal project file -->
<Project Sdk="MSTest.Sdk/3.6.0">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
</PropertyGroup>
</Project>#Where it fits
MSTest is a reasonable default for a team standardised on the Microsoft toolchain, particularly with Azure DevOps where the reporting integration is seamless. There is no longer a capability argument against it.
For a new project with a free choice, xUnit remains the most common pick in the .NET open-source world, largely for its instance model and the ecosystem that has grown around it. The difference is small enough that team familiarity should decide it.
Common questions
- Is MSTest worse than xUnit or NUnit?
- No, though it was for a long time. MSTest 3 closed most of the gap — parallelism, better assertions, proper async support and a source-generated runner. The remaining differences are matters of taste more than capability.
- Why would I pick MSTest?
- Organisational alignment, mostly — first-party support, tight Visual Studio and Azure DevOps integration, and an existing estate. Those are legitimate reasons and worth more than a marginal API preference.
- Does MSTest run tests in parallel?
- Yes, since MSTest 2, configured with an assembly-level attribute. The default is still sequential, which surprises people migrating from xUnit.
Runnable samples for this page
last test results ↗- C#
dotnet/Tests.MSTest/tools/mstest
Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.
Was this page useful?
Related topics
- xUnit.netThe .NET runner with a fresh instance per test — Facts, Theories, fixtures, parallelism and the design opinions baked into it.
- NUnitThe longest-serving .NET test framework — its constraint-based assertion model, attribute set, and the shared-instance behaviour you have to work with.
- Writing Tests in C#The .NET testing stack — xUnit, Moq or NSubstitute, WebApplicationFactory, Testcontainers — and the dependency injection story that makes it the most testable of the four.
- Testing in Azure DevOps PipelinesA full YAML pipeline with stages, jobs, parallel test slicing and the best built-in test reporting of any CI platform.