عنوان:

‫تست‌ داده‌محور در xUnit با استفاده از [InlineData]، [MemberData] و [ClassData]


نویسنده: وحید نصیری
تاریخ: ۱۴۰۴/۰۲/۲۱ ۰۶:۵۱
آدرس: www.dntips.ir
در دنیای واقعی توسعه، تست‌های واحد به منزله منبع اصلی صحت عملکرد کد، شامل اعتبارسنجی‌ها، قوانین کسب‌وکار و دیگر منطق‌های پیاده‌سازی شده، عمل می‌کنند. فریم‌ورک xUnit.net با ارائه ویژگی تست‌ داده‌محور (Data-Driven Testing) فرآیند تست را تسهیل می‌کند. این رویکرد به توسعه‌دهندگان اجازه می‌دهد تا یک تست واحد را با مجموعه‌ای از داده‌های مختلف اجرا کنند و از صحت عملکرد کد در شرایط گوناگون اطمینان حاصل نمایند. در این مقاله آموزشی، به بررسی سهAttribute مهم در xUnit.net برای پیاده‌سازی تست‌محور داده، یعنی [InlineData], [MemberData] و [ClassData] خواهیم پرداخت و نحوه استفاده از آن‌ها را با مثال‌هایی مبتنی بر تست توابع تولید دنباله فیبوناچی (Fibonacci Sequence) شرح خواهیم داد. همچنین، به منظور درک بهتر تفاوت‌های عملکردی، یک سنجش عملکرد (Benchmarking) ساده نیز برای مقایسه روش‌های مختلف پیاده‌سازی تست ارائه خواهد شد.

ویژگی [Theory]
به طور پیش‌فرض، xUnit.net از ویژگی [Fact] برای تعریف تست‌های واحد بدون پارامتر استفاده می‌کند. در مقابل، ویژگی [Theory] برای تعریف تست‌های پارامتری به کار می‌رود. این ویژگی به xUnit.net اعلام می‌کند که متد تست مورد نظر باید با مجموعه‌ای از داده‌های مختلف اجرا شود. داده‌های مورد نیاز برای این تست‌ها می‌توانند از طریق ویژگی‌های مختلفی تامین شوند که رایج‌ترین آن‌ها [InlineData] است.

ویژگی [InlineData] - داده‌های ساده و مستقیم
ویژگی [InlineData] زمانی مورد استفاده قرار می‌گیرد که مجموعه داده‌های تست کوچک و ثابت باشد. این ویژگی ساده‌ترین راه برای ارسال پارامتر به یک متد تست را فراهم می‌کند. اگرچه درک آن آسان است، اما بررسی آن به منظور پوشش کامل مفاهیم ضروری است.
در مثال زیر، نحوه استفاده از [InlineData] برای تست تابع بازگشتی (Recursive) محاسبه دنباله فیبوناچی نشان داده شده است:
using Fibonacci.Library;
using System.Diagnostics;
using Xunit;

namespace Fibonacci.UnitTest;

public class FibonacciTests
{
    private readonly FibonacciHelper fibonacciHelper;
    public FibonacciTests()
    {
        this.fibonacciHelper = new FibonacciHelper();
    }

    [Theory]
    [InlineData(0, 0)]
    [InlineData(1, 1)]
    [InlineData(5, 5)]
    [InlineData(10, 55)]
    public void Fibonacci_Recursive_InlineData(int input, int expected)
    {
        var watch = Stopwatch.StartNew();
        int result = fibonacciHelper.Recursive(input);
        watch.Stop();

        Assert.Equal(expected, result);

        Console.WriteLine($"[Recursive] Fibonacci({input}) took {watch.ElapsedMilliseconds} ms");
    }
}
در این مثال، ویژگی [Theory] به همراه چهار نمونه [InlineData] استفاده شده است. هر [InlineData] یک مجموعه داده تست منحصربه‌فرد را شامل می‌شود که در این مورد، شامل ورودی (input) و مقدار مورد انتظار (expected) برای تابع Recursive است. در نتیجه، متد Fibonacci_Recursive_InlineData چهار بار با داده‌های مختلف اجرا خواهد شد. [InlineData] برای داده‌های تست کوچک و ثابت بسیار مناسب است. خروجی کنسول نشان می‌دهد که هر تست با ورودی‌های مختلف اجرا شده و زمان اجرای هر کدام نیز ثبت شده است.

ویژگی [MemberData] - داده‌های قابل استفاده مجدد در کلاس مشابه
ویژگی [MemberData] به توسعه‌دهندگان اجازه می‌دهد تا از اعضای کلاس (فیلدها، خصوصیات یا متدهایی) که یک شیء از نوع IEnumerable<object[]> را به عنوان منبع داده تست برمی‌گردانند، استفاده مجدد کنند. این ویژگی به ویژه زمانی مفید است که بخواهید داده‌های تست را از متد تست جدا نگه دارید یا آن‌ها را به صورت برنامه‌نویسی تولید کنید.
مثال زیر نحوه استفاده از [MemberData] برای تست تابع تکراری (Iterative) محاسبه دنباله فیبوناچی را نشان می‌دهد:
using Fibonacci.Library;
using System.Collections.Generic;
using System.Linq;
using Xunit;

namespace Fibonacci.UnitTest
{
    public class FibonacciTest1
    {
        private readonly FibonacciHelper fibonacciHelper;

