Test what you ship: MSTest and Native AOT

Wait 5 sec.

If you ship an application withNative AOT,a green managed test run leaves a gap. Native AOT compiles ahead of time,removes unused code,and requires alternatives to runtime code generation and unrestrictedreflection. The published application can therefore behave differently fromthe code exercised by the managed test process.Starting with MSTest 4.4, MSTest supports publishing and running test projectsas Native AOT executables. Source generation records which tests exist and howto invoke them before trimming happens, without requiring developers to rewritetheir test classes. The result is simple: test what you ship.For teams with formal validation plans, including some in regulatedenvironments, that native run provides more representative evidence. It doesn’treplace testing the final application artifact.A managed pass can still hide a deployment failureConsider an application that serializes a receipt with System.Text.Json and atest that covers that path:[TestClass]public class ReceiptFormatterTests{ [TestMethod] public void ReceiptIsSerialized() { var json = JsonSerializer.Serialize(new Receipt(42)); StringAssert.Contains(json, "\"Total\":42"); }}public sealed record Receipt(decimal Total);The test passes in a normal managed run because System.Text.Json can discoverthe type through reflection. In a trimmed or Native AOT publish,reflection-based serialization is disabled by default. The same path throws:System.InvalidOperationException:Reflection-based serialization has been disabled for this application.That failure is useful. It identifies an application deployment problem, not atesting-framework problem. The production code should provide generated JSONmetadata, for example:[JsonSerializable(typeof(Receipt))]internal partial class AppJsonContext : JsonSerializerContext{}var json = JsonSerializer.Serialize( new Receipt(42), AppJsonContext.Default.Receipt);TheSystem.Text.Json source-generation guidancedescribes this behavior and the available generation modes. Serialization isonly one example; native testing can also expose unsupported runtime codegeneration, missing reflection metadata, or an incompatible dependency.There are two separate responsibilities here. MSTest source generation keepsthe test discoverable and runnable after trimming. The JSON source generatorfixes the application path that the test exercises. MSTest doesn’t hide theapplication problem; it lets the native test process expose it before theproduct reaches deployment.How MSTest makes the native test executable possibleMSTest’s firstNative AOT previewarrived in April 2024. It proved that an MSTest project could become a nativeexecutable, but the experimental engine and source generator had limitedcoverage.The new path moves source generation into the open MSTest toolchain and alignsit with MSTest 4.4. During compilation, the generator emits:A registry of the test classes in the assembly.Attribute data for supported test members.Delegates that construct test classes and invoke test methods.References that preserve discovered test classes and supported base classeswhen trimming runs.The important result isn’t the generated code itself. The build records whichtests exist and how to run them before trimming happens. Your tests remainordinary [TestClass] and [TestMethod] code; the generator changes the buildand execution path, not the programming model.Configure one representative projectWith the targeted release, the minimum project configuration is deliberatelysmall:For engineering leadersKeep the fast managed test lane, then pilot one additional nativepublish-and-run lane. The cost is extra CI publish time and, for VSTest users,migration to Microsoft Testing Platform. Success means identical test countsand outcomes, acceptable CI time, and earlier detection of deployment-onlydefects. net10.0 true MSTest.Sdk uses Microsoft Testing Platform (MTP) by default. SettingPublishAot enables MSTest source generation and the native executable path.Projects that still use VSTest should review theVSTest-to-MTP migration guidancebecause command-line arguments, CI integration, and supported .runsettingsentries differ.Publish for the same operating system and architecture as the application:dotnet publish ./MyProject.Tests/MyProject.Tests.csproj \ -c Release -r linux-x64 -o ./artifacts/native-tests./artifacts/native-tests/MyProject.TestsThe example uses the linux-x64 runtime identifier (RID). Replace it with theRID you deploy, such as win-x64 or osx-arm64; on Windows, runMyProject.Tests.exe. Replace the project path with your test project.Then add a focused CI pilot:Keep the existing managed test run.Publish and run one representative test project as Native AOT.Assert that both lanes discover the exact expected test count and outcomes.Record native publish-and-run time separately from test execution time.Expand only where the additional confidence justifies the CI cost.This isn’t expected after a clean migration. It can happen when a test classcan’t enter the generated registry—for example, because it only inherits[TestClass] or is inaccessible, file-local, static, or open generic—and therelated diagnostics are ignored or suppressed. The registered subset can stillpass and the process can exit successfully, so test-count parity is a releasegate.Start with a scheduled or release-validation job. Move the lane to every pullrequest only if its signal and publish time justify the added feedback cost.Choose a project with meaningful deployment-sensitive paths: serialization,dependency injection, configuration binding, reflection-based plugins, or adependency whose Native AOT support you need to prove. A project containingonly arithmetic-style unit tests can demonstrate that the runner works, but itwon’t tell you much about the application you ship.Run both layersManaged tests optimize the development feedback loop. The native lane checksthe deployment model. They answer different questions and are most usefultogether.Keep the boundaries clearThis isn’t equivalent to an end-to-end production validation. Configuration,operating system, architecture, external services, and packaging can stilldiffer. It removes one important variable: the test and application can use thesame trimming and ahead-of-time compilation model.Source generation also doesn’t mean zero reflection. The defaultReflectionFree mode uses generated attributes and delegates for supportedconstruction and invocation, but some operations retain reflective fallbacks.For compatibility investigations, set: RootingRooting preserves discovered test members but uses reflective execution.The most important migration limits are:LimitationMigration guidanceA class only inherits [TestClass]Declare the attribute directly; MSTEST0069 identifies this shape.A test class is inaccessible, file-local, static, abstract, or open genericUse a concrete, accessible, non-static, closed type. Abstract base fixtures remain supported through a concrete derived test class.A test method is generic or has ref, out, or in parametersUse a supported method signature.[AssemblyFixtureProvider] is usedReplace it with a supported fixture pattern before relying on the native run.Some MSTest SDK integrations, MTP extensions, and CI reporters aren’t availablein the Native AOT path. TRX and Code Coverage remain supported. Treat analyzerand build diagnostics as migration gates rather than warnings to suppress, andcheck theMSTest SDK documentationfor the current support matrix.For a team, the change should stay deliberately narrow:KeepAddStill requiredFast managed tests for everyday feedbackOne published native test lane for selected projectsEnd-to-end validation of the final application artifactThat separation makes the rollout reversible. If the native lane costs morethan the confidence it adds, change its frequency, choose a more representativeproject, or stop the pilot without disrupting the managed test suite.Performance is evidence, not the premiseSource generation avoids the assembly-wide Assembly.GetTypes() scan andreflective construction and invocation for supported tests. That can reducestartup and discovery work, but it doesn’t guarantee a faster end-to-end run;test execution, process startup, publishing, and remaining reflection candominate.Production fidelity is useful even if the performance improvement is small.Performance is secondary, not the reason to test under the deployment modelyou ship.Start with one projectChoose a project that exercises code you publish with Native AOT. Use the nextdeployment-only failure, test-count mismatch, or clean native run to evaluatewhether the lane adds useful confidence. Then decide whether to expand, refine,or stop the pilot.With MSTest 4.4 and a verified native publish path, the tests still look likeMSTest while the executable behaves more like the application you actuallyship.Prepare an MSTest project for Native AOTThe post Test what you ship: MSTest and Native AOT appeared first on .NET Blog.