لوگو اجیلیتی
یونیت تست چیست

آزمون واحد یا Unit Test | تعریف، مثال و بهترین روش‌ها

2 ساعت پیش
زمان مطالعه:
19 دقیقه

تصور کنید که در حال توسعه یک نرم‌افزار هستید و پس از چندین هفته کار فشرده، بالاخره محصول خود را اجرا می‌کنید، اما ناگهان با خطاهای غیرمنتظره مواجه می‌شوید! مثلاً سیستم پرداخت، تراکنش‌های ناموفق را برای بازگشت وجه به حساب مشتری به‌درستی به سیستم برنمی‌گرداند. 

در این لحظه، اهمیت یونیت تست / Unit Test بیشتر از همیشه مشخص می‌شود. زیرا مشتریان اولیه به‌شدت از محصول ناراضی شده‌اند.

آزمون واحد یکی از کلیدی‌ترین روش‌های تست نرم‌افزار است که هر جزء مستقل از کد را بررسی می‌کند تا از صحت عملکرد آن اطمینان حاصل شود. توسعه‌دهندگان با اجرای این تست‌ها، نه‌تنها مشکلات را در همان مراحل اولیه کشف می‌کنند، بلکه از بروز خطاهای بزرگ‌تر در آینده جلوگیری خواهد شد. یونیت تست‌ها حتی در مراحل ابتدایی ساخت MVP نیز کاربرد فراوان دارند.

در این مقاله، با مفهوم یونیت تست، تکنیک‌های مختلف آن، مزایا و بهترین روش‌های اجرای آن آشنا می‌شوید. اگر می‌خواهید کدهای خود را مطمئن‌تر، بادوام‌تر و بدون خطا کنید، مطالعه این راهنما را از دست ندهید!

یونیت تست چیست؟

آزمون واحد یا Unit Testing یک نوع آزمون برای تست نرم‌افزار است که می‌خواهد اجزای منفرد یک محصول نرم‌افزاری را به طور مجزا به تست و بررسی بگذارد. برنامه‌نویسان و توسعه‌دهندگان نرم‌افزار و حتی گاهی تیم‌های تضمین کیفیت / QA در طول فرایند توسعه محصول خود، تست‌های واحد را می‌نویسند و به اجرا می‌گذارند.

در یونیت تست، «واحدها» می‌توانند شامل توابع / Functions، رویه‌ها / Procedures، متدها / Methods، اشیا / Objects یا سایر اجزای سورس کد یک اپلیکیشن باشند. هر تیم توسعه بر اساس نیازهای خاص خود تعیین می‌کند که چه چیزی را به‌عنوان واحد یا Unit در نظر بگیرد. برای مثال، در «طراحی شی‌ءگرا / Object-Oriented Design»، معمولاً یک «کلاس / Class» به‌عنوان واحد تست در نظر گرفته می‌شود.

تست واحد اغلب از «ابجکت‌های شبیه‌سازی‌شده / Mock Objects» استفاده می‌کند تا از بخش‌های دیگر کدهای اصلی تقلید کند تا آزمایش‌ها تاحدامکان ساده و قابل‌پیش‌بینی باشند.

هدف اصلی یونیت تست این است که هر بخش از نرم‌افزار مطابق انتظار عمل کند و الزامات تعیین‌شده را برآورده کند. تست‌های واحد کمک می‌کنند تا ایرادات نرم‌افزار را قبل از انتشار آن به‌طور کامل بررسی کنیم.

توسعه چابک نرم‌افزار  /  Agile Software Development

مراحل اصلی اجرای یونیت تست

مراحل اصلی اجرای یونیت تست

در ادامه مراحل دقیق اجرای یونیت تست و مثالی برای درک بهتر آن را برای شما آورده‌ایم.

پیش از اجرای تست‌های واحد، باید محیط تست مناسب را راه‌اندازی کنیم. این بخش شامل انتخاب یک فریمورک تست مانند JUnit برای جاوا یا pytest برای پایتون است. به‌عنوان مثال، در یک پروژه پایتون، ابتدا باید pytest را نصب کنیم و ساختار مناسبی برای تست‌ها در نظر بگیریم.

در این مرحله، تست‌های مشخصی برای بررسی عملکرد بخش‌های مختلف نرم‌افزار نوشته می‌شوند. فرض کنید یک تابع در پایتون داریم که اعداد را به توان دو می‌رساند:

def square(n):

    return n * n

برای تست این تابع، می‌توانیم یک مورد تست ساده بنویسیم:

def test_square():

    assert square(3) == 9

    assert square(-2) == 4

