بهترین روشها برای Unit Testing) در داتنت
اصول بنیادین یک آزمون واحد خوب: آشنایی با F.I.R.S.T
برای اینکه آزمونهای واحد نوشتهشده بیشترین کارایی را داشته باشند، باید از اصول F.I.R.S.T پیروی کنند. این اصول تضمین میکنند که تستهای شما ارزشمند و قابل نگهداری هستند.
-
Fast (سریع): آزمونهای واحد باید بسیار سریع اجرا شوند. سرعت بالا به توسعهدهندگان اجازه میدهد تا مجموعه تستها را به طور مداوم و بدون وقفه در فرآیند کدنویسی اجرا کنند. یک مجموعه تست کند، به ندرت اجرا خواهد شد و ارزش خود را از دست میدهد.
-
Independent/Isolated (مستقل/ایزوله): هر آزمون باید کاملاً مستقل از سایر آزمونها باشد. ترتیب اجرای تستها نباید بر نتیجه آنها تأثیر بگذارد. وابستگی بین تستها منجر به شکستهای آبشاری (cascading failures) میشود که دیباگ کردن را دشوار میسازد.
-
Repeatable (تکرارپذیر): یک آزمون واحد باید در هر محیطی (مانند ماشین توسعهدهنده یا سرور CI/CD) نتایج یکسانی تولید کند. این اصل ایجاب میکند که تستها به عوامل خارجی مانند تنظیمات محیطی، شبکه یا پایگاه داده واقعی وابسته نباشند.
-
Self-Validating (خود-اعتبارسنج): نتیجه یک آزمون باید به صورت کاملاً واضح و بدون نیاز به تفسیر دستی، موفقیت (Pass) یا شکست (Fail) را مشخص کند. خروجی باید یک مقدار بولین (boolean) باشد.
-
Timely (بهموقع): آزمونهای واحد باید درست قبل یا همزمان با کد اصلی (Production Code) که قرار است آن را تست کنند، نوشته شوند. این اصل، که ریشه در توسعه آزمونمحور (TDD) دارد، تضمین میکند که کد از ابتدا با قابلیت تستپذیری بالا طراحی میشود.
انتخاب چارچوب آزمون (Testing Framework) مناسب
در اکوسیستم داتنت، سه فریمورک اصلی برای نوشتن آزمونهای واحد وجود دارد: MSTest، NUnit و xUnit. اگرچه هر سه هدف یکسانی را دنبال میکنند، اما در فلسفه و سینتکس تفاوتهایی دارند.
| ویژگی | MSTest | NUnit | xUnit |
| سینتکس اصلی | [TestClass], [TestMethod] | [TestFixture], [Test] | [Fact], [Theory] |
| راهاندازی/پاکسازی | [TestInitialize], [TestCleanup] | [SetUp], [TearDown] | استفاده از Constructor و IDisposable |
| تستهای پارامتری | [DataRow], [DynamicData] | [TestCase], [TestCaseSource] | [Theory], [InlineData], [MemberData] |
| فلسفه | یکپارچگی کامل با ویژوال استودیو، سنتی | بسیار منعطف و دارای ویژگیهای غنی | مدرن، مینیمال و مبتنی بر اصول سادگی |
| اجرای موازی | پشتیبانی محدود (در نسخههای جدید بهبود یافته) | نیاز به پیکربندی دارد | به صورت پیشفرض فعال است |
توصیه:
-
xUnit به دلیل رویکرد مدرن، ایزولهسازی بهتر تستها (هر تست نمونه جدیدی از کلاس تست را ایجاد میکند) و پشتیبانی پیشفرض از اجرای موازی، انتخاب اول بسیاری از پروژههای جدید داتنت، بهویژه در ASP.NET Core است.
-
NUnit با داشتن مجموعهای غنی از اتربیوتها و قابلیتها، برای پروژههای پیچیده و تیمهایی که نیاز به انعطافپذیری بالایی دارند، گزینهی بسیار قدرتمندی است.
-
MSTest به دلیل یکپارچگی عمیق با ویژوال استودیو، برای تیمهایی که به شدت به اکوسیستم مایکروسافت وابستهاند یا پروژههای قدیمیتری دارند، همچنان یک انتخاب معتبر است.
ساختار یک آزمون واحد: الگوی Arrange-Act-Assert (AAA)
یک آزمون واحد خوانا و قابل فهم، همواره از الگوی AAA پیروی میکند. این الگو تست را به سه بخش منطقی و مجزا تقسیم میکند:
-
Arrange (آمادهسازی): در این بخش، تمام پیشنیازهای تست را آماده میکنید. این شامل نمونهسازی از کلاس مورد آزمون (System Under Test - SUT)، ایجاد اشیاء Mock یا Stub برای وابستگیها و تعریف دادههای ورودی است.
-
Act (اجرا): در این مرحله، متدی را که قصد آزمودن آن را دارید، با پارامترهای آمادهشده در بخش Arrange فراخوانی میکنید. این بخش باید بسیار کوتاه و متمرکز باشد (معمولاً یک خط کد).
-
Assert (اعتبارسنجی): در بخش نهایی، نتیجهی بهدستآمده از مرحله Act را با نتیجه مورد انتظار مقایسه میکنید. اگر نتیجه مطابق انتظار بود، تست موفق میشود؛ در غیر این صورت، شکست میخورد.
[Fact]
public void Add_SimpleValues_ShouldCalculateCorrectSum()
{
// Arrange
var calculator = new Calculator();
int a = 5;
int b = 10;
int expected = 15;
// Act
int actual = calculator.Add(a, b);
// Assert
Assert.Equal(expected, actual);
}
ایزولهسازی وابستگیها: قدرت تزریق وابستگی و Mocking
یک آزمون واحد واقعی، تنها و تنها یک "واحد" از کد را میآزماید و آن را از وابستگیهای خارجی مانند پایگاه داده، سرویسهای وب، فایل سیستم یا حتی سایر کلاسها ایزوله میکند. دو تکنیک کلیدی برای دستیابی به این هدف وجود دارد:
۱. تزریق وابستگی (Dependency Injection - DI)
کد شما باید به جای ایجاد مستقیم وابستگیهای خود (با استفاده از کلمه کلیدی new)، آنها را از طریق Constructor یا متدها دریافت کند. این کار به شما اجازه میدهد تا در زمان تست، به جای وابستگی واقعی، یک نسخه جعلی یا شبیهسازیشده (Mock) را به کلاس مورد آزمون تزریق کنید.
مثال بد (بدون DI):
public class OrderService
{
private readonly DatabaseLogger _logger;
public OrderService()
{
_logger = new DatabaseLogger(); // وابستگی مستقیم
}
public void PlaceOrder()
{
// ...
_logger.Log("Order placed.");
}
}
در این حالت، تست کردن OrderService بدون اتصال به پایگاه داده واقعی غیرممکن است.
مثال خوب (با DI):
public class OrderService
{
private readonly ILogger _logger;
public OrderService(ILogger logger) // وابستگی از طریق Constructor تزریق میشود
{
_logger = logger;
}
public void PlaceOrder()
{
// ...
_logger.Log("Order placed.");
}
}

