.NET 10 Testing: Microsoft Finally Fixed the Test Runner (Mostly)

.NET 10 Testing: Microsoft Finally Fixed the Test Runner (Mostly)

Microsoft just did something unusual: they fixed a problem before most people realized they had it.

For years, dotnet test wasn’t really a test runner—it was actually just a wrapper around vstest.console.exe, a legacy artifact from the pre-.NET-Core era that Microsoft couldn’t quite kill. It worked, mostly, if you didn’t think too hard about why your tests sometimes behaved differently in Visual Studio than in GitHub Actions, or why test discovery occasionally took longer than the tests themselves.

With .NET 10, Microsoft has finally integrated testing directly into the SDK through Microsoft.Testing.Platform (MTP). The old VSTest infrastructure is now out. The new system runs tests in-process, unifies behavior across environments, and—this is actually the important part—finally respects your configuration files.

There’s a catch, of course. There always is.

From Test Wrapper to Test Platform

Running tests in .NET used to mean choosing a framework—xUnit, NUnit, MSTest, or the newer TUnit—and then essentially just hoping dotnet test could somehow figure out how to talk to it. Each framework had its own test adapter. Each adapter had its own quirks. Your CI pipeline basically just crossed its fingers and hoped for green checkmarks.

The result? Test execution that varied subtly between your laptop, your colleague’s laptop, and the build server. Debugging test failures meant first figuring out which version of which adapter was running where.

Microsoft.Testing.Platform changes that architecture. Instead of spawning separate processes and negotiating through adapters, MTP embeds the test runner directly into the SDK. Discovery, execution, and reporting now follow a single, predictable path. Tests run in-process. The CLI is cleaner. The performance is measurably better in projects with large test suites.

Enabling it requires exactly four lines in your global.json:

{
  "test": {
    "runner": "Microsoft.Testing.Platform"
  }
}

No SDK pinning required. No complicated setup. Just those four lines, and .NET 10 switches to the new test engine automatically.

The simplicity is almost suspicious.

What Actually Improves (And What Doesn’t)

Let’s be specific. MTP isn’t magic—it’s engineering. Here’s what changes when you enable it:

Test discovery is faster. In a project with ~3,500 tests, discovery dropped from 8 seconds to under 3 on my local machine. That’s honestly not earth-shattering, but it’s definitely noticeable when you’re running focused test sets repeatedly during development. Over a typical workday with 50 test runs? That actually saves roughly 4 minutes. Not revolutionary, but certainly not nothing either.

The CLI makes sense now. Previously, dotnet test --filter required arcane syntax and those bizarre -- separators to pass arguments through to the adapter. MTP removes that layer of indirection. The commands do what you’d expect without translation.

Environment consistency improves. Because the test runner is part of the SDK, your local machine and your CI pipeline execute tests the same way—assuming you actually configure your pipeline correctly (more on that disaster shortly).

But performance gains aren’t universal. If your tests are already fast, you probably won’t see dramatic improvements. MTP mainly optimizes infrastructure overhead, not slow database calls or badly written assertions. Don’t expect miracles if your test suite still takes 20 minutes because it’s hitting real APIs.

And here’s the part Microsoft doesn’t emphasize: MTP won’t save you from bad tests. If your test suite is flaky, brittle, or poorly isolated, the new platform just runs that mess faster.

What about Visual Studio integration?

Visual Studio 17.14 or later integrates with MTP. Earlier versions rely on VSTest and may behave differently. If your team uses mixed VS versions, validate results locally with the CLI to avoid IDE-specific discrepancies.

The CI Pipeline Trap (And How to Avoid It)

Here’s where things get entertaining.

You add that global.json snippet. Tests run perfectly on your machine. You commit, push, and watch your GitHub Actions pipeline… fail spectacularly.

Why? Because GitHub’s hosted runners don’t automatically respect your global.json. They just use whatever SDK version happens to be installed—often an older one that doesn’t even support MTP. Your carefully configured local environment and your CI pipeline are now essentially running completely different test infrastructure.

I learned this the hard way when a colleague spent two hours debugging “flaky” tests that weren’t actually flaky at all. The tests validated timeout behavior in an async workflow—they passed consistently with MTP locally and then failed consistently with VSTest in CI. Same code, same timeout values, completely different test runner behavior. VSTest’s process isolation apparently meant slightly different timing characteristics. We only figured it out after painstakingly comparing the test execution logs line by line and finally noticing the runner version mismatch.

The fix is one line—but you have to know it exists:

- uses: actions/setup-dotnet@v5
  with:
    global-json-file: './global.json'
- run: dotnet test

That global-json-file parameter forces the action to actually read your configuration. Without it, you’re deploying tests with one runner and debugging them with another.

If you don’t specify this explicitly, your global.json is basically just decorative. It just sits in your repository looking official while your pipeline ignores it completely. I’ve actually seen teams add comments to their global.json files carefully explaining why certain settings exist, not realizing the entire file wasn’t even being used. That’s not configuration—that’s just theater.

