{"id":60703,"date":"2026-09-03T11:00:00","date_gmt":"2026-09-03T18:00:00","guid":{"rendered":"https:\/\/devblogs.microsoft.com\/dotnet\/?p=60703"},"modified":"2026-09-03T11:00:00","modified_gmt":"2026-09-03T18:00:00","slug":"mstest-source-generation","status":"publish","type":"post","link":"https:\/\/devblogs.microsoft.com\/dotnet\/mstest-source-generation\/","title":{"rendered":"Test what you ship: MSTest and Native AOT"},"content":{"rendered":"<p>If you ship an application with\n<a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/deploying\/native-aot\/\">Native AOT<\/a>,\na green managed test run leaves a gap. Native AOT compiles ahead of time,\n<a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/deploying\/trimming\/trim-self-contained\">removes unused code<\/a>,\nand requires alternatives to runtime code generation and unrestricted\nreflection. The published application can therefore behave differently from\nthe code exercised by the managed test process.<\/p>\n<p><strong>Starting with MSTest 4.4, MSTest supports publishing and running test projects\nas Native AOT executables.<\/strong> Source generation records which tests exist and how\nto invoke them before trimming happens, without requiring developers to rewrite\ntheir test classes. The result is simple: <strong>test what you ship<\/strong>.<\/p>\n<p>For teams with formal validation plans, including some in regulated\nenvironments, that native run provides more representative evidence. It doesn&#8217;t\nreplace testing the final application artifact.<\/p>\n<h2>A managed pass can still hide a deployment failure<\/h2>\n<p>Consider an application that serializes a receipt with <code>System.Text.Json<\/code> and a\ntest that covers that path:<\/p>\n<pre><code class=\"language-csharp\">[TestClass]\npublic class ReceiptFormatterTests\n{\n    [TestMethod]\n    public void ReceiptIsSerialized()\n    {\n        var json = JsonSerializer.Serialize(new Receipt(42));\n\n        StringAssert.Contains(json, \"\\\"Total\\\":42\");\n    }\n}\n\npublic sealed record Receipt(decimal Total);<\/code><\/pre>\n<p>The test passes in a normal managed run because <code>System.Text.Json<\/code> can discover\nthe type through reflection. In a trimmed or Native AOT publish,\nreflection-based serialization is disabled by default. The same path throws:<\/p>\n<pre><code class=\"language-text\">System.InvalidOperationException:\nReflection-based serialization has been disabled for this application.<\/code><\/pre>\n<p>That failure is useful. It identifies an application deployment problem, not a\ntesting-framework problem. The production code should provide generated JSON\nmetadata, for example:<\/p>\n<pre><code class=\"language-csharp\">[JsonSerializable(typeof(Receipt))]\ninternal partial class AppJsonContext : JsonSerializerContext\n{\n}\n\nvar json = JsonSerializer.Serialize(\n    new Receipt(42),\n    AppJsonContext.Default.Receipt);<\/code><\/pre>\n<p>The\n<a href=\"https:\/\/learn.microsoft.com\/dotnet\/standard\/serialization\/system-text-json\/source-generation#disable-reflection-defaults\"><code>System.Text.Json<\/code> source-generation guidance<\/a>\ndescribes this behavior and the available generation modes. Serialization is\nonly one example; native testing can also expose unsupported runtime code\ngeneration, missing reflection metadata, or an incompatible dependency.<\/p>\n<p>There are two separate responsibilities here. MSTest source generation keeps\nthe test discoverable and runnable after trimming. The JSON source generator\nfixes the application path that the test exercises. MSTest doesn&#8217;t hide the\napplication problem; it lets the native test process expose it before the\nproduct reaches deployment.<\/p>\n<h2>How MSTest makes the native test executable possible<\/h2>\n<p>MSTest&#8217;s first\n<a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/testing-your-native-aot-dotnet-apps\/\">Native AOT preview<\/a>\narrived in April 2024. It proved that an MSTest project could become a native\nexecutable, but the experimental engine and source generator had limited\ncoverage.<\/p>\n<p>The new path moves source generation into the open MSTest toolchain and aligns\nit with MSTest 4.4. During compilation, the generator emits:<\/p>\n<ul>\n<li>A registry of the test classes in the assembly.<\/li>\n<li>Attribute data for supported test members.<\/li>\n<li>Delegates that construct test classes and invoke test methods.<\/li>\n<li>References that preserve discovered test classes and supported base classes\nwhen trimming runs.<\/li>\n<\/ul>\n<p>The important result isn&#8217;t the generated code itself. The build records which\ntests exist and how to run them before trimming happens. Your tests remain\nordinary <code>[TestClass]<\/code> and <code>[TestMethod]<\/code> code; the generator changes the build\nand execution path, not the programming model.<\/p>\n<h2>Configure one representative project<\/h2>\n<p>With the targeted release, the minimum project configuration is deliberately\nsmall:<\/p>\n<p><div class=\"alert alert-primary\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>For engineering leaders<\/strong><\/p>\nKeep the fast managed test lane, then pilot one additional native\npublish-and-run lane. The cost is extra CI publish time and, for VSTest users,\nmigration to Microsoft Testing Platform. Success means identical test counts\nand outcomes, acceptable CI time, and earlier detection of deployment-only\ndefects.\n<\/div><\/p>\n<pre><code class=\"language-xml\">&lt;Project Sdk=\"MSTest.Sdk\/4.4.0\"&gt;\n  &lt;PropertyGroup&gt;\n    &lt;TargetFramework&gt;net10.0&lt;\/TargetFramework&gt;\n    &lt;PublishAot&gt;true&lt;\/PublishAot&gt;\n  &lt;\/PropertyGroup&gt;\n\n  &lt;!-- Keep your existing ItemGroup elements and project references. --&gt;\n&lt;\/Project&gt;<\/code><\/pre>\n<p><code>MSTest.Sdk<\/code> uses Microsoft Testing Platform (MTP) by default. Setting\n<code>PublishAot<\/code> enables MSTest source generation and the native executable path.\nProjects that still use VSTest should review the\n<a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/testing\/migrating-vstest-microsoft-testing-platform\">VSTest-to-MTP migration guidance<\/a>\nbecause command-line arguments, CI integration, and supported <code>.runsettings<\/code>\nentries differ.<\/p>\n<p>Publish for the same operating system and architecture as the application:<\/p>\n<pre><code class=\"language-bash\">dotnet publish .\/MyProject.Tests\/MyProject.Tests.csproj \\\n  -c Release -r linux-x64 -o .\/artifacts\/native-tests\n.\/artifacts\/native-tests\/MyProject.Tests<\/code><\/pre>\n<p>The example uses the <code>linux-x64<\/code> runtime identifier (RID). Replace it with the\nRID you deploy, such as <code>win-x64<\/code> or <code>osx-arm64<\/code>; on Windows, run\n<code>MyProject.Tests.exe<\/code>. Replace the project path with your test project.<\/p>\n<p>Then add a focused CI pilot:<\/p>\n<ol>\n<li>Keep the existing managed test run.<\/li>\n<li>Publish and run one representative test project as Native AOT.<\/li>\n<li>Assert that both lanes discover the exact expected test count and outcomes.<\/li>\n<li>Record native publish-and-run time separately from test execution time.<\/li>\n<li>Expand only where the additional confidence justifies the CI cost.<\/li>\n<\/ol>\n<p>This isn&#8217;t expected after a clean migration. It can happen when a test class\ncan&#8217;t enter the generated registry\u2014for example, because it only inherits\n<code>[TestClass]<\/code> or is inaccessible, file-local, static, or open generic\u2014and the\nrelated diagnostics are ignored or suppressed. The registered subset can still\npass and the process can exit successfully, so test-count parity is a release\ngate.<\/p>\n<p>Start with a scheduled or release-validation job. Move the lane to every pull\nrequest only if its signal and publish time justify the added feedback cost.<\/p>\n<p>Choose a project with meaningful deployment-sensitive paths: serialization,\ndependency injection, configuration binding, reflection-based plugins, or a\ndependency whose Native AOT support you need to prove. A project containing\nonly arithmetic-style unit tests can demonstrate that the runner works, but it\nwon&#8217;t tell you much about the application you ship.<\/p>\n<p><div class=\"alert alert-info\"><p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>Run both layers<\/strong><\/p>\nManaged tests optimize the development feedback loop. The native lane checks\nthe deployment model. They answer different questions and are most useful\ntogether.\n<\/div><\/p>\n<h2>Keep the boundaries clear<\/h2>\n<p>This isn&#8217;t equivalent to an end-to-end production validation. Configuration,\noperating system, architecture, external services, and packaging can still\ndiffer. It removes one important variable: the test and application can use the\nsame trimming and ahead-of-time compilation model.<\/p>\n<p>Source generation also doesn&#8217;t mean zero reflection. The default\n<code>ReflectionFree<\/code> mode uses generated attributes and delegates for supported\nconstruction and invocation, but some operations retain reflective fallbacks.\nFor compatibility investigations, set:<\/p>\n<pre><code class=\"language-xml\">&lt;PropertyGroup&gt;\n  &lt;MSTestSourceGenMode&gt;Rooting&lt;\/MSTestSourceGenMode&gt;\n&lt;\/PropertyGroup&gt;<\/code><\/pre>\n<p><code>Rooting<\/code> preserves discovered test members but uses reflective execution.<\/p>\n<p>The most important migration limits are:<\/p>\n<table>\n<thead>\n<tr>\n<th>Limitation<\/th>\n<th>Migration guidance<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>A class only inherits <code>[TestClass]<\/code><\/td>\n<td>Declare the attribute directly; <a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/testing\/mstest-analyzers\/mstest0069\"><code>MSTEST0069<\/code><\/a> identifies this shape.<\/td>\n<\/tr>\n<tr>\n<td>A test class is inaccessible, file-local, static, abstract, or open generic<\/td>\n<td>Use a concrete, accessible, non-static, closed type. Abstract base fixtures remain supported through a concrete derived test class.<\/td>\n<\/tr>\n<tr>\n<td>A test method is generic or has <code>ref<\/code>, <code>out<\/code>, or <code>in<\/code> parameters<\/td>\n<td>Use a supported method signature.<\/td>\n<\/tr>\n<tr>\n<td><code>[AssemblyFixtureProvider]<\/code> is used<\/td>\n<td>Replace it with a supported fixture pattern before relying on the native run.<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>Some MSTest SDK integrations, MTP extensions, and CI reporters aren&#8217;t available\nin the Native AOT path. TRX and Code Coverage remain supported. Treat analyzer\nand build diagnostics as migration gates rather than warnings to suppress, and\ncheck the\n<a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/testing\/unit-testing-mstest-sdk#reflection-source-generator\">MSTest SDK documentation<\/a>\nfor the current support matrix.<\/p>\n<p>For a team, the change should stay deliberately narrow:<\/p>\n<table>\n<thead>\n<tr>\n<th>Keep<\/th>\n<th>Add<\/th>\n<th>Still required<\/th>\n<\/tr>\n<\/thead>\n<tbody>\n<tr>\n<td>Fast managed tests for everyday feedback<\/td>\n<td>One published native test lane for selected projects<\/td>\n<td>End-to-end validation of the final application artifact<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<p>That separation makes the rollout reversible. If the native lane costs more\nthan the confidence it adds, change its frequency, choose a more representative\nproject, or stop the pilot without disrupting the managed test suite.<\/p>\n<h2>Performance is evidence, not the premise<\/h2>\n<p>Source generation avoids the assembly-wide <code>Assembly.GetTypes()<\/code> scan and\nreflective construction and invocation for supported tests. That can reduce\nstartup and discovery work, but it doesn&#8217;t guarantee a faster end-to-end run;\ntest execution, process startup, publishing, and remaining reflection can\ndominate.<\/p>\n<p>Production fidelity is useful even if the performance improvement is small.\nPerformance is secondary, not the reason to test under the deployment model\nyou ship.<\/p>\n<h2>Start with one project<\/h2>\n<p>Choose a project that exercises code you publish with Native AOT. Use the next\ndeployment-only failure, test-count mismatch, or clean native run to evaluate\nwhether the lane adds useful confidence. Then decide whether to expand, refine,\nor stop the pilot.<\/p>\n<p>With MSTest 4.4 and a verified native publish path, the tests still look like\nMSTest while the executable behaves more like the application you actually\nship.<\/p>\n<p><div  class=\"d-flex justify-content-center\"><a class=\"cta_button_link btn-primary mb-24\" href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/testing\/unit-testing-mstest-sdk#reflection-source-generator\" target=\"_blank\">Prepare an MSTest project for Native AOT<\/a><\/div><\/p>\n","protected":false},"excerpt":{"rendered":"<p>MSTest source generation lets test projects use the same Native AOT and trimming deployment model as the applications they validate, while reducing reflection on the test execution path.<\/p>\n","protected":false},"author":140087,"featured_media":60704,"comment_status":"open","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[685],"tags":[7784,7798,8203,8202],"class_list":["post-60703","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet","tag-mstest","tag-native-aot","tag-source-generators","tag-trimming"],"acf":[],"blog_post_summary":"<p>MSTest source generation lets test projects use the same Native AOT and trimming deployment model as the applications they validate, while reducing reflection on the test execution path.<\/p>\n","_links":{"self":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/60703","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/users\/140087"}],"replies":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/comments?post=60703"}],"version-history":[{"count":1,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/60703\/revisions"}],"predecessor-version":[{"id":60715,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/posts\/60703\/revisions\/60715"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media\/60704"}],"wp:attachment":[{"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/media?parent=60703"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/categories?post=60703"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/devblogs.microsoft.com\/dotnet\/wp-json\/wp\/v2\/tags?post=60703"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}