پس از نوشتن تست‌ها، آن‌ها را اجرا می‌کنیم. مثلاً در پایتون با استفاده از pytest می‌توان تمامی تست‌ها را اجرا کرد. خروجی تست‌ها مشخص می‌کند که کدام بخش‌ها به‌درستی کار می‌کنند و کدام بخش‌ها نیاز به اصلاح دارند.

اگر تست‌ها موفق باشند، نشان می‌دهند که کد موردنظر درست عمل می‌کند. اما اگر تستی ناموفق باشد، باید علت آن بررسی شود. برای مثال، اگر تابع square به‌اشتباه مقدار n + n را برگرداند، تست شکست خواهد خورد و نیاز به اصلاح کد خواهیم داشت.

مزایای تست واحد

وقتی از مزایای یونیت تست صحبت می‌کنیم، در واقع به مجموعه‌ای از دستاوردها اشاره داریم که هم توسعه‌دهندگان را مطمئن‌تر می‌کند و هم کیفیت محصول را بالا می‌برد. در ادامه با مهم‌ترین مزایای یونیت تست آشنا می‌شویم.

  • شناسایی زودهنگام مشکلات در چرخه توسعه: فرض کنید در یک سیستم مدیریت کاربران، تابعی برای رمزگذاری پسوردها داریم. اگر این تابع به‌درستی کار نکند، یونیت تست به‌سرعت خطا را نشان می‌دهد و مشکل قبل از رسیدن به مرحله نهایی توسعه و قرارگرفتن در دست مشتری رفع می‌شود.

یونیت تست بهتر است در مراحل مختلف چرخه عمر محصول اجرا شود تا خطاها خیلی زودتر از رسیدن به دست کاربر نهایی شناسایی شوند.
  • کاهش هزینه‌های توسعه و نگهداری: با یافتن و رفع مشکلات در مراحل اولیه، هزینه‌های مرتبط با اصلاح کد در مراحل بعدی کاهش می‌یابد. مثلاً اگر در یک سیستم پرداخت آنلاین، یونیت تست بررسی کند که مقدار محاسبه‌شده برای مالیات صحیح است، از مشکلات مالی در آینده جلوگیری می‌شود.

  • «توسعه مبتنی بر تست / Test-Driven Development» یا TDD: در توسعه تست محور، ابتدا یک تست نوشته می‌شود و سپس کدی نوشته می‌شود که این تست را پاس کند. مثلاً قبل از نوشتن تابعی برای محاسبه فاکتوریل، تستی می‌نویسیم که انتظار دارد factorial(5) == 120 باشد.

روش دیگری نیز وجود دارد به نام توسعه رفتار محور یا BDD که بیشتر بر تعریف رفتارهای قابل مشاهده نرم‌افزار به‌جای تست مستقیم کد تأکید می‌کند که در مقاله مربوط به خود درباره آن مفصل توضیح داده‌ایم.

  • افزایش دفعات انتشار نرم‌افزار: اگر تست‌های واحد به‌خوبی پیاده‌سازی شوند، می‌توان با اطمینان بیشتری نرم‌افزار را در بازه‌های زمانی کوتاه‌تر منتشر کرد. مثلاً در یک اپلیکیشن موبایل، قبل از انتشار نسخه جدید، تست‌های واحد می‌توانند بررسی کنند که ویژگی‌های اصلی همچنان بدون مشکل کار می‌کنند یا خیر. سپس محصول را مجدداً منتشر کرد.

  • تسهیل فرایند «بازسازی و بهینه‌سازی کد / Code Refactoring»: اگر تست‌های واحد وجود داشته باشند، توسعه‌دهندگان می‌توانند با اطمینان بیشتری فرایند بازسازی و بهینه‌سازی کد را انجام دهند. برای مثال، اگر ساختار یک کلاس در جاوا تغییر کند، اما تست‌های واحد همچنان موفق باشند، نشان می‌دهد که تغییرات باعث خرابی عملکرد نشده‌اند.

  • شناسایی تغییراتی که ممکن است قوانین طراحی را نقض کنند: فرض کنید در یک سیستم بانکداری، تابعی برای انتقال وجه داریم. اگر تغییری باعث شود که مبلغ منفی نیز انتقال داده شود، تست‌های واحد می‌توانند این مشکل را شناسایی کنند.

  • کاهش عدم اطمینان در عملکرد نرم‌افزار: اگر تمام بخش‌های نرم‌افزار با تست‌های دقیق پوشش داده شوند، توسعه‌دهندگان راحت‌تر می‌فهمند که تغییرات جدید باعث ایجاد مشکلات ناخواسته نشده است.

  • مستندسازی رفتار سیستم برای درک بهتر اعضای آینده: تست‌های واحد می‌توانند نقش مستندات زنده را ایفا کنند. مثلاً اگر در یک API تابعی برای محاسبه نرخ تبدیل ارز وجود داشته باشد، تست‌های واحد به توسعه‌دهندگان و اعضای جدید تیم نشان می‌دهد که این تابع چگونه کار می‌کند و چه ورودی‌هایی را می‌پذیرد.

