{"id":5005,"date":"2026-09-03T18:48:04","date_gmt":"2026-09-03T18:48:04","guid":{"rendered":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/09\/03\/test-what-you-ship-mstest-and-native-aot\/"},"modified":"2026-09-03T18:48:04","modified_gmt":"2026-09-03T18:48:04","slug":"test-what-you-ship-mstest-and-native-aot","status":"publish","type":"post","link":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/2026\/09\/03\/test-what-you-ship-mstest-and-native-aot\/","title":{"rendered":"Test what you ship: MSTest and Native AOT"},"content":{"rendered":"<p>If you ship an application with<br \/>\n<a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/deploying\/native-aot\/\">Native AOT<\/a>,<br \/>\na green managed test run leaves a gap. Native AOT compiles ahead of time,<br \/>\n<a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/deploying\/trimming\/trim-self-contained\">removes unused code<\/a>,<br \/>\nand requires alternatives to runtime code generation and unrestricted<br \/>\nreflection. The published application can therefore behave differently from<br \/>\nthe code exercised by the managed test process.<\/p>\n<p><strong>Starting with MSTest 4.4, MSTest supports publishing and running test projects<br \/>\nas Native AOT executables.<\/strong> Source generation records which tests exist and how<br \/>\nto invoke them before trimming happens, without requiring developers to rewrite<br \/>\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<br \/>\nenvironments, that native run provides more representative evidence. It doesn\u2019t<br \/>\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<br \/>\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<br \/>\nthe type through reflection. In a trimmed or Native AOT publish,<br \/>\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<br \/>\ntesting-framework problem. The production code should provide generated JSON<br \/>\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<br \/>\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><br \/>\ndescribes this behavior and the available generation modes. Serialization is<br \/>\nonly one example; native testing can also expose unsupported runtime code<br \/>\ngeneration, missing reflection metadata, or an incompatible dependency.<\/p>\n<p>There are two separate responsibilities here. MSTest source generation keeps<br \/>\nthe test discoverable and runnable after trimming. The JSON source generator<br \/>\nfixes the application path that the test exercises. MSTest doesn\u2019t hide the<br \/>\napplication problem; it lets the native test process expose it before the<br \/>\nproduct reaches deployment.<\/p>\n<h2>How MSTest makes the native test executable possible<\/h2>\n<p>MSTest\u2019s first<br \/>\n<a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/testing-your-native-aot-dotnet-apps\/\">Native AOT preview<\/a><br \/>\narrived in April 2024. It proved that an MSTest project could become a native<br \/>\nexecutable, but the experimental engine and source generator had limited<br \/>\ncoverage.<\/p>\n<p>The new path moves source generation into the open MSTest toolchain and aligns<br \/>\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<br \/>\nwhen trimming runs.<\/li>\n<\/ul>\n<p>The important result isn\u2019t the generated code itself. The build records which<br \/>\ntests exist and how to run them before trimming happens. Your tests remain<br \/>\nordinary <code>[TestClass]<\/code> and <code>[TestMethod]<\/code> code; the generator changes the build<br \/>\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<br \/>\nsmall:<\/p>\n\n<div class=\"alert alert-primary\">\n<p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>For engineering leaders<\/strong><\/p>\n<p>Keep the fast managed test lane, then pilot one additional native<br \/>\npublish-and-run lane. The cost is extra CI publish time and, for VSTest users,<br \/>\nmigration to Microsoft Testing Platform. Success means identical test counts<br \/>\nand outcomes, acceptable CI time, and earlier detection of deployment-only<br \/>\ndefects.\n<\/p><\/div>\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<br \/>\n<code>PublishAot<\/code> enables MSTest source generation and the native executable path.<br \/>\nProjects that still use VSTest should review the<br \/>\n<a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/testing\/migrating-vstest-microsoft-testing-platform\">VSTest-to-MTP migration guidance<\/a><br \/>\nbecause command-line arguments, CI integration, and supported <code>.runsettings<\/code><br \/>\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<br \/>\nRID you deploy, such as <code>win-x64<\/code> or <code>osx-arm64<\/code>; on Windows, run<br \/>\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\u2019t expected after a clean migration. It can happen when a test class<br \/>\ncan\u2019t enter the generated registry\u2014for example, because it only inherits<br \/>\n<code>[TestClass]<\/code> or is inaccessible, file-local, static, or open generic\u2014and the<br \/>\nrelated diagnostics are ignored or suppressed. The registered subset can still<br \/>\npass and the process can exit successfully, so test-count parity is a release<br \/>\ngate.<\/p>\n<p>Start with a scheduled or release-validation job. Move the lane to every pull<br \/>\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,<br \/>\ndependency injection, configuration binding, reflection-based plugins, or a<br \/>\ndependency whose Native AOT support you need to prove. A project containing<br \/>\nonly arithmetic-style unit tests can demonstrate that the runner works, but it<br \/>\nwon\u2019t tell you much about the application you ship.<\/p>\n\n<div class=\"alert alert-info\">\n<p class=\"alert-divider\"><i class=\"fabric-icon fabric-icon--Info\"><\/i><strong>Run both layers<\/strong><\/p>\n<p>Managed tests optimize the development feedback loop. The native lane checks<br \/>\nthe deployment model. They answer different questions and are most useful<br \/>\ntogether.\n<\/p><\/div>\n<h2>Keep the boundaries clear<\/h2>\n<p>This isn\u2019t equivalent to an end-to-end production validation. Configuration,<br \/>\noperating system, architecture, external services, and packaging can still<br \/>\ndiffer. It removes one important variable: the test and application can use the<br \/>\nsame trimming and ahead-of-time compilation model.<\/p>\n<p>Source generation also doesn\u2019t mean zero reflection. The default<br \/>\n<code>ReflectionFree<\/code> mode uses generated attributes and delegates for supported<br \/>\nconstruction and invocation, but some operations retain reflective fallbacks.<br \/>\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\u2019t available<br \/>\nin the Native AOT path. TRX and Code Coverage remain supported. Treat analyzer<br \/>\nand build diagnostics as migration gates rather than warnings to suppress, and<br \/>\ncheck the<br \/>\n<a href=\"https:\/\/learn.microsoft.com\/dotnet\/core\/testing\/unit-testing-mstest-sdk#reflection-source-generator\">MSTest SDK documentation<\/a><br \/>\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<br \/>\nthan the confidence it adds, change its frequency, choose a more representative<br \/>\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<br \/>\nreflective construction and invocation for supported tests. That can reduce<br \/>\nstartup and discovery work, but it doesn\u2019t guarantee a faster end-to-end run;<br \/>\ntest execution, process startup, publishing, and remaining reflection can<br \/>\ndominate.<\/p>\n<p>Production fidelity is useful even if the performance improvement is small.<br \/>\nPerformance is secondary, not the reason to test under the deployment model<br \/>\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<br \/>\ndeployment-only failure, test-count mismatch, or clean native run to evaluate<br \/>\nwhether the lane adds useful confidence. Then decide whether to expand, refine,<br \/>\nor stop the pilot.<\/p>\n<p>With MSTest 4.4 and a verified native publish path, the tests still look like<br \/>\nMSTest while the executable behaves more like the application you actually<br \/>\nship.<\/p>\n\n<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>\n<p>The post <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\/mstest-source-generation\/\">Test what you ship: MSTest and Native AOT<\/a> appeared first on <a href=\"https:\/\/devblogs.microsoft.com\/dotnet\">.NET Blog<\/a>.<\/p>","protected":false},"excerpt":{"rendered":"<p>If you ship an application with Native AOT, a green managed test run leaves a gap. Native AOT compiles ahead [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":94,"comment_status":"","ping_status":"","sticky":false,"template":"","format":"standard","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"categories":[7],"tags":[],"class_list":["post-5005","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-dotnet"],"_links":{"self":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/5005","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/comments?post=5005"}],"version-history":[{"count":0,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/posts\/5005\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media\/94"}],"wp:attachment":[{"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/media?parent=5005"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/categories?post=5005"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rssfeedtelegrambot.bnaya.co.il\/index.php\/wp-json\/wp\/v2\/tags?post=5005"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}