        public FibonacciTest1()
        {
            this.fibonacciHelper = new FibonacciHelper();
        }

        public static IEnumerable<object[]> GetData(int numFiboTest)
        {
            var data = new List<object[]>
            {
                new object[] { 6, 8 },
                new object[] { 7, 13 },
                new object[] { 8, 21 },
                new object[] { 9, 34 }
            };

            return data.Take(numFiboTest);
        }

        [Theory]
        [MemberData(nameof(GetData), parameters: 4)]
        public void Fibonacci_Iterative_MemberData(int input, int expected)
        {
            var watch = System.Diagnostics.Stopwatch.StartNew();
            int result = fibonacciHelper.Iterative(input);

            watch.Stop();

            Assert.Equal(expected, result);

            Console.WriteLine($"[Iterative] Fibonacci({input}) took {watch.ElapsedMilliseconds} ms");
        }
    }
}
در این مثال، متد استاتیک GetData یک لیست از آرایه‌های شیء (object[]) را برمی‌گرداند که هر آرایه شامل یک ورودی و مقدار مورد انتظار برای تست است. ویژگی [MemberData] با استفاده از nameof(GetData) به این متد ارجاع می‌دهد و پارامتر parameters: 4 مشخص می‌کند که چهار مجموعه داده اول از لیست data برای تست استفاده شوند. خروجی کنسول نشان می‌دهد که تست Fibonacci_Iterative_MemberData چهار بار با داده‌های ارائه شده توسط متد GetData اجرا شده است.

ویژگی [ClassData] - داده‌های پیچیده یا منابع خارجی
ویژگی [ClassData] امکان تعریف یک کلاس را فراهم می‌کند که رابط IEnumerable<object[]> را پیاده‌سازی می‌کند. این ویژگی به شما اجازه می‌دهد تا مجموعه‌های داده پیچیده‌تری را بارگیری یا ایجاد کنید، مانند خواندن داده‌ها از فایل‌ها، پایگاه‌های داده یا فراخوانی‌های API (به صورت شبیه‌سازی شده). این ویژگی به ویژه برای مدیریت مجموعه‌های داده بزرگ یا پویا که از منابع خارجی تامین می‌شوند، مفید است.
مثال زیر نحوه استفاده از [ClassData] را نشان می‌دهد:
using Fibonacci.Library;
using System.Collections;
using System.Collections.Generic;
using Xunit;

namespace Fibonacci.UnitTest;

public class AddTestDataClass : IEnumerable<object[]>
{
    public IEnumerator<object[]> GetEnumerator()
    {
        yield return new object[] { 11, 89 };
        yield return new object[] { 12, 144 };
        yield return new object[] { 13, 233 };
        yield return new object[] { 14, 377 };
    }

    IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}

public class FibonacciTest2
{
    private readonly FibonacciHelper fibonacciHelper;

    public FibonacciTest2()
    {
        this.fibonacciHelper = new FibonacciHelper();
    }

    [Theory]
    [ClassData(typeof(AddTestDataClass))]
    public void Fibonacci_Iterative_ClassData(int input, int expected)
    {
        var watch = System.Diagnostics.Stopwatch.StartNew();
        int result = fibonacciHelper.Iterative(input);
        watch.Stop();

        Assert.Equal(expected, result);
        Console.WriteLine($"[Iterative/ClassData] Fibonacci({input}) took {watch.ElapsedMilliseconds} ms");
    }
}
در این مثال، کلاس AddTestDataClass رابط IEnumerable<object[]> را پیاده‌سازی کرده و در متد GetEnumerator خود، چهار مجموعه داده تست را به صورت yield return ارائه می‌دهد. ویژگی [ClassData] با استفاده از typeof(AddTestDataClass) به این کلاس ارجاع می‌دهد. در نتیجه، متد Fibonacci_Iterative_ClassData چهار بار با داده‌های تعریف شده در کلاس AddTestDataClass اجرا خواهد شد. خروجی کنسول نشان می‌دهد که هر تست با ورودی‌های مختلف اجرا شده و زمان اجرای آن‌ها نیز ثبت شده است.

نتیجه‌گیری
در xUnit.net، انتخاب بین [InlineData], [MemberData] و [ClassData] به نیازها و پیچیدگی داده‌های تست شما بستگی دارد.
  • ویژگی [InlineData] برای مقادیر تست کوچک و ثابت بهترین گزینه است. استفاده از آن ساده و خوانا است، اما قابلیت استفاده مجدد در تست‌های مختلف را ندارد.
  • زمانیکه به داده‌های تست پویاتر یا قابل استفاده مجدد نیاز دارید و منطق تولید این داده‌ها در داخل کلاس تست شما قرار دارد، ویژگی [MemberData] ایده‌آل است. این ویژگی تعادلی بین قابلیت استفاده مجدد و پیچیدگی ارائه می‌دهد.
  • ویژگی [ClassData] بیشترین انعطاف‌پذیری را برای سناریوهای پیشرفته‌تر، مانند بارگیری مجموعه‌های داده بزرگ از منابع خارجی (فایل‌ها، APIها یا پایگاه‌های داده) فراهم می‌کند. اگرچه تنظیمات آن پیچیده‌تر است، اما امکان جداسازی منطق داده و ارتقاء قابلیت استفاده مجدد در سراسر مجموعه‌های تست را فراهم می‌سازد.


مشاهده مطلب اصلی