تست‌نویسی با هوش مصنوعی؛ آموزش تولید Unit Test و API Test با AI

در این آموزش یاد می‌گیرید با AI سناریوهای تست، Unit Test و API Test تولید کنید و یک ابزار عملی بسازید که کد را تحلیل، تست Pytest ایجاد و نتیجه اجرای تست‌ها را گزارش می‌کند.

Share
تست‌نویسی با هوش مصنوعی؛ آموزش تولید Unit Test و API Test با AI

تست‌نویسی با هوش مصنوعی؛ از طراحی سناریو تا تولید تست قابل اجرا

هوش مصنوعی می‌تواند در چند ثانیه برای یک تابع ده‌ها تست تولید کند؛ اما تعداد زیاد تست الزاماً به معنای کیفیت بالاتر نرم‌افزار نیست.

یک تست مفید باید:

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

اگر کدی اشتباه باشد و مدل از روی همان کد تست تولید کند، ممکن است اشتباه موجود را به‌عنوان رفتار صحیح در تست ثبت کند. بنابراین ورودی مناسب برای تولید تست فقط Source Code نیست؛ مدل باید Specification، قرارداد API، قواعد کسب‌وکار و رفتار مورد انتظار را نیز ببیند.

در این مقاله یک پروژه واقعی می‌سازیم:

  1. یک سرویس محاسبه سبد خرید ایجاد می‌کنیم.
  2. Specification آن را می‌نویسیم.
  3. به‌صورت دستی سناریوهای مهم را مشخص می‌کنیم.
  4. با API درواره Test Plan ساختاریافته تولید می‌کنیم.
  5. تست‌های Pytest ایجاد می‌کنیم.
  6. تست‌ها را در یک فرایند محدود اجرا می‌کنیم.
  7. Coverage و کیفیت Assertionها را بررسی می‌کنیم.
  8. همین روش را برای APIهای FastAPI توسعه می‌دهیم.

تست‌نویسی با هوش مصنوعی چیست؟

AI-assisted Testing یعنی استفاده از مدل‌های هوش مصنوعی برای کمک به فعالیت‌هایی مانند:

  • استخراج رفتارهای قابل تست از نیازمندی‌ها
  • تولید Test Case
  • شناسایی Edge Case
  • ساخت Unit Test
  • ساخت Integration Test
  • تولید API Test
  • ساخت داده آزمایشی
  • توضیح علت شکست تست
  • یافتن تست‌های تکراری
  • تشخیص بخش‌های بدون پوشش
  • تبدیل Bug Report به Regression Test
  • تولید Property-based Test
  • نگارش Test Plan
  • تحلیل تغییرات کد و پیشنهاد تست مرتبط

هوش مصنوعی در این فرایند یک دستیار تولید و تحلیل است. تست تولیدشده باید مانند کد Production بازبینی شود.

مزایای استفاده از AI در تست نرم‌افزار

افزایش سرعت شروع تست‌نویسی

نوشتن اولین نسخه تست‌ها معمولاً زمان‌بر است. مدل می‌تواند اسکلت Fixtureها، Parametrize و Test Caseهای اولیه را بسازد.

کشف Edge Caseهای فراموش‌شده

مدل ممکن است سناریوهایی مانند این موارد را یادآوری کند:

  • ورودی خالی
  • مقدار صفر
  • عدد منفی
  • مقدار بسیار بزرگ
  • None
  • نوع داده نادرست
  • مرز دقیق تخفیف
  • خطای Dependency
  • پاسخ Timeout
  • داده تکراری

تبدیل نیازمندی به معیار قابل آزمون

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

تولید Regression Test از Bug

پس از رفع خطا، مدل می‌تواند بر اساس Bug Report یک تست بازگشت بسازد تا همان مشکل دوباره ایجاد نشود.

کمک به شناخت کد قدیمی

برای Legacy Code می‌توان ابتدا Characterization Test تولید کرد تا رفتار فعلی سیستم ثبت شود. این تست‌ها لزوماً تأیید نمی‌کنند رفتار فعلی صحیح است؛ فقط آن را مستند می‌کنند.

محدودیت‌های AI در تولید تست

کپی‌کردن پیاده‌سازی داخل تست

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

Assertionهای ضعیف

نمونه نامناسب:

result = calculate_total(items)
assert result is not None

این تست تقریباً هیچ رفتار مهمی را بررسی نمی‌کند.

Mock بیش از حد

اگر تمام Dependencyها Mock شوند، ممکن است تست دیگر رفتار واقعی واحد مورد نظر را بررسی نکند.

تست رفتار اشتباه فعلی

مدل از روی Source Code نتیجه می‌گیرد کد چه می‌کند، نه اینکه الزاماً چه باید بکند.

تولید تست‌های ظاهری

گاهی تست اجرا می‌شود و Pass می‌شود، اما Assertion آن ارزش عملی ندارد.

نادیده‌گرفتن قواعد کسب‌وکار

بدون Specification، مدل نمی‌داند تخفیف، گردکردن مبلغ یا رفتار خطا چگونه باید باشد.

ورودی مناسب برای تولید تست

برای نتیجه بهتر این اطلاعات را ارائه کنید:

  • زبان برنامه‌نویسی
  • Framework تست
  • Source Code
  • قرارداد تابع یا API
  • رفتار مورد انتظار
  • قواعد کسب‌وکار
  • Dependencyهای خارجی
  • موارد خارج از محدوده
  • Convention پروژه
  • نمونه تست موجود
  • نوع تست مورد نیاز
  • محدودیت Mock
  • نسخه Runtime

نمونه پرامپت:

برای کد زیر تست Pytest تولید کن.

رفتار مورد انتظار:
- قیمت و تعداد باید غیرمنفی باشند.
- سبد خالی باید مبلغ صفر برگرداند.
- تخفیف فقط روی جمع اقلام اعمال شود.
- مالیات بعد از تخفیف محاسبه شود.
- محاسبات پولی باید با Decimal انجام شوند.