نحوه عملکرد یونیت تست

نحوه عملکرد یونیت تست

تست‌های واحد معمولاً در چهار مرحله انجام می‌شوند:

  1. برنامه‌ریزی و راه‌اندازی محیط: اعضای تیم توسعه مشخص می‌کنند که کدام واحدها در کد نیاز به تست دارند و چگونه می‌توان عملکرد آن‌ها را به‌طور دقیق بررسی کرد. به‌عنوان مثال، اگر در یک فروشگاه اینترنتی تابعی برای محاسبه تخفیف داریم، باید تست‌هایی برای بررسی عملکرد این تابع در شرایط مختلف (مثل تخفیف درصدی یا ثابت) در نظر بگیریم.

  2. نوشتن موارد تست و اسکریپت‌ها: اعضای تیم توسعه کد تست واحد را نوشته و اسکریپت‌هایی برای اجرای آن آماده می‌کنند. مثلاً اگر تابعی برای محاسبه مجموع یک لیست از اعداد داشته باشیم، می‌توانیم تستی بنویسیم که بررسی کند آیا خروجی این تابع برای [1, 2, 3] برابر 6 است یا خیر.

  3. اجرای یونیت تست: تست اجرا شده و نشان می‌دهد که کد برای هر سناریوی تست‌شده چگونه رفتار می‌کند. اگر تابع محاسبه تخفیف به‌درستی کار نکند، تست‌ها شکست خواهند خورد و مشخص می‌شود که باید کدها را اصلاح کنیم.

  4. تحلیل نتایج: تیم توسعه خطاها و مشکلات کد را شناسایی و برطرف می‌کند. اگر تستی شکست بخورد، بررسی می‌شود که آیا مشکل از کد اصلی است یا اینکه تست به‌درستی طراحی نشده است.

توسعه مبتنی بر تست / Test-Driven Development

توسعه مبتنی بر تست یا TDD یک روش رایج در یونیت تست است که در آن، ابتدا تست نوشته می‌شود و سپس کد واقعی برای عبور از تست توسعه می‌یابد. معمولاً تست در مراحل اول شکست می‌خورد؛ سپس برنامه‌نویس کدهایی را اصلاح می‌کند و مجدداً تست را اجرا می‌کنند.

مثلاً فرض کنید می‌خواهیم تابعی برای بررسی اینکه یک عدد اول است یا نه، ایجاد کنیم. در TDD، ابتدا یک تست برای این تابع می‌نویسیم:

def test_is_prime():

    assert is_prime(2) == True

    assert is_prime(4) == False

    assert is_prime(13) == True

از آنجایی که این تابع هنوز نوشته نشده، تست اولیه شکست می‌خورد. سپس کد اصلی را می‌نویسیم تا تست‌ها را پاس کند:

def is_prime(n):

    if n < 2:

        return False

    for i in range(2, int(n ** 0.5) + 1):

        if n % i == 0:

            return False

    return True

این روش باعث می‌شود که کد منسجم، ساختارمند و بدون خطا باشد.

ویژگی‌های یک تست واحد مؤثر

یک Unit Test کارآمد باید ویژگی‌های زیر را داشته باشد:

  • اجرای هر تست به‌صورت ایزوله: هر تست باید به‌صورت مستقل اجرا شود و برای جلوگیری از وابستگی به سایر بخش‌های سیستم، می‌توان از «استاب / Stub» یا «ماک / Mock» استفاده کرد. 

به‌عنوان مثال، اگر یک تابع به پایگاه داده متصل می‌شود، برای تست آن می‌توان یک پایگاه داده جعلی ایجاد کرد تا سرعت تست افزایش یابد و نتایج قابل‌پیش‌بینی باشند.

  • تمرکز روی ویژگی‌های حیاتی، نه بررسی تمام خطوط کد: Unit Test نباید تمام خطوط کد را بررسی کند، بلکه باید روی بخش‌هایی که تأثیر مستقیم بر رفتار محصول دارند، متمرکز باشد. 

مثلاً در یک اپلیکیشن پرداخت، مهم‌ترین تست این است که آیا مبلغ، صحیح محاسبه می‌شود یا خیر، نه اینکه فقط چک شود که هر متغیر مقداردهی شده است یا نه.

  • استفاده از «اظهارات / Assertions» برای تأیید نتایج: فریمورک‌های تست از assertions برای بررسی درستی خروجی‌ها استفاده می‌کنند. مثلاً در یک تست پایتون، می‌توان نوشت:

assert add(2, 3) == 5