Version Compatibility (Or: Who Gets Left Behind)

MTP doesn’t support every test framework version ever released. Microsoft drew a line, and some older projects sit on the wrong side of it.

To use Microsoft.Testing.Platform, your test frameworks need these minimum versions:

  • xUnit → Version 3.x or later
  • MSTest → Version 3.2.0 or later
  • NUnitNUnit3TestAdapter 5.0.0 or later
  • TUnit → Works out of the box (it was designed with MTP in mind)
  • Visual Studio → Version 17.14 or later for full integration

If you’re running older versions, the SDK simply won’t negotiate. It fails hard. No fallback to VSTest, no warning, just an error message telling you to upgrade.

That’s actually good design. Ambiguity in test execution creates exactly the kind of “works on my machine” disasters MTP is supposed to prevent. Better to fail explicitly than to silently run different infrastructure depending on what’s installed.

But it does mean migration isn’t optional if you’re upgrading to .NET 10. You can’t enable MTP halfway. Either your entire test suite supports it, or you don’t use it at all.

Migration Strategy (Or: How Not to Break Everything)

Migrating to MTP isn’t technically complicated, but it does actually require coordination. You can’t just enable it in isolation—everyone on the team needs to be running compatible tools, or the test results will simply stop being reliable.

Here’s a migration approach that won’t cause chaos:

1. Audit your test framework versions first. Check every test project. If you’re running xUnit 2.x or MSTest 2.x, you’re upgrading before you can enable MTP. No shortcuts.

2. Add the global.json configuration. Start with the minimal snippet. You don’t need to pin an SDK version unless you have specific compatibility requirements elsewhere.

3. Update your CI/CD pipelines. Add the global-json-file parameter to your setup-dotnet action. Test it on a branch before merging. Verify that the pipeline is actually using MTP by checking the test output logs.

4. Run tests locally and in CI—compare the results. If they differ, you’ve found a configuration issue. Fix it now, before it becomes a debugging nightmare three months from now. Pay special attention to tests that involve timing, parallelization, or resource cleanup—these are the ones most likely to behave differently between test runners.

If you’ve read “Your Tests Are Lying — Mutation Testing in .NET”, you know how dangerous it is when tests pass for the wrong reasons. MTP reduces that risk—but only if your environments are actually configured consistently.

When Not to Migrate (Yes, Really)

Not every project should rush into MTP. Here are scenarios where you might want to wait:

Legacy test suites with heavy VSTest dependencies. If your tests rely on specific VSTest console runners, custom adapters, or undocumented behavior, migration will break things. You’ll need to refactor or rewrite parts of your test infrastructure.

Projects still on .NET 8 LTS. MTP is a .NET 10 feature. If you’re staying on an LTS version for stability, you’re essentially stuck with VSTest. That’s fine—VSTest still works. It’s just not getting any new features.

Teams without time to validate the migration. Half-migrating is worse than not migrating. If you can’t dedicate time to verify that tests behave identically across environments, defer the change until you can.

MTP is definitely an improvement, but it’s not urgent. If your current test infrastructure already works reliably, you’re really not missing out by waiting.

What This Actually Means for Your Workflow

The shift to MTP changes how you think about test configuration. Your global.json file is no longer just an SDK hint—it’s a binding contract. The SDK reads it, respects it, and enforces it. If your pipeline isn’t configured to honor that contract, your tests will diverge silently between environments.

That’s both the strength and the risk of this change. MTP removes ambiguity, but only if you configure it correctly everywhere. Miss one environment, and you’re back to debugging phantom failures that only reproduce in CI.

The good news? Once configured properly, tests become predictable. The bad news? Getting there requires discipline, not just documentation.

Should You Migrate Now?

If you’re already on .NET 10, yes. The benefits clearly outweigh the setup cost, especially if you’ve already dealt with flaky CI pipelines or inconsistent test behavior across environments.

If you’re on an LTS version and your tests are stable, there’s really no rush. VSTest isn’t going anywhere immediately, and MTP will still be there when you eventually upgrade.

But if you’re planning to move to .NET 10 anyway, enable MTP early in the migration process. It’s easier to validate test behavior during a planned upgrade than to debug it six months later when the root cause has been buried under other changes.

Add the four lines to global.json. Update your CI config. Upgrade your test frameworks. Run the tests. Compare the results.

If they match—and they should—you’re done. If they don’t, you’ve found a configuration problem that would have bitten you eventually anyway. Better to find it now during a planned migration than at 2 AM when production is down and your tests are lying to you about what’s safe to deploy.

Microsoft fixed the test runner. Whether you use it or keep debugging phantom CI failures is your choice—but when the next “works on my machine” ticket comes in, at least you’ll know exactly why.

Comments

VG Wort