تستنویسی با هوش مصنوعی؛ آموزش تولید Unit Test و API Test با AI
در این آموزش یاد میگیرید با AI سناریوهای تست، Unit Test و API Test تولید کنید و یک ابزار عملی بسازید که کد را تحلیل، تست Pytest ایجاد و نتیجه اجرای تستها را گزارش میکند.
تستنویسی با هوش مصنوعی؛ از طراحی سناریو تا تولید تست قابل اجرا
هوش مصنوعی میتواند در چند ثانیه برای یک تابع دهها تست تولید کند؛ اما تعداد زیاد تست الزاماً به معنای کیفیت بالاتر نرمافزار نیست.
یک تست مفید باید:
- رفتار مورد انتظار را بررسی کند.
- در صورت وجود خطای واقعی شکست بخورد.
- به جزئیات غیرضروری پیادهسازی وابسته نباشد.
- نتیجه قطعی و تکرارپذیر داشته باشد.
- دلیل شکست آن قابل فهم باشد.
- سناریوهای مرزی را پوشش دهد.
- فقط رفتار فعلی کد را کپی نکند.
اگر کدی اشتباه باشد و مدل از روی همان کد تست تولید کند، ممکن است اشتباه موجود را بهعنوان رفتار صحیح در تست ثبت کند. بنابراین ورودی مناسب برای تولید تست فقط Source Code نیست؛ مدل باید Specification، قرارداد API، قواعد کسبوکار و رفتار مورد انتظار را نیز ببیند.
در این مقاله یک پروژه واقعی میسازیم:
- یک سرویس محاسبه سبد خرید ایجاد میکنیم.
- Specification آن را مینویسیم.
- بهصورت دستی سناریوهای مهم را مشخص میکنیم.
- با API درواره Test Plan ساختاریافته تولید میکنیم.
- تستهای Pytest ایجاد میکنیم.
- تستها را در یک فرایند محدود اجرا میکنیم.
- Coverage و کیفیت Assertionها را بررسی میکنیم.
- همین روش را برای 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 | قیمت منفی | قیمت منفی | خطای Validation | Invalid |
| T-05 | تخفیف در مرز | جمع دقیقاً برابر حداقل | رفتار مطابق Specification | Boundary |
| 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 هستند. فرایند کلی:
- تستهای عادی باید Pass شوند.
- Mutationها روی کد ایجاد شوند.
- تستها دوباره اجرا شوند.
- تست باکیفیت باید Mutation اشتباه را شناسایی کند.
- 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 روشن آزمایش کنید.
مقالات مرتبط
- راهنمای بهترین مدل هوش مصنوعی برای برنامهنویسی
- بهترین ابزارهای برنامهنویسی با هوش مصنوعی؛ بخش اول
- بهترین ابزارهای برنامهنویسی با هوش مصنوعی؛ بخش دوم
- ساخت دستیار برنامهنویسی اختصاصی برای شرکت
- آموزش Structured Outputs و JSON Schema
- آموزش ارزیابی مدلهای هوش مصنوعی و Evals
- چگونه API هوش مصنوعی را به نرمافزار خود اضافه کنیم؟
- راهنمای ساخت API هوش مصنوعی آماده محیط Production
برای مطالعه شرایط استفاده و محدودیتهای مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.