تست Native AOT با MSTest 4.4: چگونه همان چیزی را بسنجیم که Deploy میکنیم؟
راهنمای اجرای یک مسیر تست Native AOT در کنار تستهای Managed؛ شامل پیکربندی MSTest 4.4، انتخاب پروژه نماینده، بررسی برابری تعداد تستها، کشف خطاهای Reflection و طراحی CI متناسب با Runtime مقصد.
وقتی برنامه .NET را بهصورت Native AOT منتشر میکنید، خروجی نهایی فقط نسخه سریعتر همان اجرای Managed نیست. کد Ahead-of-Time برای یک Runtime مشخص کامپایل میشود، بخشهای بلااستفاده میتوانند Trim شوند و قابلیتهایی مانند Reflection نامحدود یا تولید کد در Runtime محدودترند. بنابراین ممکن است dotnet test سبز باشد، اما Artifact منتشرشده در همان مسیر کاربردی شکست بخورد.
طبق مقاله رسمی Test what you ship: MSTest and Native AOT، از MSTest 4.4 میتوان پروژه تست را به باینری Native AOT تبدیل و اجرا کرد. Source Generator پیش از Trimming ثبت میکند چه تستهایی وجود دارند و چگونه فراخوانی میشوند. نکته کلیدی این است که این مسیر جای تستهای Managed یا تست Artifact نهایی را نمیگیرد؛ یک لایه تکمیلی برای نزدیککردن مدل اجرای تست به مدل Deploy است.
شکاف میان تست Managed و Artifact Native AOT چیست؟
در اجرای معمول Managed، Runtime میتواند typeها را با Reflection پیدا کند، Expression یا Proxy بسازد و Assembly را پویا بارگذاری کند. Native AOT میخواهد بخش بزرگی از این تصمیمها در زمان Build معلوم باشد. Trimmer نیز کدی را که قابل دسترس تشخیص نمیدهد حذف میکند. اگر برنامه بر قراردادهای ضمنی Reflection تکیه کرده باشد، Build یا Publish ممکن است هشدار بدهد و گاهی خطا فقط در اجرای مسیر خاص دیده شود.
راهنمای Native AOT در Microsoft Learn توضیح میدهد که برنامه خروجی Self-contained و برای Runtime Identifier مشخص منتشر میشود. همین وابستگی به RID یعنی تست linux-x64 روی ماشین مقصد با تست Managed روی Windows یک شواهد یکسان نیست.
- Reflection-based serialization ممکن است Metadata لازم را نداشته باشد.
- Dependency Injection یا Configuration Binding میتواند به تولید کد یا Reflection وابسته باشد.
- Plugin loading پویا ممکن است با مدل بسته AOT سازگار نباشد.
- Dependency شخص ثالث ممکن است Annotation یا مسیر سازگار با Trimming نداشته باشد.
چرا دو مسیر تست بهتر از جایگزینی کامل است؟
تست Managed برای حلقه توسعه سریع است: Build کوتاهتر، Debug راحتتر و ابزارهای تشخیص گستردهتر. تست Native AOT پرسش دیگری دارد: آیا کد تحت شرایط Publish و Trimming نیز همان رفتار را دارد؟ اگر مسیر دوم را جای اول بگذارید، Feedback روزانه را کند میکنید؛ اگر فقط مسیر اول را نگه دارید، بخشی از ریسک Deploy را نمیبینید.
| مسیر | پرسش اصلی | زمان مناسب |
|---|---|---|
| Managed | منطق برنامه و تستها سریع و درست اجرا میشوند؟ | هر Commit و Pull Request |
| Native AOT | مدل Publish، Trimming و Runtime مقصد سازگارند؟ | Job نماینده، Release یا PR حساس |
| Artifact نهایی | بسته واقعی در محیط شبیه Production کار میکند؟ | پیش از انتشار و Smoke Test |
این تفکیک شبیه گیتهای مستقل در راهنمای بازبینی Pull Request با هوش مصنوعی است: یک سیگنال سبز کافی نیست؛ هر Gate بخشی از ریسک را پوشش میدهد.
پیشنیازها و پیکربندی حداقلی MSTest
نمونه رسمی برای MSTest 4.4 از MSTest.Sdk و Microsoft Testing Platform استفاده میکند. حداقل پیکربندی یک پروژه نماینده شامل Target Framework سازگار و فعالکردن PublishAot است. مقدار دقیق Target Framework باید با برنامه و نسخه پشتیبانیشده تیم هماهنگ باشد. PublishAot فقط یک سوییچ برای خروجی نیست؛ تحلیلهای AOT را نیز وارد Build میکند. هشدارها را Suppress نکنید تا CI سبز شود؛ ابتدا مشخص کنید هشدار به کد برنامه، تست یا Dependency مربوط است.
net10.0 true MTP را با VSTest اشتباه نگیرید
MSTest.Sdk بهطور پیشفرض Microsoft Testing Platform را به کار میگیرد. اگر Pipeline فعلی روی VSTest، پارامترهای اختصاصی یا runsettings قدیمی تکیه دارد، مهاجرت را جداگانه ارزیابی کنید. تغییر Runner و فعالکردن AOT در یک Commit بزرگ، تشخیص علت خطا را دشوار میکند.
یک پروژه نماینده انتخاب کنید
اولین Pilot نباید مجموعهای از تستهای ساده جمع و تفریق باشد. پروژهای را انتخاب کنید که مسیرهای حساس به Deploy را لمس میکند: JSON Serialization، DI، Configuration Binding، Reflection، Source Generator یا Dependencyای که درباره AOT آن مطمئن نیستید.
- یک Test Project با زمان اجرای قابل کنترل انتخاب کنید.
- Test Count و نتیجه اجرای Managed را Baseline بگیرید.
- همان پروژه را برای RID واقعی Publish کنید.
- باینری Native را مستقیماً اجرا و نتیجه را ثبت کنید.
- اختلاف تعداد، نتیجه و زمان را بهعنوان Gate گزارش دهید.
اگر سرویس شما در Container لینوکسی Deploy میشود، مسیر آزمایش باید همان معماری و توزیع سازگار را هدف بگیرد. مقاله Docker برای توسعهدهندگان درباره ثابتکردن محیط اجرا توضیح میدهد؛ با این حال Container بهتنهایی تفاوت AOT و Managed را حذف نمیکند.
دستور Publish و اجرای باینری Native
برای Linux x64 ابتدا پروژه را با Release و RID مقصد Publish و سپس فایل اجرایی ایجادشده را مستقیم اجرا کنید. برای Windows از RID متناسب مانند win-x64 و فایل اجرایی exe استفاده کنید. Cross-compilation همیشه گزینه مطمئن نیست؛ Toolchain و کتابخانههای Native مقصد باید در Runner موجود باشند.
dotnet publish ./MyApp.Tests/MyApp.Tests.csproj -c Release -r linux-x64 -o ./artifacts/native-testsخروجی Publish را Artifact موقت CI نگه دارید تا در شکست قابل بررسی باشد، اما فایلهای بزرگ و حساس را بیدلیل آرشیو نکنید.
Runtime Identifier بخشی از قرارداد تست است
اگر Production روی Linux ARM64 است، موفقیت win-x64 همان شواهد را نمیدهد. RID، نسخه سیستمعامل پایه و Architecture را در Job صریح ثبت کنید. تغییر Base Image یا Toolchain باید همانند تغییر Dependency بازبینی شود.
مثال خطای پنهان: Serialization مبتنی بر Reflection
مقاله رسمی MSTest نمونهای را شرح میدهد که تست Managed برای System.Text.Json عبور میکند، زیرا type از طریق Reflection کشف میشود؛ اما مسیر Native AOT با غیرفعالبودن Reflection-based serialization شکست میخورد. راهحل، پنهانکردن خطا در تست نیست؛ برنامه باید Metadata تولیدشده JSON را فراهم کند.
[JsonSerializable(typeof(Receipt))] internal partial class AppJsonContext : JsonSerializerContext { } var json = JsonSerializer.Serialize(new Receipt(42), AppJsonContext.Default.Receipt);Source Generation در دو محل نقش متفاوت دارد: MSTest Generator خود تست را پس از Trimming قابل کشف نگه میدارد؛ JSON Source Generator مسیر برنامه را اصلاح میکند. سبزشدن اولی به معنی حل خودکار دومی نیست.
برابری Test Count را به Gate تبدیل کنید
یکی از خطرناکترین حالتها این است که اجرای Native موفق شود، اما بخشی از تستها اصلاً کشف نشده باشند. راهنمای MSTest SDK محدودیتهای Source Generation را توضیح میدهد؛ برای مثال کلاس تست باید شکل قابل پشتیبانی داشته باشد و برخی الگوهای ارثبری یا دسترسی میتوانند Diagnostic تولید کنند.
CI باید تعداد تستهای مورد انتظار را بداند. صرف Exit Code صفر کافی نیست.
- تعداد تست Managed و Native را ثبت و مقایسه کنید.
- Diagnosticهای Generator را Warning عادی تلقی نکنید.
- کلاسهای Test را مستقیم و قابل دسترس تعریف کنید.
- تستهای Ignored یا Conditional را جداگانه گزارش دهید.
اگر تعدادها عمداً متفاوتاند، دلیل و فهرست استثنا باید Version-controlled باشد؛ در غیر این صورت اختلاف یک Failure است.
هشدارهای AOT و Trimming را بودجهبندی نکنید
مستندات ASP.NET Core و Native AOT توصیه میکند برنامه روی مدل استقرار AOT آزمایش و هشدارهای Publish بررسی شوند. خاموشکردن عمومی Warningها بدهی پنهان ایجاد میکند. هر Suppression باید نزدیک کد، با توضیح و تست باشد.
- هشدار را بر اساس کد پروژه یا Dependency دستهبندی کنید.
- برای کد خودتان Source Generation یا Annotation درست اضافه کنید.
- نسخه سازگار Dependency را بررسی کنید.
- اگر Suppression لازم است، دامنه آن را حداقلی و دلیل را مستند کنید.
- در ارتقای SDK، فهرست Suppressionها را بازبینی کنید.
طراحی Job در CI بدون کندکردن همه PRها
با یک Lane زمانبندیشده یا Release Validation شروع کنید. زمان Publish Native از اجرای تست Managed بیشتر است و Toolchain بیشتری نیاز دارد. سه معیار اندازه بگیرید: زمان کل، نرخ شکست ناپایدار و تعداد خطاهای Deploy-only که کشف میشوند.
اگر Signal ارزشمند و زمان قابل قبول بود، Lane را برای PRهای حساس یا همه PRها فعال کنید. Path Filter میتواند تغییرهای مرتبط با Serialization، DI، Startup و Dependency را هدف بگیرد، اما مراقب باشید تغییر غیرمستقیم خارج از Filter ریسک ایجاد نکند.
managed-tests -> fast feedback | native-publish -> AOT evidence | native-run -> count parity | artifact-smoke -> final behaviorهر مرحله باید Log کوتاه و قابل تشخیص داشته باشد. Retry خودکار بیحد، Failure واقعی Toolchain یا Runtime را پنهان میکند.
چه چیزهایی هنوز خارج از پوشش هستند؟
اجرای Test Project به صورت Native AOT اثبات نمیکند که بسته نهایی برنامه، Configuration، فایلهای جانبی، Permission و شبکه Production درستاند. مقاله رسمی نیز تأکید میکند Native Test جای آزمایش Artifact نهایی را نمیگیرد.
- بسته Deploy نهایی را در محیط شبیه Production Smoke Test کنید.
- Configuration Binding و Secret Provider واقعی را بدون افشای Secret بررسی کنید.
- Health Check، Logging و Shutdown را تست کنید.
- برای Migration و Dependency بیرونی تست Integration مستقل داشته باشید.
- Rollback و نسخه قبلی Artifact را قابل دسترس نگه دارید.
سناریوی نمونه برای یک API سفارش
یک API سفارش از Minimal API، System.Text.Json source generation و SQL استفاده میکند. تیم تست Managed را روی هر PR نگه میدارد. یک Test Project نماینده مسیر ثبت سفارش و Serialize پاسخ را پوشش میدهد و برای linux-x64 Native Publish میشود.
در اجرای اول، Test Count یکی است اما تست Serialization در Native شکست میخورد. تیم بهجای حذف تست یا فعالکردن Reflection عمومی، JsonSerializerContext اضافه و همان مسیر را دوباره اجرا میکند. سپس Smoke Test باینری اصلی، Configuration و اتصال آزمایشی پایگاهداده را بررسی میکند. اگر قرارداد نتیجه API نیازمند بازطراحی باشد، مفاهیم مقاله Union Type و Result در C# 15 میتواند برای یک Spike جداگانه مفید باشد، نه بخشی اجباری از همین اصلاح AOT.
چکلیست آمادهسازی
- پروژه نماینده مسیرهای Reflection و Deploy را لمس میکند.
- نسخه MSTest و MTP بهصورت صریح قفل شده است.
- PublishAot فعال و Warningها بررسی شدهاند.
- RID با محیط مقصد تطابق دارد.
- Test Count Managed و Native مقایسه میشود.
- زمان Publish و اجرای تست جداگانه ثبت میشوند.
- Job ابتدا در Schedule یا Release Pilot شده است.
- Artifact نهایی همچنان Smoke Test مستقل دارد.
جمعبندی
تست Native AOT با MSTest 4.4 شکاف مهمی را هدف میگیرد: تفاوت میان کدی که روی Runtime کامل تست میکنید و مدلی که با Trimming و AOT Deploy میشود. Source Generation تستها را برای مسیر Native قابل کشف میکند، اما مسئولیت اصلاح Serialization، Reflection و Dependencyهای ناسازگار همچنان با برنامه است.
یک مسیر دوگانه بسازید: Managed برای Feedback سریع و Native برای شواهد مدل انتشار. پروژه نماینده، RID واقعی، Test Count parity، Warningهای بدون Suppression عمومی و Smoke Test Artifact نهایی، کیفیت این شواهد را تعیین میکنند.
اگر میخواهید خط تست و انتشار پروژه .NET خود را از Build سبز به شواهد قابلاعتماد نزدیک کنید، برای ارزیابی CI و معماری تست با خانه برنامهنویسان البرز همراه شوید.