قواعد تست:
- رفتار عمومی را بررسی کن، نه جزئیات داخلی تابع.
- از Parametrize برای ورودی‌های مشابه استفاده کن.
- برای هر Test نام توصیفی انتخاب کن.
- فقط در صورت نیاز Mock بساز.
- خطاهای مورد انتظار را با pytest.raises بررسی کن.
- ابتدا Test Plan و سپس کد تست را ارائه بده.

کد:
[SOURCE CODE]

تفاوت Test Plan و Test Code

بهتر است مستقیماً از مدل نخواهید کد تست تولید کند. ابتدا یک Test Plan بسازید.

نمونه:

شناسهسناریوورودینتیجه مورد انتظارنوع
T-01سبد خالی[]مبلغ صفرNormal
T-02یک کالاقیمت ۱۰۰، تعداد ۲جمع ۲۰۰Normal
T-03تعداد صفرقیمت ۱۰۰، تعداد ۰جمع صفرBoundary
T-04قیمت منفیقیمت منفیخطای ValidationInvalid
T-05تخفیف در مرزجمع دقیقاً برابر حداقلرفتار مطابق SpecificationBoundary
T-06مالیات بعد از تخفیفتخفیف و مالیاتترتیب صحیح محاسبهBusiness Rule

ابتدا این برنامه را بررسی کنید، سپس Test Code را از روی موارد تأییدشده بسازید.

انواع تست‌هایی که AI می‌تواند تولید کند

Unit Test

یک تابع یا کلاس را جدا از اجزای دیگر بررسی می‌کند.

Integration Test

تعامل چند جزء مانند Service و Database Repository را آزمایش می‌کند.

API Test

Request و Response، Status Code، Validation و Contract را بررسی می‌کند.

Regression Test

خطایی که قبلاً رخ داده است به یک تست دائمی تبدیل می‌شود.

Property-based Test

به‌جای چند ورودی ثابت، ویژگی عمومی تابع روی تعداد زیادی داده تولیدشده بررسی می‌شود.

Snapshot Test

خروجی پیچیده با نسخه مرجع مقایسه می‌شود. استفاده بی‌دقت از Snapshot می‌تواند تغییرات اشتباه را صرفاً با به‌روزرسانی فایل مرجع مخفی کند.

Contract Test

توافق میان سرویس مصرف‌کننده و ارائه‌دهنده بررسی می‌شود.

پروژه عملی: تست سرویس محاسبه سبد خرید

ساختار پروژه:

ai-test-generator/
├── app/
│   ├── __init__.py
│   ├── cart.py
│   └── api.py
├── tests/
│   ├── __init__.py
│   ├── test_cart.py
│   └── test_api.py
├── generated_tests/
├── specifications/
│   └── cart.md
├── tools/
│   ├── generate_test_plan.py
│   └── generate_tests.py
├── .env
└── requirements.txt

ایجاد پروژه و نصب ابزارها

mkdir ai-test-generator
cd ai-test-generator

python -m venv .venv

فعال‌سازی در Linux و macOS:

source .venv/bin/activate

فعال‌سازی در Windows:

.venv\Scripts\Activate.ps1

نصب وابستگی‌ها:

pip install \
  openai \
  python-dotenv \
  pydantic \
  pytest \
  pytest-cov \
  fastapi \
  httpx

ساخت پوشه‌ها:

mkdir app tests generated_tests specifications tools

فایل‌های خالی زیر را ایجاد کنید:

app/__init__.py
tests/__init__.py

پیاده‌سازی سرویس سبد خرید

فایل app/cart.py:

from dataclasses import dataclass
from decimal import (
    Decimal,
    ROUND_HALF_UP,
)


MONEY_QUANTIZER = Decimal("0.01")


@dataclass(frozen=True)
class CartItem:
    sku: str
    unit_price: Decimal
    quantity: int


@dataclass(frozen=True)
class CartResult:
    subtotal: Decimal
    discount: Decimal
    taxable_amount: Decimal
    tax: Decimal
    total: Decimal


class InvalidCartItemError(ValueError):
    pass


def money(value: Decimal) -> Decimal:
    return value.quantize(
        MONEY_QUANTIZER,
        rounding=ROUND_HALF_UP,
    )


def calculate_cart(
    items: list[CartItem],
    discount_rate: Decimal = Decimal("0"),
    tax_rate: Decimal = Decimal("0"),
) -> CartResult:
    if discount_rate < 0 or discount_rate > 1:
        raise ValueError(
            "discount_rate must be between 0 and 1"
        )

    if tax_rate < 0 or tax_rate > 1:
        raise ValueError(
            "tax_rate must be between 0 and 1"
        )

    subtotal = Decimal("0")

    for item in items:
        if item.unit_price < 0:
            raise InvalidCartItemError(
                "unit_price cannot be negative"
            )

        if item.quantity < 0:
            raise InvalidCartItemError(
                "quantity cannot be negative"
            )

        subtotal += (
            item.unit_price
            * item.quantity
        )

    subtotal = money(subtotal)
    discount = money(
        subtotal * discount_rate
    )
    taxable_amount = money(
        subtotal - discount
    )
    tax = money(
        taxable_amount * tax_rate
    )
    total = money(
        taxable_amount + tax
    )

    return CartResult(
        subtotal=subtotal,
        discount=discount,
        taxable_amount=taxable_amount,
        tax=tax,
        total=total,
    )

نوشتن Specification مستقل از کد

فایل specifications/cart.md:

# Cart calculation specification

- قیمت و تعداد هر کالا باید صفر یا مثبت باشد.
- سبد خالی معتبر است و تمام مبالغ آن صفر هستند.
- Subtotal برابر مجموع قیمت واحد ضرب‌در تعداد است.
- نرخ تخفیف باید بین صفر و یک باشد.
- تخفیف روی Subtotal محاسبه می‌شود.
- Taxable Amount برابر Subtotal منهای Discount است.
- نرخ مالیات باید بین صفر و یک باشد.
- مالیات روی Taxable Amount محاسبه می‌شود.
- Total برابر Taxable Amount به‌علاوه Tax است.
- مبالغ با روش ROUND_HALF_UP تا دو رقم اعشار گرد می‌شوند.
- SKU خالی در این نسخه توسط calculate_cart اعتبارسنجی نمی‌شود.

