امروزه تست نویسی یکی از موارد مهم در توسعه و تولید یک نرم‌افزار به حساب می‌آید، اگرچه با نوشتن تست سرعت توسعه کاهش می‌یابد اما مزایای آن در زمان توسعه، افزودن ویژگی‌های جدید و تغییرات نمایان می‌شود. اگر از تست نویسی در پروژه‌های خود استفاده کنید و قسمتی از کد را تغییر دهید، نگران نیستید که قسمت دیگری از کار بیوفتد یا رفتار آن عوض شود چون در تست‌هایی که نوشته شده است مشخص می‌شود. در این مطلب میخواهیم چند نمونه تست با کتابخانه xUnit بنویسیم. 

یک سرویس را در نظر بگیرید که دیتایی را از سمت ریپازیتوری دریافت میکند و باید یک سری پردازش روی آن انجام دهد. در این مطلب هدف ما تست سرویس مربوطه می‌باشد نه ریپازیتوری یعنی میخواهیم برای خروجی سرویس تست بنویسم.
کلاس UserService را در نظر بگیرید که یک متد به نام AverageUsersAge دارد که میانگین سن کاربران را نمایش می‌دهد.

public class UserService : IUserService
{
    private readonly IUserRepository _userRepository;

    public UserService(IUserRepository userRepository)
    {
        _userRepository = userRepository;
    }
    public async Task<int> AverageUsersAge()
    {
        int result = 0;
        var people = await _userRepository.GetAllUsers();
        if (people is null)
            return result;

        result = (int)people.Select(a => a.Age).DefaultIfEmpty().Average();
        return result;
    }
}

در ابتدا بررسی می‌شود که اگر دیتای دریافت شده از ریپازیتوری null باشد، مقدار 0 را برمی‌گرداند. در غیر این صورت میانگین سن کاربران را باید برگشت بدهد. اکنون اگر بخواهیم برای این متد تست بنویسیم باید به صورت زیر عمل کنیم.
ابتدا یک پروژه از نوع xUnit Test Project .Net Core ایجاد کنید و پکیچ های زیر را نصب نمایید.

  • FluentAssertions
  • Moq
  • xunit
  • xunit.runner.visualstudio

همچنین می‌توانید برای نصب پکیج های مورد نیاز از کانفیگ زیر استفاده نمایید:

<ItemGroup>
  <PackageReference Include="FluentAssertions" Version="6.2.0" />
  <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.0.0" />
  <PackageReference Include="Moq" Version="4.16.1" />
  <PackageReference Include="xunit" Version="2.4.1" />
  <PackageReference Include="xunit.runner.visualstudio" Version="2.4.3"/>
</ItemGroup>

سپس یک کلاس به نام UserServiceTest درون پروژه تست ایجاد می‌کنیم.

public class UserServiceTest
{
    private readonly IUserService _userService;
    private readonly Mock<IUserRepository> _userRepository;
    public UserServiceTest()
    {
        _userRepository = new Mock<IUserRepository>();
        _userService = new UserService(_userRepository.Object);
    }
}

در سازنده کلاس UserTestService فیلد userService_ نمونه سازی شده است و یک ورودی از نوع <Mock<IUserRepository دریافت کرده است. اما ورودی کلاس UserService یک نمونه از IUserRepository دریافت میکند نه <Mock<IUserRepository. با استفاده از کتابخانه Mock میتوانیم یک آبجکت Fake را به سرویس ارسال کنیم تا در زمان نمونه سازی خطایی دریافت نکنیم. سپس میتوانیم متدهایی استفاده شده از Repository درون کلاس UserService را Mock کنیم. یعنی می‌توانیم خروجی متدهای Repository را شبیه‌ سازی کنیم و بر اساس آن تست های خودمان را بنویسیم.
برای مثال در تست زیر مشخص کرده‌ایم که درحالتی که خروجی متد GetAllUsers مربوط به IUserRepository برابر با null بود، باید مقدار صفر برگردانده شود:

[Fact]
public async Task AverageUsersAge_When_Repository_Return_Null_Then_Zero_Should_Be_Returned()
{
    _userRepository.Setup(a => a.GetAllUsers())
        .ReturnsAsync((List<User>)null);

    var result = await _userService.AverageUsersAge();

    result.Should().Be(0);
}