۲. شبیهسازی با استفاده از کتابخانههای Mocking
کتابخانههایی مانند Moq، NSubstitute و FakeItEasy به شما اجازه میدهند تا به سادگی نسخههای شبیهسازیشده از اینترفیسها یا کلاسهای مجازی (virtual) بسازید. این اشیاء Mock رفتار وابستگیها را در سناریوهای مختلف کنترل میکنند.
بهترین روشها برای Mocking:
-
فقط وابستگیهای خارجی را Mock کنید: هدف از Mocking، ایزوله کردن واحد تحت تست است، نه شبیهسازی کل سیستم.
-
از Mock کردن کلاسهای Concrete بپرهیزید: همیشه سعی کنید به جای کلاسهای واقعی، اینترفیسها را Mock کنید. این کار انعطافپذیری تستها را افزایش میدهد.
-
رفتار Mock را ساده نگه دارید: یک Mock نباید منطق پیچیدهای داشته باشد. تنها رفتار لازم برای سناریوی تست مورد نظر را شبیهسازی کنید.
-
اعتبارسنجی تعاملات (Interaction Testing): علاوه بر بررسی خروجی متد (State Testing)، میتوانید بررسی کنید که آیا متدهای خاصی روی Mock شما با پارامترهای صحیحی فراخوانی شدهاند یا خیر.
مثال با استفاده از Moq و xUnit:
[Fact]
public void PlaceOrder_WhenCalled_ShouldLogMessage()
{
// Arrange
var mockLogger = new Mock<ILogger>();
var orderService = new OrderService(mockLogger.Object);
// Act
orderService.PlaceOrder();
// Assert
mockLogger.Verify(logger => logger.Log("Order placed."), Times.Once);
}
پوشش کد (Code Coverage) چیست و چرا اهمیت دارد؟
Code Coverage معیاری است که نشان میدهد چه درصدی از کد اصلی شما توسط آزمونهای واحد اجرا شده است. ابزارهایی مانند Coverlet، dotCover و ابزار داخلی ویژوال استودیو (نسخه Enterprise) میتوانند این معیار را اندازهگیری کنند.
نکات مهم:
-
یک هدف، نه یک قانون: پوشش کد 80% تا 85% معمولاً یک هدف خوب در نظر گرفته میشود. تلاش برای رسیدن به پوشش 100% اغلب هزینهبر و غیرعملی است.
-
کیفیت بر کمیت: پوشش کد 100% به معنای بینقص بودن کد شما نیست. این معیار فقط نشان میدهد که کد اجرا شده، نه اینکه به درستی تست شده است. یک تست ضعیف با Assert-های بیمعنی هم میتواند پوشش کد را افزایش دهد.
-
روی منطق پیچیده تمرکز کنید: از گزارشهای پوشش کد برای شناسایی بخشهای حیاتی و پیچیده برنامه که تست نشدهاند، استفاده کنید.
اشتباهات رایج در نوشتن آزمونهای واحد
-
تست کردن جزئیات پیادهسازی به جای رفتار: تست شما باید رفتار عمومی یک متد را بسنجد، نه نحوه پیادهسازی داخلی آن را. وابستگی به جزئیات پیادهسازی، تستها را شکننده (Brittle) میکند.
-
نوشتن تستهای یکپارچهسازی (Integration Tests) به جای تستهای واحد: اگر تست شما با پایگاه داده، فایل سیستم یا شبکه ارتباط برقرار میکند، دیگر یک تست واحد نیست.
-
وجود منطق شرطی (if/else, switch) در تستها: یک تست باید مسیر مشخص و واحدی را دنبال کند. برای هر سناریو، یک تست جداگانه بنویسید.
-
نامگذاری نامفهوم تستها: نام تست باید به وضوح بیانگر واحد تحت تست، سناریو و نتیجه مورد انتظار باشد. یک الگوی خوب: [MethodName]_[Scenario]_[ExpectedBehavior]
-
Assert-های متعدد برای رفتارهای متفاوت: هر تست باید یک مفهوم یا رفتار واحد را اعتبارسنجی کند. اگر نیاز به Assert-های متعدد برای جنبههای مختلف یک رفتار دارید، اشکالی ندارد، اما از تست کردن چندین رفتار مجزا در یک تست واحد خودداری کنید.
نتیجهگیری
نوشتن آزمونهای واحد باکیفیت در داتنت یک مهارت ضروری برای توسعهدهندگان مدرن است. با پیروی از اصول F.I.R.S.T، استفاده از الگوی AAA، انتخاب چارچوب مناسب و بهرهگیری هوشمندانه از تزریق وابستگی و کتابخانههای Mocking، میتوانید کدی بنویسید که نه تنها امروز به درستی کار میکند، بلکه در آینده نیز به راحتی قابل توسعه و نگهداری خواهد بود. سرمایهگذاری بر روی آزمونهای واحد، سرمایهگذاری بر روی کیفیت و پایداری بلندمدت نرمافزار شماست.
0 نظر
هنوز نظری برای این مقاله ثبت نشده است.