وجود جمله آخر مهم است. مدل نباید تستی بسازد که انتظار داشته باشد calculate_cart برای SKU خالی خطا ایجاد کند؛ زیرا این رفتار در قرارداد فعلی وجود ندارد.

نوشتن تست‌های دستی پایه

فایل tests/test_cart.py:

from decimal import Decimal

import pytest

from app.cart import (
    CartItem,
    InvalidCartItemError,
    calculate_cart,
)


def test_empty_cart_returns_zero_amounts():
    result = calculate_cart([])

    assert result.subtotal == Decimal("0.00")
    assert result.discount == Decimal("0.00")
    assert result.taxable_amount == Decimal("0.00")
    assert result.tax == Decimal("0.00")
    assert result.total == Decimal("0.00")


def test_calculates_subtotal_for_multiple_items():
    items = [
        CartItem(
            sku="A",
            unit_price=Decimal("10.50"),
            quantity=2,
        ),
        CartItem(
            sku="B",
            unit_price=Decimal("5.00"),
            quantity=3,
        ),
    ]

    result = calculate_cart(items)

    assert result.subtotal == Decimal("36.00")
    assert result.total == Decimal("36.00")


def test_applies_discount_before_tax():
    items = [
        CartItem(
            sku="A",
            unit_price=Decimal("100.00"),
            quantity=1,
        ),
    ]

    result = calculate_cart(
        items,
        discount_rate=Decimal("0.10"),
        tax_rate=Decimal("0.20"),
    )

    assert result.subtotal == Decimal("100.00")
    assert result.discount == Decimal("10.00")
    assert result.taxable_amount == Decimal("90.00")
    assert result.tax == Decimal("18.00")
    assert result.total == Decimal("108.00")


@pytest.mark.parametrize(
    "discount_rate",
    [
        Decimal("-0.01"),
        Decimal("1.01"),
    ],
)
def test_rejects_invalid_discount_rate(
    discount_rate,
):
    with pytest.raises(ValueError):
        calculate_cart(
            [],
            discount_rate=discount_rate,
        )


@pytest.mark.parametrize(
    "unit_price,quantity",
    [
        (Decimal("-1"), 1),
        (Decimal("10"), -1),
    ],
)
def test_rejects_negative_item_values(
    unit_price,
    quantity,
):
    item = CartItem(
        sku="A",
        unit_price=unit_price,
        quantity=quantity,
    )

    with pytest.raises(
        InvalidCartItemError
    ):
        calculate_cart([item])

اجرای تست‌ها:

pytest -q

ساخت Test Plan با API درواره

فایل .env:

DARVAREH_API_KEY=YOUR_API_KEY
DARVAREH_MODEL=MODEL_ID_DARVAREH

فایل .gitignore:

.env
.venv/
__pycache__/
.pytest_cache/
.coverage
htmlcov/
generated_tests/*.py

برای دریافت API Key در درواره ثبت‌نام کنید. مدل مناسب را از صفحه مدل‌های درواره انتخاب کنید.

تعریف Schema برنامه تست

فایل tools/test_plan_schema.py:

from typing import Literal

from pydantic import BaseModel, Field


class TestCasePlan(BaseModel):
    id: str
    title: str
    test_type: Literal[
        "normal",
        "boundary",
        "invalid",
        "regression",
        "property",
    ]
    preconditions: list[str] = Field(
        default_factory=list
    )
    inputs: dict
    expected_behavior: list[str]
    rationale: str
    source_requirement: str
    priority: Literal[
        "high",
        "medium",
        "low",
    ]


class TestPlan(BaseModel):
    target: str
    framework: str
    assumptions: list[str] = Field(
        default_factory=list
    )
    ambiguities: list[str] = Field(
        default_factory=list
    )
    existing_coverage_gaps: list[str] = Field(
        default_factory=list
    )
    test_cases: list[TestCasePlan]

تولید Test Plan

فایل tools/generate_test_plan.py:

import json
import os
from pathlib import Path

from dotenv import load_dotenv
from openai import OpenAI

from tools.test_plan_schema import TestPlan


load_dotenv()

api_key = os.getenv("DARVAREH_API_KEY")
model = os.getenv("DARVAREH_MODEL")

if not api_key:
    raise RuntimeError(
        "DARVAREH_API_KEY is missing."
    )

if not model:
    raise RuntimeError(
        "DARVAREH_MODEL is missing."
    )

client = OpenAI(
    api_key=api_key,
    base_url="https://api.darvareh.ir/v1",
)

source_code = Path(
    "app/cart.py"
).read_text(encoding="utf-8")

specification = Path(
    "specifications/cart.md"
).read_text(encoding="utf-8")

existing_tests = Path(
    "tests/test_cart.py"
).read_text(encoding="utf-8")

system_prompt = """
تو یک مهندس تست نرم‌افزار هستی.

وظیفه:
بر اساس Specification، Source Code و تست‌های موجود،
یک Test Plan تکمیلی تولید کن.

اولویت منابع:
1. Specification
2. قرارداد عمومی تابع
3. Source Code

قواعد:
- رفتار فعلی کد را به‌عنوان Specification فرض نکن.
- تست‌های تکراری پیشنهاد نده.
- جزئیات داخلی پیاده‌سازی را تست نکن.
- هر Test Case را به یک Requirement متصل کن.
- اگر Specification مبهم است، آن را در ambiguities بنویس.
- رفتار جدیدی اختراع نکن.
- فقط JSON معتبر برگردان.
"""

output_example = {
    "target": "app.cart.calculate_cart",
    "framework": "pytest",
    "assumptions": [],
    "ambiguities": [],
    "existing_coverage_gaps": [
        "string"
    ],
    "test_cases": [
        {
            "id": "TC-001",
            "title": "string",
            "test_type": "boundary",
            "preconditions": [],
            "inputs": {},
            "expected_behavior": [
                "string"
            ],
            "rationale": "string",
            "source_requirement": "string",
            "priority": "high",
        }
    ],
}

prompt = f"""
Specification:
{specification}

Source code:
```python
{source_code}

Existing tests:

{existing_tests}

Required output format:
{json.dumps(
output_example,
ensure_ascii=False,
indent=2,
)}
"""

response = client.chat.completions.create(
model=model,
temperature=0.1,
messages=[
{
"role": "system",
"content": system_prompt,
},
{
"role": "user",
"content": prompt,
},
],
)

raw_output = response.choices[0].message.content

if not raw_output:
raise RuntimeError(
"The model returned an empty response."
)

parsed = json.loads(raw_output)
test_plan = TestPlan.model_validate(parsed)

output_path = Path(
"generated_tests/test_plan.json"
)

output_path.parent.mkdir(
parents=True,
exist_ok=True,
)

output_path.write_text(
test_plan.model_dump_json(indent=2),
encoding="utf-8",
)

print(test_plan.model_dump_json(indent=2))
print(f"\nSaved to {output_path}")


اجرای ابزار:

```bash
python -m tools.generate_test_plan

قبل از تولید کد، فایل generated_tests/test_plan.json را بررسی کنید.

چه Test Caseهایی احتمالاً کم هستند؟

برای نمونه ما، مدل باید مواردی مانند این‌ها را پیشنهاد کند:

  • نرخ تخفیف دقیقاً صفر
  • نرخ تخفیف دقیقاً یک
  • نرخ مالیات دقیقاً صفر
  • نرخ مالیات دقیقاً یک
  • تعداد صفر
  • گردکردن ROUND_HALF_UP
  • چند قلم با قیمت اعشاری
  • تخفیف کامل و مالیات پس از آن
  • Subtotal بزرگ
  • اطمینان از تغییرنکردن فهرست ورودی

همه این موارد الزاماً ارزش یکسانی ندارند. اولویت باید بر اساس ریسک کسب‌وکار تعیین شود.

تولید کد Pytest با AI

بعد از تأیید Test Plan، کد تولید می‌شود.

فایل tools/generate_tests.py:

import json
import os
import re
from pathlib import Path

from dotenv import load_dotenv
from openai import OpenAI


load_dotenv()

api_key = os.getenv("DARVAREH_API_KEY")
model = os.getenv("DARVAREH_MODEL")

if not api_key or not model:
    raise RuntimeError(
        "Darvareh configuration is missing."
    )

client = OpenAI(
    api_key=api_key,
    base_url="https://api.darvareh.ir/v1",
)

source_code = Path(
    "app/cart.py"
).read_text(encoding="utf-8")

specification = Path(
    "specifications/cart.md"
).read_text(encoding="utf-8")

existing_tests = Path(
    "tests/test_cart.py"
).read_text(encoding="utf-8")

test_plan = Path(
    "generated_tests/test_plan.json"
).read_text(encoding="utf-8")

system_prompt = """
تو کد تست Pytest تولید می‌کنی.

قواعد:
- فقط محتوای یک فایل Python را برگردان.
- Markdown و code fence ننویس.
- فقط API عمومی ماژول را تست کن.
- Specification منبع رفتار مورد انتظار است.
- تست‌های موجود را تکرار نکن.
- هر تست باید حداقل یک Assertion معنادار داشته باشد.
- از sleep، شبکه، فایل سیستم و Dependency خارجی استفاده نکن.
- از eval، exec و subprocess استفاده نکن.
- تست‌ها باید قطعی و مستقل باشند.
- نام تست‌ها باید رفتار مورد انتظار را توضیح دهند.
- برای مقادیر پولی از Decimal استفاده کن.
"""

prompt = f"""
Specification:
{specification}

Source:
{source_code}

Existing tests:
{existing_tests}

Approved test plan:
{test_plan}

یک فایل تست تکمیلی Pytest تولید کن.
"""

response = client.chat.completions.create(
    model=model,
    temperature=0,
    messages=[
        {
            "role": "system",
            "content": system_prompt,
        },
        {
            "role": "user",
            "content": prompt,
        },
    ],
)

generated_code = (
    response.choices[0].message.content
)

if not generated_code:
    raise RuntimeError(
        "No test code was generated."
    )

forbidden_patterns = [
    r"\beval\s*\(",
    r"\bexec\s*\(",
    r"\bsubprocess\b",
    r"\bos\.system\b",
    r"\brequests\.",
    r"\bsocket\.",
]

for pattern in forbidden_patterns:
    if re.search(pattern, generated_code):
        raise RuntimeError(
            "Generated code contains a "
            "forbidden operation."
        )

try:
    compile(
        generated_code,
        "test_cart_ai.py",
        "exec",
    )
except SyntaxError as error:
    raise RuntimeError(
        f"Generated test has invalid syntax: "
        f"{error}"
    ) from error

output_path = Path(
    "generated_tests/test_cart_ai.py"
)

output_path.write_text(
    generated_code,
    encoding="utf-8",
)

print(generated_code)
print(f"\nSaved to {output_path}")

این کنترل‌ها کامل نیستند. کد تولیدشده باید پیش از اجرا بازبینی شود و اجرای آن در محیط جداگانه انجام شود.

اجرای تست تولیدشده

ابتدا فایل را بخوانید:

python -m py_compile generated_tests/test_cart_ai.py

سپس فقط همان فایل را اجرا کنید:

pytest -q generated_tests/test_cart_ai.py

بعد تمام تست‌ها:

pytest -q

تست تولیدشده نباید مستقیماً در Branch اصلی پذیرفته شود. آن را مانند Pull Request بازبینی کنید.

اجرای کنترل‌شده تست با پایتون

فایل tools/run_generated_tests.py:

import subprocess
import sys


command = [
    sys.executable,
    "-m",
    "pytest",
    "-q",
    "generated_tests/test_cart_ai.py",
]

try:
    result = subprocess.run(
        command,
        capture_output=True,
        text=True,
        timeout=30,
        check=False,
    )
except subprocess.TimeoutExpired:
    raise RuntimeError(
        "Generated tests exceeded timeout."
    )

print(result.stdout)

if result.stderr:
    print(result.stderr)

raise SystemExit(result.returncode)

اجرای این اسکریپت روی سیستم اصلی Production مناسب نیست. در CI بهتر است تست‌های تولیدشده داخل Container یا Runner محدود اجرا شوند.

نمونه تست‌های تکمیلی باکیفیت

from decimal import Decimal

from app.cart import (
    CartItem,
    calculate_cart,
)


def test_quantity_zero_does_not_change_subtotal():
    items = [
        CartItem(
            sku="A",
            unit_price=Decimal("99.99"),
            quantity=0,
        ),
    ]

    result = calculate_cart(items)

    assert result.subtotal == Decimal("0.00")
    assert result.total == Decimal("0.00")


def test_full_discount_makes_taxable_amount_zero():
    items = [
        CartItem(
            sku="A",
            unit_price=Decimal("100.00"),
            quantity=1,
        ),
    ]

    result = calculate_cart(
        items,
        discount_rate=Decimal("1"),
        tax_rate=Decimal("0.25"),
    )

    assert result.discount == Decimal("100.00")
    assert result.taxable_amount == Decimal("0.00")
    assert result.tax == Decimal("0.00")
    assert result.total == Decimal("0.00")


def test_money_uses_round_half_up():
    items = [
        CartItem(
            sku="A",
            unit_price=Decimal("1.005"),
            quantity=1,
        ),
    ]

    result = calculate_cart(items)

    assert result.subtotal == Decimal("1.01")

این تست‌ها رفتارهای مشخص Specification را بررسی می‌کنند.

افزودن Coverage

اجرای Coverage:

pytest \
  --cov=app \
  --cov-branch \
  --cov-report=term-missing \
  --cov-report=html

گزارش HTML در پوشه زیر ساخته می‌شود:

htmlcov/index.html

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

مثلاً این تست ممکن است Coverage ایجاد کند ولی Assertion نداشته باشد:

def test_calculate_cart():
    calculate_cart([])

برای کیفیت تست باید قدرت Assertion و توانایی کشف خطا نیز بررسی شود.

Branch Coverage چرا مهم است؟

فرض کنید کد چنین باشد:

if discount_rate < 0 or discount_rate > 1:
    raise ValueError(...)

Line Coverage ممکن است نشان دهد این خط اجرا شده، اما Branch Coverage مشخص می‌کند آیا هر دو طرف مرز بررسی شده‌اند:

  • مقدار منفی
  • مقدار بیشتر از یک
  • مقدار معتبر
  • مرز صفر
  • مرز یک

Mutation Testing برای بررسی قدرت تست

Mutation Testing تغییرات کوچکی در کد ایجاد می‌کند:

discount_rate > 1

به:

discount_rate >= 1

اگر تست‌ها همچنان Pass شوند، احتمالاً مرز 1 به‌درستی بررسی نشده است.

ابزارهای متداول Python برای Mutation Testing شامل mutmut و cosmic-ray هستند. فرایند کلی:

  1. تست‌های عادی باید Pass شوند.
  2. Mutationها روی کد ایجاد شوند.
  3. تست‌ها دوباره اجرا شوند.
  4. تست باکیفیت باید Mutation اشتباه را شناسایی کند.
  5. Mutationهای باقی‌مانده بازبینی شوند.

AI می‌تواند برای Mutationهای باقی‌مانده تست پیشنهاد کند، اما نباید صرفاً برای افزایش امتیاز Mutation تست‌های کم‌ارزش بسازد.

تولید Property-based Test

برای بررسی ویژگی‌های عمومی می‌توان از Hypothesis استفاده کرد.

نصب:

pip install hypothesis

نمونه:

from decimal import Decimal

from hypothesis import (
    given,
    strategies as st,
)

from app.cart import (
    CartItem,
    calculate_cart,
)


@given(
    unit_price=st.decimals(
        min_value="0",
        max_value="1000000",
        places=2,
        allow_nan=False,
        allow_infinity=False,
    ),
    quantity=st.integers(
        min_value=0,
        max_value=1000,
    ),
)
def test_total_never_negative(
    unit_price,
    quantity,
):
    item = CartItem(
        sku="A",
        unit_price=Decimal(unit_price),
        quantity=quantity,
    )

    result = calculate_cart([item])

    assert result.subtotal >= 0
    assert result.total >= 0

Hypothesis ورودی‌های متنوع تولید و در صورت شکست تلاش می‌کند یک نمونه کوچک‌تر ارائه دهد. مستندات رسمی Hypothesis

ویژگی Property باید واقعاً از قرارداد استخراج شود. جمله عمومی «تابع نباید خطا دهد» معمولاً Property مناسبی نیست.

تبدیل Bug Report به Regression Test

Bug Report:

وقتی تخفیف ۱۰۰ درصد و مالیات ۹ درصد بود،
سیستم روی مبلغ قبل از تخفیف مالیات محاسبه می‌کرد.

پرامپت مناسب:

Bug Report و Specification زیر را به یک Regression Test تبدیل کن.

قواعد:
- ابتدا رفتار مورد انتظار را از Specification استخراج کن.
- تست باید در نسخه دارای Bug شکست بخورد.
- تست باید پس از اصلاح Bug موفق شود.
- فقط یک رفتار را بررسی کند.
- نام تست مسئله را توضیح دهد.
- از Pytest و Decimal استفاده کن.

Bug:
[BUG REPORT]

Specification:
[SPECIFICATION]

Relevant code:
[SOURCE]

تست:

def test_full_discount_produces_zero_tax():
    item = CartItem(
        sku="A",
        unit_price=Decimal("100"),
        quantity=1,
    )

    result = calculate_cart(
        [item],
        discount_rate=Decimal("1"),
        tax_rate=Decimal("0.09"),
    )

    assert result.taxable_amount == Decimal("0.00")
    assert result.tax == Decimal("0.00")
    assert result.total == Decimal("0.00")

ساخت API نمونه با FastAPI

فایل app/api.py:

from decimal import Decimal

from fastapi import FastAPI
from pydantic import BaseModel, Field

from app.cart import (
    CartItem,
    InvalidCartItemError,
    calculate_cart,
)


app = FastAPI()


class ItemRequest(BaseModel):
    sku: str
    unit_price: Decimal = Field(ge=0)
    quantity: int = Field(ge=0)


class CartRequest(BaseModel):
    items: list[ItemRequest]
    discount_rate: Decimal = Field(
        default=Decimal("0"),
        ge=0,
        le=1,
    )
    tax_rate: Decimal = Field(
        default=Decimal("0"),
        ge=0,
        le=1,
    )


@app.post("/cart/calculate")
def calculate_cart_endpoint(
    request: CartRequest,
):
    items = [
        CartItem(
            sku=item.sku,
            unit_price=item.unit_price,
            quantity=item.quantity,
        )
        for item in request.items
    ]

    try:
        result = calculate_cart(
            items,
            discount_rate=(
                request.discount_rate
            ),
            tax_rate=request.tax_rate,
        )
    except InvalidCartItemError as error:
        return {
            "error": str(error),
        }

    return {
        "subtotal": str(result.subtotal),
        "discount": str(result.discount),
        "taxable_amount": str(
            result.taxable_amount
        ),
        "tax": str(result.tax),
        "total": str(result.total),
    }

تست API با Pytest

FastAPI امکان استفاده از TestClient و Pytest را فراهم می‌کند. راهنمای رسمی تست FastAPI

فایل tests/test_api.py:

from fastapi.testclient import TestClient

from app.api import app


client = TestClient(app)


def test_calculate_cart_endpoint():
    response = client.post(
        "/cart/calculate",
        json={
            "items": [
                {
                    "sku": "A",
                    "unit_price": "100.00",
                    "quantity": 2,
                }
            ],
            "discount_rate": "0.10",
            "tax_rate": "0.20",
        },
    )

    assert response.status_code == 200
    assert response.json() == {
        "subtotal": "200.00",
        "discount": "20.00",
        "taxable_amount": "180.00",
        "tax": "36.00",
        "total": "216.00",
    }


def test_rejects_negative_quantity():
    response = client.post(
        "/cart/calculate",
        json={
            "items": [
                {
                    "sku": "A",
                    "unit_price": "100",
                    "quantity": -1,
                }
            ]
        },
    )

    assert response.status_code == 422


def test_rejects_discount_above_one():
    response = client.post(
        "/cart/calculate",
        json={
            "items": [],
            "discount_rate": "1.01",
        },
    )

    assert response.status_code == 422

پرامپت مناسب برای تولید API Test

برای Endpoint زیر تست Pytest و FastAPI TestClient تولید کن.

Contract:
POST /cart/calculate

ورودی معتبر:
- items: array
- unit_price: عدد غیرمنفی
- quantity: عدد صحیح غیرمنفی
- discount_rate: بین صفر و یک
- tax_rate: بین صفر و یک

خروجی موفق:
- HTTP 200
- تمام مبالغ به‌صورت رشته با دو رقم اعشار

قواعد:
- مسیر موفق را تست کن.
- خطاهای Validation را تست کن.
- مرزهای صفر و یک را بررسی کن.
- قرارداد Response را بررسی کن.
- به جزئیات داخلی calculate_cart وابسته نشو.
- تست شبکه واقعی نساز.
- از TestClient استفاده کن.
- تست‌های تکراری ایجاد نکن.

کد Endpoint:
[SOURCE]

تست رفتار، نه پیاده‌سازی

تست نامناسب:

def test_uses_money_function(monkeypatch):
    ...

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

تست مناسب:

def test_rounds_money_half_up():
    ...
    assert result.total == Decimal("1.01")

تست دوم رفتار عمومی را بررسی می‌کند.

چه چیزهایی را Mock کنیم؟

Mock زمانی مفید است که Dependency:

  • کند باشد.
  • نتیجه غیرقطعی داشته باشد.
  • به شبکه متصل شود.
  • هزینه ایجاد کند.
  • خارج از محدوده Unit Test باشد.
  • کنترل سناریوهای خطای آن دشوار باشد.

مواردی که نباید بی‌دلیل Mock شوند:

  • تابع ساده و قطعی
  • Value Object
  • ساختار داده
  • منطق اصلی مورد آزمون
  • بخشی که قرار است Integration آن بررسی شود

قانون مفید:

مرزهای سیستم را Mock کنید، نه منطق اصلی رفتار را.

استفاده از AI برای ساخت Fixture

پرامپت:

برای تست‌های زیر Fixtureهای Pytest طراحی کن.

قواعد:
- Fixtureها کوچک و قابل ترکیب باشند.
- Fixture بسیار عمومی نساز.
- Mutable State بین تست‌ها به اشتراک گذاشته نشود.
- Scope فقط در صورت نیاز از function بزرگ‌تر باشد.
- داده‌های تست هدف هر Test را پنهان نکنند.

تست‌ها:
[TEST CODE]

نمونه:

import pytest
from decimal import Decimal

from app.cart import CartItem


@pytest.fixture
def basic_item():
    return CartItem(
        sku="A",
        unit_price=Decimal("100.00"),
        quantity=1,
    )

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

تحلیل تست شکست‌خورده با AI

ورودی مفید:

  • Test Code
  • Source Code مرتبط
  • Specification
  • Traceback
  • نسخه Dependencyها
  • خروجی واقعی و مورد انتظار
  • تغییر اخیر مرتبط

پرامپت:

شکست تست زیر را تحلیل کن.

ابتدا موارد زیر را جدا کن:
1. واقعیت‌های قابل اثبات از Traceback
2. تفاوت Expected و Actual
3. فرضیه‌های علت
4. شواهد موافق و مخالف هر فرضیه
5. حداقل بررسی بعدی
6. اینکه احتمالاً مشکل در Production Code است یا Test

قواعد:
- فقط برای Pass شدن تست، Assertion را تغییر نده.
- Specification را منبع رفتار صحیح بدان.
- بدون شواهد، تغییر کد پیشنهاد نده.

Specification:
[SPEC]

Test:
[TEST]

Source:
[SOURCE]

Failure:
[TRACEBACK]

AI و تست‌های Snapshot

مدل می‌تواند تغییر Snapshot را توضیح دهد، اما نباید خودکار تمام Snapshotها را Update کند.

قبل از تأیید Snapshot جدید بررسی کنید:

  • آیا تغییر مورد انتظار بوده است؟
  • آیا فیلد جدیدی حذف شده؟
  • آیا ترتیب عناصر تغییر کرده؟
  • آیا عدد یا تاریخ غیرقطعی وارد Snapshot شده؟
  • آیا تغییر Contract محسوب می‌شود؟
  • آیا Snapshot بیش از حد بزرگ است؟

تولید تست از Diff کد

برای Pull Request می‌توان Diff، تست‌های موجود و Specification مرتبط را به مدل داد.

خروجی مورد انتظار:

{
  "changed_behaviors": [],
  "affected_test_files": [],
  "missing_test_scenarios": [],
  "regression_risks": [],
  "suggested_tests": []
}

پرامپت:

Diff زیر را از دید تست بررسی کن.

قواعد:
- فقط تغییر رفتار عمومی را مبنای تست قرار بده.
- Refactor بدون تغییر رفتار را از Feature Change جدا کن.
- تست‌های موجود را در نظر بگیر.
- Test Case تکراری پیشنهاد نده.
- هر تست پیشنهادی را به خط یا رفتار تغییرکرده متصل کن.
- اگر اطلاعات کافی نیست، آن را اعلام کن.

معماری AI Test Generator در تیم

Specification + Source + Existing Tests + Diff
                    ↓
             Context Builder
                    ↓
             Darvareh API
                    ↓
        Structured Test Plan JSON
                    ↓
             Human Approval
                    ↓
            Test Code Generation
                    ↓
       Static Checks + Isolated Pytest
                    ↓
       Coverage + Mutation Evaluation
                    ↓
              Code Review

نباید مرحله Human Approval بین Test Plan و Test Code حذف شود، مگر برای پروژه‌ای که کیفیت و محدودیت‌های آن به‌طور کافی ارزیابی شده است.

اعتبارسنجی کد تست تولیدشده

حداقل کنترل‌ها:

  • Syntax معتبر
  • فقط Importهای مجاز
  • نبود شبکه واقعی
  • نبود eval و exec
  • نبود اجرای Shell
  • نبود نوشتن خارج از پوشه موقت
  • وجود حداقل یک Assertion در هر تست
  • Timeout اجرا
  • اجرای Container یا Runner محدود
  • بررسی Formatter و Linter
  • اجرای کل Test Suite پس از تست جدید

می‌توانید AST پایتون را نیز بررسی کنید:

import ast


def validate_test_ast(
    source_code: str,
) -> None:
    tree = ast.parse(source_code)

    forbidden_imports = {
        "subprocess",
        "socket",
        "requests",
    }

    for node in ast.walk(tree):
        if isinstance(node, ast.Import):
            for alias in node.names:
                root_name = (
                    alias.name.split(".")[0]
                )

                if root_name in forbidden_imports:
                    raise ValueError(
                        f"Forbidden import: "
                        f"{root_name}"
                    )

        if isinstance(
            node,
            ast.ImportFrom,
        ):
            module = node.module or ""
            root_name = module.split(".")[0]

            if root_name in forbidden_imports:
                raise ValueError(
                    f"Forbidden import: "
                    f"{root_name}"
                )

AST Validation نیز کامل نیست، اما یک لایه کنترل مفید ایجاد می‌کند.

ارزیابی Test Generator

برای سنجش ابزار، یک مجموعه وظیفه مرجع بسازید:

  • تابع ساده
  • Validation مرزی
  • محاسبات پولی
  • تابع دارای Dependency
  • API Endpoint
  • Bug Report واقعی
  • کد دارای Branch پنهان
  • Legacy Code بدون Specification کامل
  • Refactor بدون تغییر رفتار
  • تغییر Contract

معیارها:

معیارتوضیح
نرخ اجرای موفقچند تست بدون خطای Syntax اجرا می‌شوند
نرخ تست پذیرفته‌شدهچند تست پس از Review پذیرفته می‌شوند
Duplicate Rateچند تست رفتار موجود را تکرار می‌کنند
Assertion Qualityتست واقعاً نتیجه مهم را بررسی می‌کند
Mutation Scoreچند تغییر اشتباه توسط تست کشف می‌شود
Specification Alignmentتست با قرارداد سازگار است
False Failure Rateچند تست به‌دلیل فرض اشتباه شکست می‌خورند
Review Timeاصلاح تست تولیدشده چقدر زمان می‌برد
Coverage Improvementپوشش معنادار چه مقدار بیشتر شده است
Cost per Accepted Testهزینه تقسیم بر تعداد تست‌های پذیرفته‌شده

انتخاب مدل مناسب

برای تولید تست، مدل باید در این زمینه‌ها مناسب باشد:

  • درک زبان برنامه‌نویسی
  • پیروی از Specification
  • شناخت Framework تست
  • تولید کد معتبر
  • درک Dependencyها
  • خروجی ساختاریافته
  • تحلیل Edge Case
  • Context کافی برای فایل‌های مرتبط

برای تولید Test Plan ساده می‌توان از مدل سریع‌تر استفاده کرد. برای تحلیل Diff پیچیده، Legacy Code یا چند فایل مرتبط، مدل قوی‌تر ممکن است نتیجه بهتری ارائه دهد.

قیمت و فهرست مدل‌ها ممکن است تغییر کند؛ بنابراین صفحه مدل‌های درواره را بررسی کنید.

اشتباهات رایج

تولید تست فقط از روی Source Code

Specification را نیز ارائه کنید تا اشتباه فعلی کد به رفتار مورد انتظار تبدیل نشود.

پذیرش تست فقط به دلیل Pass شدن

تستی که همیشه Pass می‌شود ارزشی ندارد.

تمرکز صرف بر Coverage

Coverage بالا می‌تواند با Assertionهای ضعیف ایجاد شود.

تولید ده‌ها تست تکراری

ابتدا تست‌های موجود را به مدل بدهید و Test Plan را بررسی کنید.

Mock کردن همه چیز

Mock بیش از حد باعث می‌شود Integration واقعی اجزا آزمایش نشود.

تغییر Assertion برای Pass شدن

ابتدا مشخص کنید Source Code اشتباه است یا Test.

اجرای کد تولیدشده بدون کنترل

کد تست نیز کد اجرایی است و باید در محیط محدود اجرا شود.

استفاده مستقیم از تست AI در Branch اصلی

تست باید Review، اجرا و با Specification تطبیق داده شود.

نداشتن تست برای خود Test Generator

Prompt، Model و Schema ابزار نیز باید با Dataset مرجع ارزیابی شوند.

برنامه پیاده‌سازی در تیم

مرحله اول: Test Plan

AI فقط سناریو پیشنهاد دهد و مهندس تست آن‌ها را تأیید کند.

مرحله دوم: تولید تست واحد

فقط برای توابع قطعی و بدون Dependency خارجی تست تولید شود.

مرحله سوم: بررسی خودکار

  • Syntax
  • AST
  • Pytest
  • Coverage
  • Linter

مرحله چهارم: Regression Test

Bugهای حل‌شده به Test Case تبدیل شوند.

مرحله پنجم: تحلیل Pull Request

Diff بررسی و تست‌های کمبود پیشنهاد شوند.

مرحله ششم: ارزیابی مستمر

نرخ پذیرش، Mutation Score و خطاهای مدل ثبت شوند.

چک‌لیست تست تولیدشده با AI

  • تست به Requirement مشخص متصل است.
  • رفتار عمومی را بررسی می‌کند.
  • Assertion معنادار دارد.
  • تست موجود را تکرار نمی‌کند.
  • مستقل و تکرارپذیر است.
  • به زمان یا شبکه واقعی وابسته نیست.
  • نام Test رفتار را توضیح می‌دهد.
  • Edge Case واقعی را پوشش می‌دهد.
  • فقط برای Pass شدن نوشته نشده است.
  • در نسخه دارای Bug شکست می‌خورد.
  • پس از اصلاح Bug موفق می‌شود.
  • به جزئیات داخلی غیرضروری وابسته نیست.
  • در CI اجرا می‌شود.
  • در Code Review بررسی شده است.

پرسش‌های متداول

آیا هوش مصنوعی می‌تواند Unit Test بنویسد؟

بله. AI می‌تواند برای Frameworkهایی مانند Pytest، Jest، JUnit و NUnit تست تولید کند. کیفیت نتیجه به Specification، Context و بازبینی انسانی وابسته است.

آیا تست تولیدشده توسط AI قابل اعتماد است؟

بدون بازبینی خیر. تست باید اجرا، با رفتار مورد انتظار تطبیق و از نظر قدرت Assertion بررسی شود.

بهترین پرامپت برای تست‌نویسی چیست؟

پرامپتی که Source Code، Specification، تست‌های موجود، Framework، قواعد Mock و قالب خروجی را مشخص کند. بهتر است ابتدا Test Plan درخواست شود.

آیا AI می‌تواند API Test بنویسد؟

بله. با ارائه OpenAPI Schema، قرارداد Endpoint و نمونه تست‌های پروژه، مدل می‌تواند سناریوهای موفق، Validation، خطا و Boundary را تولید کند.

چگونه بفهمیم تست AI واقعاً مفید است؟

بررسی کنید آیا تست در صورت تغییر اشتباه کد شکست می‌خورد. Mutation Testing یکی از روش‌های مناسب برای سنجش این موضوع است.

آیا Coverage بالا یعنی تست‌ها خوب‌اند؟

خیر. Coverage فقط اجرای خط‌ها و Branchها را نشان می‌دهد و کیفیت Assertion را تضمین نمی‌کند.

آیا می‌توان تولید تست را در CI خودکار کرد؟

بله، اما بهتر است AI ابتدا Test Plan یا پیشنهاد تغییر تولید کند. کد تولیدشده باید در Runner محدود اجرا و پیش از ادغام بازبینی شود.

API درواره چه نقشی در این سیستم دارد؟

API درواره امکان دسترسی به مدل‌های مختلف را از طریق رابط سازگار فراهم می‌کند. ابزار Test Generator می‌تواند Source، Specification و تست‌های موجود را به مدل ارسال و Test Plan یا کد تست دریافت کند.

کدام مدل برای تولید تست بهتر است؟

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

جمع‌بندی

هوش مصنوعی می‌تواند تست‌نویسی را سریع‌تر کند، اما نباید هدف را صرفاً افزایش تعداد تست یا Coverage قرار داد. تست خوب باید رفتار مورد انتظار را بررسی و خطای واقعی را کشف کند.

مهم‌ترین اصل این است که مدل فقط Source Code را نبیند. Specification، قواعد کسب‌وکار، قرارداد عمومی و تست‌های موجود باید در Context قرار گیرند. سپس Test Plan تولیدشده پیش از تبدیل به کد بازبینی شود.

در پروژه این مقاله یک جریان عملی ساختیم که با API درواره Test Plan ساختاریافته تولید می‌کند، آن را به Pytest تبدیل می‌کند و تست‌ها را پس از کنترل Syntax در محیط محدود اجرا می‌کند. همچنین با Coverage، Property-based Testing و Mutation Testing روش‌های ارزیابی قدرت تست را بررسی کردیم.

برای شروع، در درواره ثبت‌نام و API Key دریافت کنید. سپس مدل مناسب پروژه را از صفحه مدل‌های درواره انتخاب کرده و ابزار را ابتدا روی یک تابع کوچک و دارای Specification روشن آزمایش کنید.

مقالات مرتبط

برای مطالعه شرایط استفاده و محدودیت‌های مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.

Read more

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

اتوماسیون هوش مصنوعی چیست؟ کاربردها و آموزش ساخت AI Automation

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

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce چیست؟ آینده خرید با ایجنت هوش مصنوعی

Agentic Commerce شیوه‌ای جدید برای خرید اینترنتی است که در آن ایجنت هوش مصنوعی می‌تواند نیاز کاربر را بفهمد، محصولات را جست‌وجو و مقایسه کند و فرایند خرید را پیش ببرد. در این راهنما با معماری، UCP، ACP و پیاده‌سازی آن با API درواره آشنا می‌شوید.