The compilation and deployment of a.NET program are altered by native AOT. Native AOT creates a native executable in advance rather than sending IL that the.NET runtime builds during execution.
This modification may reveal issues that don’t show up when the identical program uses the.NET runtime properly.
Additional consideration should be given to reflection, runtime-specific behavior, trimming, serialization, dynamic code creation, and dependency injection. It is therefore insufficient to test the application just in its standard managed configuration.
With the addition of support, MSTest 4.4 makes it feasible to test Native AOT-using libraries and apps. Knowing what should be compiled using Native AOT, what should continue to be a standard test project, and which behaviors require specialized testing is crucial.
What Native AOT Changes
A normal .NET application is commonly compiled into assemblies containing Intermediate Language (IL). At runtime, the .NET runtime loads those assemblies and uses the JIT compiler to generate native machine code.
Native AOT changes that model.
The application is compiled ahead of time into native code:
1 2 3 4 5 6 7 8 9 10 | C# source | v .NET compiler | v Native AOT compiler | v Native executable |
This can provide faster startup, smaller deployment requirements, and a deployment model that does not require the normal .NET runtime.
It also means that code relying on runtime discovery needs more careful testing.
For example:
1 | var type = Type.GetType("MyApplication.Services.EmailService"); |
Code like this may require additional configuration or annotations when trimming and Native AOT are involved.
A test that passes against the normal managed application does not automatically prove that the Native AOT executable will behave the same way.
Why Native AOT Needs Dedicated Testing
Native AOT introduces constraints around:
- Reflection
- Dynamic code generation
- Trimming
- Runtime type discovery
- Serialization
- Dependency injection
- Native library dependencies
- Platform-specific behavior
Consider an application that registers services using assembly scanning:
1 2 3 4 5 6 7 8 9 | var services = new ServiceCollection(); foreach (var type in assembly.GetTypes()) { if (typeof(IHandler).IsAssignableFrom(type)) { services.AddTransient(typeof(IHandler), type); } } |
This can behave differently after trimming because the linker may remove types that it cannot determine are required.
The issue is not that Native AOT randomly breaks working code. The problem is that Native AOT needs the application’s dependencies and runtime behavior to be statically understood wherever possible.
Tests should verify those assumptions.
MSTest 4.4 and Native AOT
MSTest 4.4 provides Native AOT support for test projects.
This matters because a test suite can now be used to validate code paths under an AOT-compatible execution model rather than relying only on tests running through the normal managed runtime.
A useful way to think about the setup is:
1 2 3 4 5 6 7 8 9 10 11 12 | ┌──────────────────┐ │ MSTest tests │ └────────┬─────────┘ | ┌───────────┴───────────┐ | | v v Normal .NET execution Native AOT execution | | └───────────┬───────────┘ v Application behavior |
The normal test suite remains valuable. Native AOT testing adds another validation layer for applications that will actually be deployed as native binaries.
Create a Test Project
Start with a normal MSTest project:
1 | dotnet new mstest -n MyApplication.Tests |
Add a reference to the application project:
1 | dotnet add MyApplication.Tests reference ../MyApplication/MyApplication.csproj |
The exact project structure will depend on the application.
A typical solution might look like:
1 2 3 4 5 6 7 8 9 10 11 12 | MyApplication.sln | +-- src | | | +-- MyApplication | +-- MyApplication.csproj | +-- tests | +-- MyApplication.Tests +-- MyApplication.Tests.csproj +-- ServicesTests.cs |
Install the required MSTest packages using the versions approved for your project.
The test project should remain easy to execute using the normal test workflow:
1 | dotnet test |
This gives you a fast feedback loop before adding Native AOT-specific validation.
Write Tests Around Observable Behavior
Native AOT testing is not a reason to rewrite every test.
Focus on behavior that can be affected by AOT compilation.
For example, suppose the application uses a JSON serializer:
1 2 3 4 5 | public sealed class Customer { public int Id { get; set; } public string Name { get; set; } = string.Empty; } |
A test can verify serialization and deserialization:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | [TestClass] public class CustomerSerializationTests { [TestMethod] public void Customer_CanBeSerializedAndDeserialized() { var customer = new Customer { Id = 10, Name = "John" }; var json = JsonSerializer.Serialize(customer); var result = JsonSerializer.Deserialize<Customer>(json); Assert.IsNotNull(result); Assert.AreEqual(customer.Id, result.Id); Assert.AreEqual(customer.Name, result.Name); } } |
The important point is the behavior being tested.
If the production application depends on source-generated JSON metadata for AOT compatibility, test the actual production serialization configuration rather than creating a separate test-only serializer.
Test Reflection-Heavy Code Carefully
Reflection is one of the areas where Native AOT can expose hidden assumptions.
For example:
1 2 3 4 5 | public object CreateInstance(Type type) { return Activator.CreateInstance(type) ?? throw new InvalidOperationException(); } |
A test might verify that the expected type can be created:
1 2 3 4 5 6 7 8 | [TestMethod] public void Service_CanBeCreated() { var service = CreateInstance(typeof(MyService)); Assert.IsNotNull(service); Assert.IsInstanceOfType(service, typeof(MyService)); } |
But the test should be run against the actual AOT-compatible configuration.
If the application depends on runtime type discovery, review whether the types need trimming annotations, source generation, or another AOT-compatible design.
A passing test under the normal runtime does not prove that the linker will preserve every type needed by the Native AOT application.
Test Dependency Injection
Dependency injection is another area worth testing because applications sometimes use reflection or assembly scanning during registration.
Suppose the application has:
1 2 3 4 5 6 7 8 9 10 11 12 | public interface IMessageSender { Task SendAsync(string message); } public sealed class EmailMessageSender : IMessageSender { public Task SendAsync(string message) { return Task.CompletedTask; } } |
The test should verify the actual service registration:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | [TestMethod] public void MessageSender_IsRegistered() { var services = new ServiceCollection(); services.AddSingleton<IMessageSender, EmailMessageSender>(); using var provider = services.BuildServiceProvider(); var sender = provider.GetService<IMessageSender>(); Assert.IsNotNull(sender); Assert.IsInstanceOfType(sender, typeof(EmailMessageSender)); } |
For production applications, test the application’s real IServiceCollection configuration instead of duplicating it inside the test.
This catches missing registrations and runtime activation problems.
Test Trimming-Sensitive Code
Native AOT relies heavily on trimming.
Unused code can be removed from the final application. That is useful for reducing the deployment footprint, but it can expose code that relies on runtime discovery.
Watch for patterns such as:
1 | Assembly.GetExecutingAssembly() |
1 | Type.GetType(...) |
1 | Activator.CreateInstance(...) |
1 | MethodInfo.Invoke(...) |
and runtime-generated code.
The presence of reflection is not automatically a problem. The important question is whether the AOT compiler and linker can determine what the application needs.
When a library reports trimming or AOT warnings, treat them as migration work rather than suppressing them immediately.
Test the Published Native Binary
A successful test run is useful, but you should also test the actual published application.
Publish the application for the target runtime and architecture:
1 2 3 4 | dotnet publish \ -c Release \ -r linux-x64 \ -p:PublishAot=true |
For an ARM64 deployment:
1 2 3 4 | dotnet publish \ -c Release \ -r linux-arm64 \ -p:PublishAot=true |
The runtime identifier should match the environment where the executable will run.
After publishing, execute the resulting binary:
1 | ./MyApplication |
Then run application-level smoke tests against it.
For an API, that could include:
1 2 3 | GET /health POST /api/orders GET /api/orders/10 |
This verifies the complete deployment path instead of testing only the source project.
Native AOT Testing in CI
Native AOT should be part of CI when it is part of the production deployment.
A simplified workflow can look like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | name: Native AOT Tests on: push: branches: - main pull_request: jobs: test: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v5 - name: Setup .NET uses: actions/setup-dotnet@v5 with: dotnet-version: 11.x - name: Restore run: dotnet restore - name: Run tests run: dotnet test --configuration Release --no-restore - name: Publish Native AOT application run: | dotnet publish \ src/MyApplication/MyApplication.csproj \ -c Release \ -r linux-x64 \ -p:PublishAot=true \ --no-restore |
The important distinction is that dotnet test and Native AOT publishing validate different parts of the system.
You want both.
Test Native Dependencies
Native AOT produces a native executable, so external native dependencies need attention.
Check:
- P/Invoke calls
- Native database drivers
- Image libraries
- Compression libraries
- Cryptography libraries
- Platform-specific native packages
- Custom
.so,.dylib, or.dlldependencies
For example:
1 2 | [DllImport("nativehelper")] private static extern int ProcessData(); |
A normal .NET test might pass because the development machine has the required native library.
The published application may fail on a clean deployment environment.
Test the final binary in an environment that resembles production.
Native AOT vs Normal .NET Testing
| Area | Normal .NET testing | Native AOT testing |
|---|---|---|
| Execution | Managed runtime | Native executable |
| JIT | Available | Not used for application execution |
| Reflection | More flexible | Requires additional AOT consideration |
| Trimming | Usually less restrictive | Important |
| Dynamic code | More options | May not be supported |
| Native dependencies | Still relevant | More visible during publishing and deployment |
| Startup | Tests managed startup | Tests native startup behavior |
| Deployment | Runtime-based | Native binary |
The two approaches complement each other.
Native AOT testing should not replace the normal test suite.
Common Problems
The Application Builds but Native AOT Publishing Fails
Check the warnings produced by:
1 | dotnet publish -c Release -r linux-x64 -p:PublishAot=true |
Look for trimming, dynamic-code, reflection, and native dependency warnings.
Do not immediately suppress them.
Find out which part of the application generated the warning.
Reflection Works in Tests but Fails in Production
The normal test environment may preserve metadata that is unavailable after trimming.
Review the reflection path and use AOT-compatible patterns such as source generation or the appropriate runtime annotations.
A Native Library Cannot Be Loaded
Verify that the native library exists in the target environment and supports the target architecture.
For Linux:
1 | ldd ./MyApplication |
This can help identify missing shared-library dependencies.
The Test Suite Passes but the Native Binary Fails
This usually means the test suite did not execute the same deployment configuration used by production.
Add a publish-and-smoke-test stage to CI.
Best Practices
- Keep the normal MSTest suite. Native AOT testing adds coverage. It does not replace ordinary unit and integration tests.
- Test AOT-sensitive behavior explicitly. Reflection, serialization, dependency injection, and dynamic code deserve dedicated tests.
- Treat AOT warnings as actionable. Do not hide them with broad warning suppression.
- Publish for the real target architecture.
linux-x64andlinux-arm64are different deployment targets. - Test the published executable. A successful compilation is not enough.
- Use production configuration in integration tests. Test the actual dependency injection, serialization, and application startup configuration.
- Include Native AOT publishing in CI. Catch publishing failures before a release reaches production.
- Test on a clean environment. This helps expose missing native libraries and deployment assumptions.
Advantages and Disadvantages
Advantages
- Finds trimming and AOT compatibility problems earlier.
- Validates the application in a deployment model closer to production.
- Helps identify unsupported dynamic behavior.
- Exposes native dependency problems before deployment.
- Makes Native AOT publishing part of the regular development workflow.
Disadvantages
- Native AOT builds take additional CI time.
- Some libraries require changes for AOT compatibility.
- Reflection-heavy applications may need redesign or additional metadata.
- Tests must cover both normal .NET execution and the native deployment path.
- Cross-platform applications may require separate validation for each target architecture.
Conclusion
Testing a Native AOT application requires more than running dotnet test.
MSTest 4.4 makes it easier to include Native AOT in the testing workflow, but the test strategy still matters. Start with normal unit and integration tests, then add coverage for the parts of the application most affected by AOT, especially reflection, trimming, serialization, dependency injection, and native dependencies.
Most importantly, publish and execute the actual Native AOT binary during CI. That final step catches problems that a normal managed test run cannot see. For applications deployed with Native AOT, the goal is straightforward: the same code that passes the test suite should also survive AOT compilation, publishing, and execution in the target production environment.
Recommendation for ASP.NET 10.0 Hosting
A solid base for developing online services and applications is ASP.NET. Before creating an ASP.NET web application, you must be proficient in JavaScript, HTML, CSS, and C#. There are thousands of web hosting providers offering ASP.NET hosting on the market. However, there are relatively few web hosting providers that offer top-notch ASP.NET hosting.
ASP.NET is the best development language in Windows platform, which is released by Microsoft and widely used to build all types of dynamic Web sites and XML Web services. With this article, we’re going to help you to find the best ASP.NET Hosting solution in Europe based on reliability, features, price, performance and technical support. After we reviewed about 30+ ASP.NET hosting providers in Europe, our Best ASP.NET Hosting Award in Europe goes to HostForLIFE.eu, one of the fastest growing private companies and one of the most reliable hosting providers in Europe.