اکنون اگر تست را دیباگ کنید و متد AverageUsersAge را دیباگ کنید، زمانی که متد GetAllUsers می‌رسد، مقدار null برگشت داده می‌شود. در مثال پایین اگر خروجی متد GetAllUsers اگر Empty باشد تست شده است:

[Fact]
public async Task AverageUsersAge_When_Repository_Return_Empty_Then_Zero_Should_Be_Returned()
{
    _userRepository.Setup(a => a.GetAllUsers())
        .ReturnsAsync(new List<User>());

    var result = await _userService.AverageUsersAge();

    result.Should().Be(0);
}

در مثال پایین یک کلاس برای دیتاهای Fake در نظر گرفته‌ایم و بررسی کرده ایم که اگر از سمت ریپازیتوری سه رکورد با مقدارهای مشخص شده دریافت شود، میانگین رده سنی کاربران باید برابر مقدار محاسبه شده باشد.

[Fact]
public async Task AverageUsersAge_When_Repository_Return_List_Of_Users_Then_Average_Of_Age_Should_Be_Retunred()
{

    _userRepository.Setup(a => a.GetAllUsers())
        .ReturnsAsync(UserMockData.People);

    var result = await _userService.AverageUsersAge();

    var expected = (UserMockData.AdultUser.Age + UserMockData.ChildUser.Age + UserMockData.InfantUser.Age) / 3;

    result.Should().Be(expected);
}

کلاس UserMockData:

public static class UserMockData
 {
     public static List<User> People
     {
         get
         {
             return new List<User>
             {
                 AdultUser,
                 ChildUser,
                 InfantUser
             };
         }
     }
     public static User AdultUser
     {
         get
         {
             return new User
             {
                 Age = 55
             };
         }
     }
     public static User ChildUser
     {
         get
         {
             return new User
             {
                 Age = 10
             };
         }
     }
     public static User InfantUser
     {
         get
         {
             return new User
             {
                 Age = 2
             };
         }
     }
 }

با این روش ما می‌توانید برای سرویس‌های خود را که به یک ریپازیتوری یا سرویس دیگر وابستگی دارن تست بنویسید. در ادامه چند نکته در مورد best practices های تست نویسی را ارائه می‌دهیم.

;)

Powered by Froala Editor

نظرات

سایر نظرات
  • sivail

    عالی بود

    ارسال شده در تاریخ پنج شنبه، ۲۰ آبان ۱۴۰۰
    • admin

      قزووت سیویلی

      ارسال شده در تاریخ پنج شنبه، ۲۰ آبان ۱۴۰۰
  • ف.م

    با سلام و خسته نباشید یه سوال داشتم، تو پروژه های قدیمی یا در پروژه هایی که بصورت interface پیاده سازی نشدن، به عنوان مثال همین user که نوشتید فکر کنید بدون ارث بری از یک interface هست، تست نوشتن این موارد به چه شکلی هستمن خیلی دارم تحقیق می کنم رو این مورد اما به نتیجه مطلوبی نرسیدم

    ارسال شده در تاریخ شنبه، ۲۹ آبان ۱۴۰۰
    • admin

      سلام و وقت بخیر یکی از مزایای استفاده اینترفیس ها همین بحث تست نویسی هاست که خیلی کمک میکنه. اگه از اینترفیس استفاده نکرده باشید باید متدهایی رو که میخواید mock کنید رو به صورت virtual تعریف کنید. برای مثال من توی این مثال اومدم متد GetAllUsers رو mock کردم و چون از اینترفیس استفاده کردم این عمل به راحتی توسط کتابخانه Moq انجام میشه در غیر اینصورت مجبور بودم اون کلاسی رو که متد GetAllUsers داخلش قرار داره رو متدش رو به صورت virtual تعریف کنم تا کتابخانه moq بتونه خروجی مورد نظر من رو جنریت کنه. در واقع باید متدهاتون رو به صورت virtual تعریف کنید تا امکان mock شدن متدها برای کتابخانه هایی که عملیات mock رو انجام میدن فراهم بشه.

      ارسال شده در تاریخ شنبه، ۲۹ آبان ۱۴۰۰