اگر مقدار بازگشتی 5 نباشد، تست شکست خواهد خورد.

  • اجرای مکرر و تست در مراحل اولیه توسعه: تست‌های واحد باید مرتباً و در همان مراحل اولیه توسعه اجرا شوند تا مشکلات زودتر شناسایی و رفع شوند. در بسیاری از پروژه‌های بزرگ، تست‌های خودکار به‌طور مداوم اجرا می‌شوند تا از عملکرد صحیح نرم‌افزار اطمینان حاصل شود.

«یکی از معیارهای مهم برای بررسی کامل بودن تست‌ها، رعایت تعریف انجام‌شده یا همان DoD در چارچوب تیمی است.»

انواع تکنیک‌های Unit Test

انواع تکنیک‌های Unit Test

در این بخش انواع تکنیک‌های رایج اجرای یونیت تست را با هم بررسی می‌کنیم.

آزمون واحد ساختاری / Structural Unit Testing

تست ساختاری یک روش «تست جعبه سفید یا White Box Testing» است که در آن برنامه‌نویس یا توسعه‌دهنده موارد تست را بر اساس ساختار داخلی کد طراحی می‌کند. در این روش، تمامی مسیرهای ممکن در کد شناسایی می‌شوند. سپس تستر ورودی‌های مناسب را انتخاب کرده، تست را اجرا می‌کند و خروجی را بررسی می‌کند.

روش‌های اصلی تست ساختاری شامل موارد زیر است:

  • تست عبارات، شاخه‌ها و مسیرها: هر عبارت / Statement، شاخه / Branch یا مسیر / Path در برنامه باید حداقل یک‌بار توسط یک تست اجرا شود. تست عبارات، دقیق‌ترین سطح این روش است.

مثال: در یک تابع بررسی سن کاربر، اگر شاخه‌هایی برای زیر ۱۸ سال و ۱۸ سال به بالا وجود داشته باشد، باید تست‌هایی نوشته شود که هر دو مسیر را اجرا کنند:

def check_age(age):

    if age >= 18:

        return "Allowed"

    else:

        return "Not Allowed"

 

assert check_age(20) == "Allowed"

assert check_age(15) == "Not Allowed"

  • تست شرطی / Conditional Testing: این حالت به توسعه‌دهنده اجازه می‌دهد مسیر اجرای تست را بر اساس مقایسه مقادیر تعیین کند.

مثال: تست تابعی که وضعیت یک سفارش را بر اساس مقدار ورودی تعیین می‌کند:

def order_status(payment_done):

    return "Confirmed" if payment_done else "Pending"

 

assert order_status(True) == "Confirmed"

assert order_status(False) == "Pending"

  • تست عبارات منطقی / Expression Testing: بررسی رفتار برنامه در برابر مقادیر مختلف یک عبارت منظم / Regex

مثال: تست صحت فرمت یک آدرس ایمیل:

import re

 

def is_valid_email(email):

    return bool(re.match(r"[^@]+@[^@]+\.[^@]+", email))

 

assert is_valid_email("test@example.com") == True

assert is_valid_email("invalid-email") == False

آزمون واحد عملکردی / Functional Unit Testing

Unit Test عملکردی، یک روش «تست جعبه سیاه یا Black Box Testing» است که برای بررسی عملکرد یک مؤلفه نرم‌افزار استفاده می‌شود. در این روش، بدون بررسی جزئیات داخلی و منطق پیاده‌سازی مؤلفه، ورودی‌ها و خروجی‌های آن بررسی می‌شوند تا مشخص شود که آیا مطابق انتظار عمل می‌کند یا خیر. این نوع تست معمولاً برای تأیید صحت عملکردهای خاص یک ماژول یا کامپوننت منفرد در نرم‌افزار مورد استفاده قرار می‌گیرد.

به بیان ساده‌تر، یونیت تست عملکردی تضمین می‌کند که هر بخش از سیستم نرم‌افزاری، به‌طور مستقل و مطابق با الزامات عملکردی تعریف‌شده، رفتار صحیحی از خود نشان می‌دهد. برخلاف یونیت تست سنتی‌تر که ممکن است بر پیاده‌سازی داخلی و کدها تمرکز کند، یونیت تست عملکردی تنها بر خروجی‌های قابل مشاهده و تعامل با مؤلفه‌ها تأکید دارد.

 

روش‌های اصلی تست عملکردی شامل موارد زیر است:

  • تست دامنه ورودی / Input Domain Testing: بررسی اندازه و نوع داده‌های ورودی و مقایسه آن‌ها با کلاس‌های معادل.

مثال: تست تابعی که عدد صحیح را می‌گیرد و بررسی می‌کند که مقدار در محدوده مجاز است یا خیر:

def validate_age(age):

    return 0 <= age <= 100

 

assert validate_age(25) == True

assert validate_age(-5) == False

assert validate_age(150) == False

  • تحلیل مقدار مرزی / Boundary Value Analysis: بررسی رفتار نرم‌افزار در برابر ورودی‌هایی که خارج از مقادیر مرزی هستند.

مثال: تست تابعی که نمره آزمون را دریافت می‌کند و بررسی می‌کند که مقدار ورودی معتبر است یا نه:

def validate_score(score):

    return 0 <= score <= 100

 

assert validate_score(0) == True

assert validate_score(100) == True

assert validate_score(-1) == False

assert validate_score(101) == False

  • بررسی نحوی / Syntax Checking: بررسی اینکه آیا نرم‌افزار ورودی‌های نحوی را به‌درستی تفسیر می‌کند.

مثال: تست تابعی که یک عبارت محاسباتی را دریافت کرده و بررسی می‌کند که آیا نحو  / Syntax آن معتبر است یا خیر:

def is_valid_expression(expr):

    try:

        eval(expr)

        return True

    except:

        return False

 

assert is_valid_expression("2 + 3 * 4") == True

assert is_valid_expression("10 / 0") == False  # خطای تقسیم بر صفر

assert is_valid_expression("2 + (3 * 4") == False  # پرانتز بسته نشده

  • تقسیم‌بندی معادل / Equivalent Partitioning: تقسیم داده‌های ورودی یک واحد نرم‌افزاری به دسته‌های معادل و اعمال تست روی هر دسته.

مثال: در تابعی که سطح دسترسی کاربران را تعیین می‌کند، داده‌های ورودی به سه دسته تقسیم می‌شوند:

def access_level(role):

    if role == "admin":

        return "Full Access"

    elif role == "user":

        return "Limited Access"

    else:

        return "No Access"

 

assert access_level("admin") == "Full Access"

assert access_level("user") == "Limited Access"

assert access_level("guest") == "No Access"

این روش‌ها به توسعه‌دهندگان کمک می‌کند تا عملکرد و استحکام کد را از زوایای مختلف بررسی کنند.

تکنیک‌های مبتنی بر خطا

تست‌های واحد مبتنی بر خطا بهتر است توسط توسعه‌دهندگانی که کد اصلی را طراحی کرده‌اند نوشته شوند. این تکنیک‌ها شامل موارد زیر هستند:

  • کاشتن خطا / Fault Seeding: در این روش، توسعه‌دهندگان عمداً خطاهای مشخصی را در کد قرار می‌دهند و سپس تست‌ها را اجرا می‌کنند تا مطمئن شوند که این خطاها شناسایی می‌شوند.

مثال: فرض کنید تابع زیر برای محاسبه مجموع دو عدد نوشته شده است، اما به‌عمد یک خطای منطقی در آن قرار داده‌ایم:

def add(a, b):

    return a - b  # خطای عمدی در عملیات جمع

 

assert add(2, 3) == 5  # این تست شکست خواهد خورد و خطا را آشکار می‌کند

  • تست جهش / Mutation Testing: در این تکنیک، برخی از دستورات در سورس کد تغییر داده می‌شوند تا بررسی شود که آیا تست‌ها می‌توانند این تغییرات را تشخیص دهند یا نه. این روش هزینه‌بر است، به‌ویژه در پروژه‌های نرم‌افزاری بزرگ. مثال: فرض کنید در یک تابع بررسی عدد زوج، عمداً یک تغییر ایجاد کنیم:

def is_even(n):

    return n % 2 != 0  # جهش عمدی (باید == 0 باشد)

 

assert is_even(4) == True  # این تست شکست خواهد خورد و خطا را آشکار می‌کند

  • استفاده از داده‌های تست تاریخی / Historical Test Data: این روش از اطلاعات تست‌های گذشته استفاده می‌کند تا اولویت اجرای تست‌ها را بر اساس میزان احتمال بروز خطا تعیین کند.

مثال: اگر در نسخه‌های قبلی نرم‌افزار، تابع تبدیل دما به‌کرات دچار مشکل شده باشد، آن را به‌عنوان اولویت بالاتر برای تست در نظر می‌گیریم:

def celsius_to_fahrenheit(c):

    return (c * 9/5) + 32

 

assert celsius_to_fahrenheit(0) == 32  # بر اساس داده‌های تاریخی، این تابع قبلاً خطا داشته است.

assert celsius_to_fahrenheit(100) == 212

این تکنیک‌ها به توسعه‌دهندگان کمک می‌کنند تا کدهای مقاوم‌تر و قابل‌اطمینان‌تری تولید کنند و از بروز خطاهای احتمالی قبل از انتشار نرم‌افزار جلوگیری کنند.

مثال‌هایی از Unit Test

مثال‌هایی از Unit Test

سیستم‌های مختلف از انواع متفاوتی از تست‌های واحد پشتیبانی می‌کنند.

یونیت تست در اندروید

تیم‌ها می‌توانند تست‌های واحد را روی دستگاه‌های اندرویدی یا کامپیوترهای دیگر اجرا کنند. دو نوع اصلی از تست‌های واحد برای اندروید وجود دارد:

  1. تست‌های ابزارمحور / Instrumented Tests: این تست‌ها روی دستگاه‌های واقعی یا شبیه‌سازی اندرویدی اجرا می‌شوند. متخصص تست، اپلیکیشن و تست مرتبط با آن را نصب می‌کند تا بتواند دستورات را اعمال کرده و وضعیت اپلیکیشن را بررسی کند.

    • این تست‌ها معمولاً برای بررسی رابط کاربری / UI Testing به کار می‌روند.

    • یک مثال ساده از تست ابزارمحور: بررسی سازگاری پایگاه داده SQLite روی نسخه‌های مختلف اندروید. توسعه‌دهنده تست‌هایی را اجرا می‌کند تا مطمئن شود که اپلیکیشن به‌درستی با نسخه‌های مختلف SQLite سازگار است.

  2. تست‌های محلی / Local Unit Tests: این تست‌ها روی سرور توسعه یا کامپیوتر شخصی اجرا می‌شوند و معمولاً کوچک، سریع و مستقل از سایر قسمت‌های اپلیکیشن هستند.

    • تست‌های محلیِ بزرگ‌تر می‌توانند شامل اجرای شبیه‌ساز اندروید روی سیستم محلی باشند. مانند Robolectric برای تست‌های محلی.

مثال: در یک تست ابزارمحور UI، اگر متخصص تست یا کاربر روی دکمه «Start» کلیک کند، باید پیغام «Hello» در صفحه نمایش داده شود:

اگر دکمه "Start" کلیک شود//

onView(withText("Start"))

    .perform(click())

 

سپس پیام "Hello" باید نمایش داده شود//

onView(withText("Hello"))

    .check(matches(isDisplayed()))

در این مثال، یک تست محلی روی ViewModel اجرا شده که بررسی می‌کند پس از بارگذاری داده‌ها، مقدار داده‌ها نامعتبر نباشد:

// اگر یک نمونه از ViewModel1 داشته باشیم

val viewModel = ViewModel1(ExampleDataRepository)

 

// پس از بارگذاری داده‌ها

viewModel.loadData()

 

// بررسی می‌کنیم که داده‌ها مقداردهی شده باشند

assertTrue(viewModel.data != null)

یونیت تست در Angular

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

Angular یک پکیج تست داخلی ارائه می‌دهد که شامل دو ابزار اصلی است:

  • TestBed: ابزار اصلی تست در Angular

  • async: برای مدیریت عملیات ناهمگام در تست‌ها

ساختار تست در Angular

  • describe: شامل مجموعه‌ای از بلوک‌های تست مانند it، xit و beforeEach است.

  • beforeEach: پیش از اجرای سایر تست‌ها اجرا شده و محیط تست را آماده می‌کند.

در کد زیر، قبل از اجرای تست‌ها، کامپوننت درون beforeEach تعریف و کامپایل می‌شود:

beforeEach(async(() => {

   TestBed.configureTestingModule({

      declarations: [

         AppComponent

      ],

   }).compileComponents();

}));

تست ایجادشدن کامپوننت

پس از مقداردهی اولیه، در بلوک it بررسی می‌کنیم که آیا کامپوننت به‌درستی ایجاد شده است یا خیر:

it('creates the app', async(() => {

    const fixture = TestBed.createComponent(AppComponent);

    const app = fixture.debugElement.componentInstance;

    expect(app).toBeTruthy();

}));

بررسی مقدار ویژگی title در کامپوننت

در این مرحله، مقدار ویژگی title را در کامپوننت بررسی می‌کنیم تا مطمئن شویم مقدار مورد انتظار را دارد:

it(`title should be 'angular-unit-test'`, async(() => {

     const fixture = TestBed.createComponent(AppComponent);

     const app = fixture.debugElement.componentInstance;

     expect(app.title).toEqual('angular-unit-test');

}));

بررسی نمایش صحیح عنوان در مرورگر

در این مرحله، اجرای کامپوننت در محیط مرورگر شبیه‌سازی می‌شود و بررسی می‌کنیم که عنوان درون تگ <h1> به‌درستی نمایش داده شود:

it('render title in h1 tag', async(() => {

   const fixture = TestBed.createComponent(AppComponent);

   fixture.detectChanges();

   const compiled = fixture.debugElement.nativeElement;

   expect(compiled.querySelector('h1').textContent).toContain('Welcome to angular-unit-test!');

}));

این تست‌ها باعث می‌شوند که توسعه‌دهندگان قبل از انتشار برنامه، عملکرد صحیح کامپوننت‌های کلیدی را بررسی کرده و از عدم وجود خطاهای اساسی مطمئن شوند.

یونیت تست در Node.js

Node.js به توسعه‌دهندگان اجازه می‌دهد تا کدهای جاوا اسکریپت را در سمت سرور اجرا کنند. این پلتفرم متن‌باز بوده و با فریمورک‌های محبوب تست جاوا اسکریپت مانند Mocha ادغام می‌شود.

در Mocha، از کلمات کلیدی API تست برای مشخص‌کردن تست‌ها استفاده می‌شود:

  • ()it: نشان‌دهنده یک تست واحد

  • ()describe: گروهی از تست‌ها را در بر می‌گیرد و می‌تواند شامل زیرگروه‌های تست نیز باشد.

هر تابع تست شامل دو آرگومان است:

  1. توضیحی که در گزارش تست نمایش داده می‌شود.

  2. یک تابع بازگشتی / Callback Function که تست را اجرا می‌کند.

مثال از یک تست ساده در Mocha

const { describe } = require('mocha');

const assert = require('assert');

 

describe('Simple test suite:', function() {

    it('1 === 1 should be true', function() {

        assert(1 === 1);

    });

});

اجرای تست و خروجی آن:

$ cd src/projects/IBM-Developer/Node.js/Course/Unit-9

$ ./node_modules/.bin/mocha test/example1.js

 

  Simple test suite:

     1 === 1 should be true

 

  1 passing (5ms)

کتابخانه‌های Assertion در Mocha

Mocha از هر کتابخانه Assertion پشتیبانی می‌کند.

در مثال بالا از ماژول assert در Node.js استفاده شده که ساده است اما انعطاف‌پذیری کمتری از گزینه‌های دیگر دارد.

توسعه‌دهندگان می‌توانند از کتابخانه‌های پیشرفته‌تری مانند Chai برای نوشتن تست‌های توصیفی‌تر استفاده کنند.

یونیت تست در React Native

  • React Native یک فریمورک متن‌باز برای توسعه اپلیکیشن‌های موبایل با جاوا اسکریپت است. این فریمورک به‌صورت پیش‌فرض از Jest برای انجام تست‌های واحد پشتیبانی می‌کند.

  • Jest: معمولاً در پروژه‌های React Native از ابتدا نصب شده و به‌عنوان یک راهکار تست آماده‌به‌کار ارائه می‌شود.

توسعه‌دهندگان می‌توانند تنظیمات Jest را در فایل package.json مشخص کنند:

"scripts": {

    "test": "jest"

},

"jest": {

    "preset": "jest-react-native"

}

مثال از تست یک تابع ساده در Jest

فرض کنید در یک برنامه، تابعی برای جمع دو عدد داریم. می‌توانیم تست آن را در یک فایل جداگانه بنویسیم:

فایل ExampleSum.js

function ExampleSum(a, b) {

    return a + b;

}

 

module.exports = ExampleSum;

فایل تست ExampleSumTest.js

const ExampleSum = require('./ExampleSum');

 

test('ExampleSum equals 3', () => {

    expect(ExampleSum(1, 2)).toBe(3);

});

اجرای تست و خروجی پیش‌بینی‌شده Jest

PASS  ./ExampleSumTest.js

ExampleSum equals 3 (5ms)

 اگر مقدار خروجی تابع با مقدار مورد انتظار مطابقت داشته باشد، تست با موفقیت پاس می‌شود.

بهترین شیوه‌ها / Best Practices برای تست واحد

بهترین شیوه‌ها Best Practices برای تست واحد

1. از یک فریمورک یونیت تست استفاده کنید

به‌جای نوشتن تست‌های سفارشی برای هر بخش از کد، از فریمورک‌های تست استاندارد مانند pytest یا Unit Test در پایتون استفاده کنید. این فریمورک‌ها باعث خودکارسازی و استانداردسازی تست‌ها در پروژه‌های کوچک و بزرگ می‌شوند.

2. تست‌های واحد را خودکار کنید

تست‌های واحد را طوری برنامه‌ریزی کنید که در رویدادهای کلیدی توسعه، مانند پوش کردن کد / Pushing Code یا استقرار به‌روزرسانی‌ها / Deploying Updates، به‌صورت خودکار اجرا شوند. این کار باعث می‌شود مشکلات در مراحل اولیه توسعه شناسایی شوند.

«در بسیاری از تیم‌های حرفه‌ای، تست‌های واحد به‌صورت خودکار در فرایند CI/CD اجرا می‌شوند تا مطمئن شوند کد جدید باعث خرابی بخش‌های موجود نمی‌شود.»

3. فقط یک دستور assert در هر تست استفاده کنید

هر Unit Test باید فقط یک دستور assert داشته باشد. این کار وضوح نتیجه تست را افزایش می‌دهد و یافتن و برطرف‌کردن مشکلات را ساده‌تر می‌کند.

4. تست‌های واحد را از ابتدای پروژه اجرا کنید

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

تفاوت تست واحد با سایر انواع تست نرم‌افزار

1. تست یکپارچگی / Integration Testing

برای بررسی این است که آیا بخش‌های مختلف سیستم به‌درستی با یکدیگر کار می‌کنند یا خیر. این تست تعامل میان ماژول‌های مختلف را ارزیابی می‌کند.

2. تست کارکردی / Functional Testing

برای بررسی این است که آیا نرم‌افزار، مطابق با نیازهای از پیش تعیین‌شده عمل می‌کند یا خیر. هدف اصلی این تست، تطابق عملکرد نرم‌افزار با مستندات نیازمندی‌ها است.

3. تست عملکرد / Performance Testing

عملکرد نرم‌افزار را از نظر سرعت، مصرف حافظه و کارایی کلی بررسی می‌کند تا اطمینان حاصل شود که سیستم در شرایط مختلف روان و بهینه اجرا می‌شود.

4. تست پذیرش / Acceptance Testing

به‌صورت دستی توسط ذی‌نفعان انجام می‌شود تا تأیید کنند که نرم‌افزار مطابق با انتظارات واقعی کاربران عمل می‌کند.

5. تست امنیت / Security Testing 

امنیت نرم‌افزار را ارزیابی کرده و به دنبال شناسایی آسیب‌پذیری‌ها و ریسک‌های امنیتی (مانند تهدیدهای مربوط به کتابخانه‌های شخص ثالث) در فرایند توسعه نرم‌افزار است.

جمع‌بندی

یونیت تست یا آزمون واحد نه‌تنها کیفیت نرم‌افزار را تضمین می‌کند، بلکه فرایند توسعه را سریع‌تر، کم‌هزینه‌تر و مطمئن‌تر می‌سازد. با اجرای درست این تست‌ها، می‌توانید خطاها را قبل از رسیدن به کاربر نهایی شناسایی کنید و محصولی ارائه دهید که پایدار، مقیاس‌پذیر و بدون نقص باشد. پیشنهاد این است: تست واحد را به بخشی از جریان توسعه محصول خود تبدیل کنید.

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

سوالات متداول

آزمون واحد یک روش تست نرم‌افزار است که در آن هر بخش از کد به‌صورت مستقل بررسی می‌شود تا از صحت عملکرد آن اطمینان حاصل شود. این تست به شناسایی سریع خطاها، کاهش هزینه‌های توسعه و افزایش کیفیت کد کمک می‌کند.
آزمون واحد روی اجزای مستقل نرم‌افزار تمرکز دارد، درحالی‌که آزمون یکپارچگی / Integration Testing بررسی می‌کند که ماژول‌ها و بخش‌های مختلف نرم‌افزار چگونه با یکدیگر تعامل دارند.
ابزارهای مختلفی برای Unit Test در زبان‌های برنامه‌نویسی مختلف وجود دارند، از جمله: - Junit برای جاوا - Pytest و unittest برای پایتون - Mocha و Jest برای جاوا اسکریپت و Node.js - TestBed برای Angular
بهترین زمان برای اجرای آزمون واحد، در همان مراحل ابتدایی توسعه است. همچنین، می‌توان این تست‌ها را به‌صورت خودکار در فرایندهای CI/CD اجرا کرد تا با هر تغییر در کد، صحت عملکرد بررسی شود.
تست واحد دستی شامل اجرای تست‌ها توسط توسعه‌دهنده است، درحالی‌که در تست واحد خودکار، تست‌ها با استفاده از ابزارهایی مانند Jest، Mocha یا pytest به‌طور خودکار اجرا می‌شوند. تست‌های خودکار سرعت بالاتری دارند و از بروز خطاهای انسانی جلوگیری می‌کنند.
خیر، آزمون واحد در محیط توسعه و تست اجرا می‌شود و تأثیری روی عملکرد نسخه اصلی نرم‌افزار ندارد. بااین‌حال، اجرای بیش از حد تست‌های واحد در محیط تولید می‌تواند منابع سیستمی پردازشی را اشغال کند.
عضو خبرنامه اجیلیتی شوید تا مقالات تخصصی، راهکارهای به‌روز و ابزارهای کاربردی را در باکس خود دریافت کنید.