تولید داده تست با هوش مصنوعی؛ ساخت Synthetic Data Generator و Mock API
در این آموزش یک AI Test Data Generator واقعی میسازید که نیاز متنی را به سناریوی ساختاریافته تبدیل میکند، داده فارسی معتبر میسازد، روابط را حفظ میکند و Mock API قابل اجرا ارائه میدهد.
تولید داده تست با هوش مصنوعی؛ از Mock Data تا Mock API قابل اجرا
توسعه و تست نرمافزار بدون داده مناسب دشوار است. برای آزمایش یک فروشگاه اینترنتی فقط داشتن چند رکورد تصادفی کافی نیست. داده باید سناریوهای واقعی و مرزی را پوشش دهد:
- مشتری فعال و غیرفعال
- سفارش تکمیلشده، لغوشده و در انتظار
- سبد خرید خالی
- سفارش دارای تخفیف
- محصول ناموجود
- مقدار صفر
- مبلغ نزدیک به حد مجاز
- داده تکراری کنترلشده
- فیلد اختیاری خالی
- رابطه معتبر میان مشتری، سفارش و محصول
- تاریخهای مختلف
- متن فارسی
- خطاهای مورد انتظار API
ابزارهای سنتی مانند Faker میتوانند نام، آدرس، تاریخ و شماره تصادفی تولید کنند؛ اما نمیدانند برای آزمایش «گزارش مشتریان تکرارشونده» چه توزیع دادهای لازم است.
مدل هوش مصنوعی میتواند نیاز کسبوکار را درک و یک برنامه تولید داده بسازد:
برای آزمایش گزارش مشتریان تکرارشونده، حداقل ۳۰ درصد مشتریان باید
بیش از یک سفارش تکمیلشده داشته باشند و چند مشتری نیز فقط سفارش
لغوشده داشته باشند.
بااینحال، تولید هزاران ردیف مستقیماً توسط مدل زبانی انتخاب مناسبی نیست:
- هزینه افزایش مییابد.
- ساختار ممکن است ناپایدار باشد.
- شناسههای تکراری ایجاد میشوند.
- رابطه Foreign Key ممکن است بشکند.
- توزیع عددی دقیق رعایت نمیشود.
- بازتولید همان Dataset دشوار است.
- محاسبات و Constraintها ممکن است نادرست باشند.
معماری بهتر:
- کاربر هدف تست را توضیح میدهد.
- مدل یک Data Generation Plan ساختاریافته تولید میکند.
- Backend برنامه را اعتبارسنجی میکند.
- Python و Faker دادهها را با Seed مشخص میسازند.
- Pydantic و قواعد دامنه خروجی را بررسی میکنند.
- تست کیفیت Dataset اجرا میشود.
- داده به JSON یا CSV صادر میشود.
- یک Mock API آن را در اختیار Frontend و تستها قرار میدهد.
داده تست چیست؟
Test Data مجموعهای از ورودیها و وضعیتها برای بررسی رفتار نرمافزار است.
انواع اصلی:
داده مسیر عادی
سناریوهای رایج و معتبر:
{
"name": "کیبورد مکانیکی",
"price": 3500000,
"stock": 12,
"is_active": true
}
داده مرزی
مقادیر نزدیک حداقل یا حداکثر:
{
"quantity": 0
}
{
"discount_rate": 1
}
داده نامعتبر
برای بررسی Validation:
{
"price": -1000
}
داده رابطهای
چند موجودیت مرتبط:
Customer → Order → Order Item → Product
داده حجمی
برای آزمایش Pagination، گزارش و عملکرد:
100,000 سفارش
داده Regression
سناریویی که یک Bug قبلی را بازتولید میکند.
تفاوت Fake Data و Synthetic Data
Fake Data
داده ساختگی ساده مانند نام، ایمیل و آدرس تصادفی.
Synthetic Data
داده مصنوعی با ساختار، توزیع و رابطه مشخص که برای شبیهسازی سناریو طراحی شده است.
مثال Fake Data:
{
"customer": "کاربر نمونه",
"order_amount": 1250000
}
مثال Synthetic Dataset:
- ۱۰۰۰ مشتری
- ۲۰ درصد مشتری غیرفعال
- ۳۵ درصد مشتری دارای خرید تکراری
- ۷۰ درصد سفارش تکمیلشده
- ۱۵ درصد لغوشده
- ۱۵ درصد در انتظار
- مبلغ سفارش بر اساس اقلام محاسبه شود
- هر Order Item به Product موجود متصل باشد
هوش مصنوعی در تولید داده تست چه نقشی دارد؟
تبدیل Requirement به Data Plan
ورودی:
برای تست داشبورد فروش، داده سه ماه با سفارشهای موفق، لغوشده و
در انتظار میخواهم. هر شهر حداقل یک سفارش موفق داشته باشد.
خروجی:
{
"entities": {
"customers": 100,
"products": 50,
"orders": 1000
},
"distributions": {
"order_status": {
"completed": 0.70,
"cancelled": 0.15,
"pending": 0.15
}
},
"constraints": [
"هر شهر حداقل یک سفارش completed داشته باشد"
],
"edge_cases": [
"سفارش با مقدار صفر",
"مشتری بدون سفارش",
"محصول غیرفعال"
]
}
پیشنهاد Edge Case
مدل میتواند حالتهای فراموششده را پیشنهاد دهد.
تولید متنهای واقعگرایانه
- عنوان محصول
- توضیح تیکت
- متن بازخورد
- نام کمپین
- توضیح سفارش
تولید Test Fixture کوچک
برای تعداد محدود رکورد، مدل میتواند JSON نمونه تولید کند؛ خروجی همچنان باید با Schema بررسی شود.
ساخت Mock Response
بر اساس OpenAPI یا JSON Schema، نمونه Response موفق و خطا تولید میشود.
طراحی سناریو
مثلاً:
مشتری سفارش ایجاد میکند، پرداخت ناموفق میشود و در تلاش دوم
سفارش تکمیل میشود.
چرا داده را مستقیماً از مدل نگیریم؟
فرض کنید ۱۰ هزار سفارش میخواهیم. ارسال درخواست تولید تمام ردیفها به مدل ممکن است باعث این مشکلات شود:
- شماره سفارش تکراری
- جمع مبلغ اشتباه
- تاریخ خارج از بازه
- وضعیت خارج از Enum
- محصول موجودنبوده
- مشتری نامعتبر
- JSON ناقص
- Dataset غیرقابل بازتولید
مدل برای طراحی Plan مناسب است. تولید ردیفها بهتر است با کد قطعی انجام شود.
معماری AI Test Data Generator
Requirement
↓
Darvareh API
↓
Structured Data Plan
↓
Schema and Rule Validation
↓
Faker + Deterministic Generator
↓
Dataset Quality Tests
↓
JSON / CSV / Mock API
پروژه عملی این آموزش
یک فروشگاه نمونه با این موجودیتها میسازیم:
- Customer
- Product
- Order
- Order Item
ابزار ما:
- نیاز فارسی دریافت میکند.
- تعداد و توزیع موجودیتها را پیشنهاد میدهد.
- Plan را با Pydantic بررسی میکند.
- با Seed ثابت داده فارسی میسازد.
- روابط را حفظ میکند.
- مبلغ سفارش را از اقلام محاسبه میکند.
- کیفیت Dataset را ارزیابی میکند.
- داده را در فایل JSON ذخیره میکند.
- APIهای Mock برای فهرست و جزئیات ارائه میدهد.
ایجاد پروژه
mkdir ai-test-data-generator
cd ai-test-data-generator
python -m venv .venv
فعالسازی در Linux و macOS:
source .venv/bin/activate
فعالسازی در Windows:
.venv\Scripts\Activate.ps1
نصب وابستگیها:
pip install \
openai \
python-dotenv \
pydantic \
faker \
fastapi \
uvicorn
ساخت پوشهها:
mkdir generator datasets output
فایلهای زیر را ایجاد کنید:
generator/__init__.py
generator/schemas.py
generator/planner.py
generator/validator.py
generator/data_generator.py
generator/quality.py
generator/exporter.py
build_dataset.py
mock_api.py
تنظیم API درواره
فایل .env:
DARVAREH_API_KEY=YOUR_API_KEY
DARVAREH_MODEL=MODEL_ID_DARVAREH
DATASET_SEED=1405
فایل .gitignore:
.env
.venv/
__pycache__/
datasets/
output/
برای دریافت API Key در درواره ثبتنام کنید. Model ID مناسب را از صفحه مدلهای درواره انتخاب کنید.
تعریف مدل داده فروشگاه
فایل generator/schemas.py:
from datetime import datetime
from typing import Literal
from pydantic import BaseModel, Field
OrderStatus = Literal[
"completed",
"pending",
"cancelled",
]
CustomerStatus = Literal[
"active",
"inactive",
]
ProductCategory = Literal[
"laptop",
"monitor",
"accessory",
"mobile",
]
class Distribution(BaseModel):
completed: float = Field(
ge=0,
le=1,
)
pending: float = Field(
ge=0,
le=1,
)
cancelled: float = Field(
ge=0,
le=1,
)
class CustomerDistribution(BaseModel):
active: float = Field(
ge=0,
le=1,
)
inactive: float = Field(
ge=0,
le=1,
)
class EdgeCasePlan(BaseModel):
name: str
description: str
count: int = Field(
ge=1,
le=100,
)
expected_use: str
class DataGenerationPlan(BaseModel):
title: str
purpose: str
seed: int = Field(
ge=0,
)
customer_count: int = Field(
ge=1,
le=100_000,
)
product_count: int = Field(
ge=1,
le=100_000,
)
order_count: int = Field(
ge=1,
le=1_000_000,
)
minimum_items_per_order: int = Field(
ge=1,
le=100,
)
maximum_items_per_order: int = Field(
ge=1,
le=100,
)
start_date: datetime
end_date: datetime
cities: list[str] = Field(
min_length=1,
max_length=100,
)
order_status_distribution: Distribution
customer_status_distribution: (
CustomerDistribution
)
repeat_customer_ratio: float = Field(
ge=0,
le=1,
)
inactive_product_ratio: float = Field(
ge=0,
le=1,
)
minimum_unit_price: int = Field(
ge=0,
)
maximum_unit_price: int = Field(
ge=0,
)
edge_cases: list[EdgeCasePlan] = Field(
default_factory=list
)
assumptions: list[str] = Field(
default_factory=list
)
class Customer(BaseModel):
id: str
full_name: str
city: str
status: CustomerStatus
created_at: datetime
class Product(BaseModel):
id: str
name: str
category: ProductCategory
unit_price: int = Field(
ge=0,
)
stock: int = Field(
ge=0,
)
is_active: bool
class OrderItem(BaseModel):
product_id: str
product_name: str
quantity: int = Field(
ge=1,
)
unit_price: int = Field(
ge=0,
)
line_total: int = Field(
ge=0,
)
class Order(BaseModel):
id: str
customer_id: str
status: OrderStatus
created_at: datetime
items: list[OrderItem] = Field(
min_length=1
)
total_amount: int = Field(
ge=0,
)
class GeneratedDataset(BaseModel):
metadata: dict
customers: list[Customer]
products: list[Product]
orders: list[Order]
تولید Plan با API درواره
فایل generator/planner.py:
import json
import os
from dotenv import load_dotenv
from openai import OpenAI
from pydantic import ValidationError
from generator.schemas import (
DataGenerationPlan,
)
load_dotenv()
api_key = os.getenv("DARVAREH_API_KEY")
model = os.getenv("DARVAREH_MODEL")
default_seed = int(
os.getenv(
"DATASET_SEED",
"1405",
)
)
if not api_key:
raise RuntimeError(
"DARVAREH_API_KEY is not configured."
)
if not model:
raise RuntimeError(
"DARVAREH_MODEL is not configured."
)
client = OpenAI(
api_key=api_key,
base_url="https://api.darvareh.ir/v1",
)
SYSTEM_PROMPT = """
تو یک مهندس تست و طراح داده مصنوعی هستی.
وظیفه:
Requirement فارسی را به یک Data Generation Plan تبدیل کن.
قواعد:
- خود Dataset را تولید نکن؛ فقط Plan بساز.
- مجموع توزیع وضعیت سفارش دقیقاً 1 باشد.
- مجموع توزیع وضعیت مشتری دقیقاً 1 باشد.
- تاریخ شروع پیش از تاریخ پایان باشد.
- حداقل تعداد اقلام از حداکثر بیشتر نباشد.
- حداقل قیمت از حداکثر بیشتر نباشد.
- حجم Dataset را متناسب با هدف پیشنهاد بده.
- Edge Caseها را صریح ثبت کن.
- رابطه Customer، Order و Product را در نظر بگیر.
- مبلغ سفارش در Backend محاسبه خواهد شد.
- فیلد یا موجودیت جدید اختراع نکن.
- خروجی فقط JSON معتبر باشد.
"""
OUTPUT_TEMPLATE = {
"title": "فروشگاه آزمایشی",
"purpose": "تست داشبورد فروش",
"seed": 1405,
"customer_count": 100,
"product_count": 50,
"order_count": 1000,
"minimum_items_per_order": 1,
"maximum_items_per_order": 5,
"start_date": (
"2026-05-01T00:00:00"
),
"end_date": (
"2026-07-31T23:59:59"
),
"cities": [
"تهران",
"شیراز",
"اصفهان",
],
"order_status_distribution": {
"completed": 0.70,
"pending": 0.15,
"cancelled": 0.15,
},
"customer_status_distribution": {
"active": 0.80,
"inactive": 0.20,
},
"repeat_customer_ratio": 0.35,
"inactive_product_ratio": 0.10,
"minimum_unit_price": 100000,
"maximum_unit_price": 100000000,
"edge_cases": [
{
"name": "customer_without_order",
"description": (
"مشتری بدون سفارش"
),
"count": 5,
"expected_use": (
"تست گزارش مشتریان "
"بدون خرید"
),
}
],
"assumptions": [
"واحد مبلغ ریال است"
],
}
def generate_plan(
requirement: str,
) -> DataGenerationPlan:
response = client.chat.completions.create(
model=model,
temperature=0.1,
messages=[
{
"role": "system",
"content": SYSTEM_PROMPT,
},
{
"role": "user",
"content": (
"Requirement:\n"
+ requirement
+ "\n\nSeed پیشفرض:\n"
+ str(default_seed)
+ "\n\nقالب خروجی:\n"
+ json.dumps(
OUTPUT_TEMPLATE,
ensure_ascii=False,
indent=2,
)
),
},
],
)
raw_output = (
response.choices[0]
.message.content
)
if not raw_output:
raise RuntimeError(
"The model returned an "
"empty plan."
)
try:
parsed = json.loads(raw_output)
except json.JSONDecodeError as error:
raise RuntimeError(
f"Invalid JSON from model: "
f"{error}"
) from error
try:
return DataGenerationPlan.model_validate(
parsed
)
except ValidationError as error:
raise RuntimeError(
f"Plan validation failed: "
f"{error}"
) from error
اعتبارسنجی قواعد Plan
Pydantic نوع و محدوده هر فیلد را بررسی میکند، اما قواعد بین چند فیلد را باید جداگانه کنترل کنیم.
فایل generator/validator.py:
from decimal import Decimal
from generator.schemas import (
DataGenerationPlan,
)
class PlanValidationError(
ValueError
):
pass
TOLERANCE = Decimal("0.000001")
def is_one(
values: list[float],
) -> bool:
total = sum(
Decimal(str(value))
for value in values
)
return abs(
total - Decimal("1")
) <= TOLERANCE
def validate_plan(
plan: DataGenerationPlan,
) -> None:
if plan.start_date >= plan.end_date:
raise PlanValidationError(
"start_date must be before "
"end_date."
)
if (
plan.minimum_items_per_order
> plan.maximum_items_per_order
):
raise PlanValidationError(
"Minimum items cannot exceed "
"maximum items."
)
if (
plan.minimum_unit_price
> plan.maximum_unit_price
):
raise PlanValidationError(
"Minimum price cannot exceed "
"maximum price."
)
order_distribution = (
plan.order_status_distribution
)
if not is_one(
[
order_distribution.completed,
order_distribution.pending,
order_distribution.cancelled,
]
):
raise PlanValidationError(
"Order status distribution "
"must sum to 1."
)
customer_distribution = (
plan.customer_status_distribution
)
if not is_one(
[
customer_distribution.active,
customer_distribution.inactive,
]
):
raise PlanValidationError(
"Customer status distribution "
"must sum to 1."
)
if (
plan.order_count > 0
and plan.product_count == 0
):
raise PlanValidationError(
"Orders require products."
)
تولید تصادفی قابل بازتولید
Seed باعث میشود اجرای یک Plan مشخص، Dataset یکسانی تولید کند:
random.Random(plan.seed)
Faker نیز باید Seed دریافت کند:
Faker.seed(plan.seed)
بدون Seed، عیبیابی Test شکستخورده دشوار میشود.
تولید نام محصول فارسی
فایل generator/catalog.py:
PRODUCT_NAMES = {
"laptop": [
"لپتاپ مدل آریا",
"لپتاپ مدل دنا",
"لپتاپ مدل پارس",
"لپتاپ مدل سپهر",
],
"monitor": [
"مانیتور ۲۴ اینچ",
"مانیتور ۲۷ اینچ",
"مانیتور حرفهای",
"مانیتور اداری",
],
"accessory": [
"ماوس بیسیم",
"کیبورد مکانیکی",
"هاب USB-C",
"پایه لپتاپ",
"هدفون اداری",
],
"mobile": [
"گوشی مدل آلفا",
"گوشی مدل نوا",
"گوشی مدل پلاس",
"گوشی مدل پرو",
],
}
برای فیلدهای محدود و ساختاری بهتر است از فهرست کنترلشده استفاده شود، نه تولید آزاد مدل.
تولید Dataset
فایل generator/data_generator.py:
import random
from datetime import (
datetime,
timedelta,
)
from faker import Faker
from generator.catalog import (
PRODUCT_NAMES,
)
from generator.schemas import (
Customer,
DataGenerationPlan,
GeneratedDataset,
Order,
OrderItem,
Product,
)
CATEGORIES = [
"laptop",
"monitor",
"accessory",
"mobile",
]
def weighted_choice(
randomizer: random.Random,
values: list[str],
weights: list[float],
) -> str:
return randomizer.choices(
values,
weights=weights,
k=1,
)[0]
def random_datetime(
randomizer: random.Random,
start: datetime,
end: datetime,
) -> datetime:
total_seconds = int(
(end - start).total_seconds()
)
offset = randomizer.randint(
0,
total_seconds,
)
return start + timedelta(
seconds=offset
)
def generate_customers(
plan: DataGenerationPlan,
randomizer: random.Random,
fake: Faker,
) -> list[Customer]:
customers = []
statuses = [
"active",
"inactive",
]
weights = [
(
plan.customer_status_distribution
.active
),
(
plan.customer_status_distribution
.inactive
),
]
for index in range(
1,
plan.customer_count + 1,
):
customer = Customer(
id=f"C-{index:06d}",
full_name=fake.name(),
city=randomizer.choice(
plan.cities
),
status=weighted_choice(
randomizer,
statuses,
weights,
),
created_at=random_datetime(
randomizer,
plan.start_date
- timedelta(days=365),
plan.end_date,
),
)
customers.append(customer)
return customers
def generate_products(
plan: DataGenerationPlan,
randomizer: random.Random,
) -> list[Product]:
products = []
inactive_count = round(
plan.product_count
* plan.inactive_product_ratio
)
inactive_indexes = set(
randomizer.sample(
range(plan.product_count),
k=min(
inactive_count,
plan.product_count,
),
)
)
for index in range(
plan.product_count
):
category = randomizer.choice(
CATEGORIES
)
base_name = randomizer.choice(
PRODUCT_NAMES[category]
)
product = Product(
id=f"P-{index + 1:06d}",
name=(
f"{base_name} "
f"{index + 1}"
),
category=category,
unit_price=randomizer.randint(
plan.minimum_unit_price,
plan.maximum_unit_price,
),
stock=randomizer.randint(
0,
500,
),
is_active=(
index
not in inactive_indexes
),
)
products.append(product)
return products
def select_order_customers(
plan: DataGenerationPlan,
customers: list[Customer],
randomizer: random.Random,
) -> list[Customer]:
repeat_pool_size = max(
1,
round(
len(customers)
* plan.repeat_customer_ratio
),
)
repeat_pool = randomizer.sample(
customers,
k=min(
repeat_pool_size,
len(customers),
),
)
selected = []
for _ in range(plan.order_count):
use_repeat_customer = (
randomizer.random()
< plan.repeat_customer_ratio
)
source = (
repeat_pool
if use_repeat_customer
else customers
)
selected.append(
randomizer.choice(source)
)
return selected
def generate_orders(
plan: DataGenerationPlan,
customers: list[Customer],
products: list[Product],
randomizer: random.Random,
) -> list[Order]:
orders = []
statuses = [
"completed",
"pending",
"cancelled",
]
status_weights = [
(
plan.order_status_distribution
.completed
),
(
plan.order_status_distribution
.pending
),
(
plan.order_status_distribution
.cancelled
),
]
selected_customers = (
select_order_customers(
plan,
customers,
randomizer,
)
)
for index in range(
1,
plan.order_count + 1,
):
item_count = randomizer.randint(
plan.minimum_items_per_order,
min(
plan.maximum_items_per_order,
len(products),
),
)
selected_products = (
randomizer.sample(
products,
k=item_count,
)
)
items = []
for product in selected_products:
quantity = randomizer.randint(
1,
5,
)
line_total = (
product.unit_price
* quantity
)
items.append(
OrderItem(
product_id=product.id,
product_name=(
product.name
),
quantity=quantity,
unit_price=(
product.unit_price
),
line_total=line_total,
)
)
total_amount = sum(
item.line_total
for item in items
)
orders.append(
Order(
id=f"O-{index:08d}",
customer_id=(
selected_customers[
index - 1
].id
),
status=weighted_choice(
randomizer,
statuses,
status_weights,
),
created_at=random_datetime(
randomizer,
plan.start_date,
plan.end_date,
),
items=items,
total_amount=(
total_amount
),
)
)
return orders
def generate_dataset(
plan: DataGenerationPlan,
) -> GeneratedDataset:
randomizer = random.Random(
plan.seed
)
Faker.seed(plan.seed)
fake = Faker("fa_IR")
fake.seed_instance(plan.seed)
customers = generate_customers(
plan,
randomizer,
fake,
)
products = generate_products(
plan,
randomizer,
)
orders = generate_orders(
plan,
customers,
products,
randomizer,
)
return GeneratedDataset(
metadata={
"title": plan.title,
"purpose": plan.purpose,
"seed": plan.seed,
"generated_at": (
datetime.utcnow()
.isoformat()
+ "Z"
),
"currency": "IRR",
},
customers=customers,
products=products,
orders=orders,
)
در Python جدیدتر بهتر است از datetime.now(timezone.utc) استفاده شود. در نمونه عملیاتی زمان تولید را با Timezone صریح ثبت کنید.
بررسی کیفیت Dataset
فایل generator/quality.py:
from collections import Counter
from generator.schemas import (
DataGenerationPlan,
GeneratedDataset,
)
class DatasetQualityError(
ValueError
):
pass
def validate_dataset(
plan: DataGenerationPlan,
dataset: GeneratedDataset,
) -> dict:
errors = []
warnings = []
if (
len(dataset.customers)
!= plan.customer_count
):
errors.append(
"Customer count does not "
"match the plan."
)
if (
len(dataset.products)
!= plan.product_count
):
errors.append(
"Product count does not "
"match the plan."
)
if (
len(dataset.orders)
!= plan.order_count
):
errors.append(
"Order count does not "
"match the plan."
)
customer_ids = {
customer.id
for customer in (
dataset.customers
)
}
product_ids = {
product.id
for product in (
dataset.products
)
}
if (
len(customer_ids)
!= len(dataset.customers)
):
errors.append(
"Duplicate customer IDs found."
)
if (
len(product_ids)
!= len(dataset.products)
):
errors.append(
"Duplicate product IDs found."
)
order_ids = {
order.id
for order in dataset.orders
}
if (
len(order_ids)
!= len(dataset.orders)
):
errors.append(
"Duplicate order IDs found."
)
for order in dataset.orders:
if (
order.customer_id
not in customer_ids
):
errors.append(
f"Order {order.id} references "
"an unknown customer."
)
expected_total = 0
for item in order.items:
if (
item.product_id
not in product_ids
):
errors.append(
f"Order {order.id} references "
"an unknown product."
)
expected_line_total = (
item.quantity
* item.unit_price
)
if (
item.line_total
!= expected_line_total
):
errors.append(
f"Invalid line total in "
f"order {order.id}."
)
expected_total += (
item.line_total
)
if (
order.total_amount
!= expected_total
):
errors.append(
f"Invalid total amount in "
f"order {order.id}."
)
status_counts = Counter(
order.status
for order in dataset.orders
)
cities_with_completed_order = {
customer.city
for customer in (
dataset.customers
)
if any(
order.customer_id
== customer.id
and order.status
== "completed"
for order in dataset.orders
)
}
missing_cities = (
set(plan.cities)
- cities_with_completed_order
)
if missing_cities:
warnings.append(
"Some cities have no completed "
"orders: "
+ ", ".join(
sorted(missing_cities)
)
)
if errors:
raise DatasetQualityError(
"\n".join(errors[:20])
)
return {
"valid": True,
"customer_count": len(
dataset.customers
),
"product_count": len(
dataset.products
),
"order_count": len(
dataset.orders
),
"order_status_counts": dict(
status_counts
),
"warnings": warnings,
}
این مرحله از پذیرش Dataset دارای Foreign Key یا مبلغ اشتباه جلوگیری میکند.
نکته مهم درباره توزیع احتمالی
اگر Plan میگوید:
completed = 70%
در نمونه تصادفی کوچک لزوماً دقیقاً ۷۰ درصد رکوردها completed نمیشوند. دو رویکرد وجود دارد:
توزیع احتمالی
برای هر سفارش با وزن مشخص انتخاب میشود. نسبت نهایی تقریباً نزدیک است.
توزیع دقیق
تعداد هر وضعیت از قبل محاسبه و سپس فهرست Shuffle میشود.
برای تست گزارشهای دقیق، روش دوم بهتر است.
def build_exact_statuses(
order_count,
distribution,
randomizer,
):
completed_count = round(
order_count
* distribution.completed
)
pending_count = round(
order_count
* distribution.pending
)
cancelled_count = (
order_count
- completed_count
- pending_count
)
statuses = (
["completed"] * completed_count
+ ["pending"] * pending_count
+ ["cancelled"]
* cancelled_count
)
randomizer.shuffle(statuses)
return statuses
ساخت فایل Plan و Dataset
فایل build_dataset.py:
from pathlib import Path
from generator.data_generator import (
generate_dataset,
)
from generator.planner import (
generate_plan,
)
from generator.quality import (
validate_dataset,
)
from generator.validator import (
validate_plan,
)
REQUIREMENT = """
برای تست داشبورد فروش یک فروشگاه اینترنتی داده بساز.
نیازها:
- 200 مشتری
- 80 محصول
- 3000 سفارش
- بازه سهماهه
- شهرهای تهران، شیراز، اصفهان، مشهد و تبریز
- بیشتر سفارشها تکمیلشده باشند
- بخشی از مشتریان خرید تکراری داشته باشند
- چند مشتری بدون سفارش وجود داشته باشند
- چند محصول غیرفعال باشد
- واحد مبلغ ریال باشد
- داده برای تست فیلتر، نمودار و Pagination مناسب باشد
"""
def main():
plan = generate_plan(
REQUIREMENT
)
validate_plan(plan)
plan_path = Path(
"output/data-plan.json"
)
plan_path.parent.mkdir(
parents=True,
exist_ok=True,
)
plan_path.write_text(
plan.model_dump_json(indent=2),
encoding="utf-8",
)
dataset = generate_dataset(
plan
)
quality_report = validate_dataset(
plan,
dataset,
)
dataset_path = Path(
"datasets/store-dataset.json"
)
dataset_path.parent.mkdir(
parents=True,
exist_ok=True,
)
dataset_path.write_text(
dataset.model_dump_json(indent=2),
encoding="utf-8",
)
quality_path = Path(
"output/quality-report.json"
)
import json
quality_path.write_text(
json.dumps(
quality_report,
ensure_ascii=False,
indent=2,
),
encoding="utf-8",
)
print(
f"Plan saved to {plan_path}"
)
print(
f"Dataset saved to {dataset_path}"
)
print(
f"Quality report saved to "
f"{quality_path}"
)
if __name__ == "__main__":
main()
اجرا:
python build_dataset.py
خروجیهای پروژه
output/data-plan.json
output/quality-report.json
datasets/store-dataset.json
با Seed ثابت، اجرای دوباره همان Plan باید داده یکسانی تولید کند؛ بهجز Metadata زمانی که باید برای مقایسه نادیده گرفته یا ثابت شود.
خروجی CSV
برای تحلیل داده ممکن است CSV مناسبتر باشد.
فایل generator/exporter.py:
import csv
from pathlib import Path
from generator.schemas import (
GeneratedDataset,
)
def export_orders_csv(
dataset: GeneratedDataset,
output_path: Path,
) -> None:
output_path.parent.mkdir(
parents=True,
exist_ok=True,
)
customer_map = {
customer.id: customer
for customer in (
dataset.customers
)
}
with output_path.open(
"w",
encoding="utf-8-sig",
newline="",
) as output_file:
writer = csv.DictWriter(
output_file,
fieldnames=[
"order_id",
"created_at",
"customer_id",
"customer_city",
"status",
"item_count",
"total_amount",
"currency",
],
)
writer.writeheader()
for order in dataset.orders:
customer = customer_map[
order.customer_id
]
writer.writerow(
{
"order_id": order.id,
"created_at": (
order.created_at
.isoformat()
),
"customer_id": (
order.customer_id
),
"customer_city": (
customer.city
),
"status": order.status,
"item_count": len(
order.items
),
"total_amount": (
order.total_amount
),
"currency": "IRR",
}
)
استفاده از utf-8-sig نمایش فارسی را در بسیاری از نسخههای Excel بهتر میکند.
ساخت Mock API
فایل mock_api.py:
from pathlib import Path
from fastapi import (
FastAPI,
HTTPException,
Query,
)
from generator.schemas import (
GeneratedDataset,
)
DATASET_PATH = Path(
"datasets/store-dataset.json"
)
if not DATASET_PATH.exists():
raise RuntimeError(
"Dataset does not exist. "
"Run build_dataset.py first."
)
dataset = (
GeneratedDataset.model_validate_json(
DATASET_PATH.read_text(
encoding="utf-8"
)
)
)
app = FastAPI(
title="Store Mock API",
version="1.0.0",
)
@app.get("/health")
def health_check():
return {
"status": "ok",
}
@app.get("/customers")
def list_customers(
page: int = Query(
default=1,
ge=1,
),
page_size: int = Query(
default=20,
ge=1,
le=100,
),
city: str | None = None,
status: str | None = None,
):
customers = dataset.customers
if city:
customers = [
customer
for customer in customers
if customer.city == city
]
if status:
customers = [
customer
for customer in customers
if customer.status == status
]
offset = (
page - 1
) * page_size
page_items = customers[
offset:offset + page_size
]
return {
"items": [
item.model_dump(
mode="json"
)
for item in page_items
],
"page": page,
"page_size": page_size,
"total_items": len(customers),
}
@app.get("/customers/{customer_id}")
def get_customer(
customer_id: str,
):
customer = next(
(
item
for item in (
dataset.customers
)
if item.id == customer_id
),
None,
)
if customer is None:
raise HTTPException(
status_code=404,
detail="Customer not found",
)
return customer.model_dump(
mode="json"
)
@app.get("/products")
def list_products(
active_only: bool = False,
):
products = dataset.products
if active_only:
products = [
product
for product in products
if product.is_active
]
return [
product.model_dump(
mode="json"
)
for product in products
]
@app.get("/orders")
def list_orders(
page: int = Query(
default=1,
ge=1,
),
page_size: int = Query(
default=20,
ge=1,
le=100,
),
status: str | None = None,
customer_id: str | None = None,
):
orders = dataset.orders
if status:
orders = [
order
for order in orders
if order.status == status
]
if customer_id:
orders = [
order
for order in orders
if (
order.customer_id
== customer_id
)
]
offset = (
page - 1
) * page_size
page_items = orders[
offset:offset + page_size
]
return {
"items": [
order.model_dump(
mode="json"
)
for order in page_items
],
"page": page,
"page_size": page_size,
"total_items": len(orders),
}
@app.get("/orders/{order_id}")
def get_order(
order_id: str,
):
order = next(
(
item
for item in dataset.orders
if item.id == order_id
),
None,
)
if order is None:
raise HTTPException(
status_code=404,
detail="Order not found",
)
return order.model_dump(
mode="json"
)
اجرای Mock API
uvicorn mock_api:app --reload
Swagger UI:
http://127.0.0.1:8000/docs
آزمایش Mock API
فهرست سفارشها:
curl \
"http://127.0.0.1:8000/orders?page=1&page_size=10"
فقط سفارشهای تکمیلشده:
curl \
"http://127.0.0.1:8000/orders?status=completed"
جزئیات سفارش:
curl \
"http://127.0.0.1:8000/orders/O-00000001"
مشتریان تهران:
curl \
"http://127.0.0.1:8000/customers?city=تهران"
Mock API مبتنی بر سناریو
فقط داشتن داده کافی نیست. Frontend باید حالتهای مختلف API را نیز آزمایش کند:
- پاسخ موفق
- فهرست خالی
- 404
- تأخیر
- خطای موقت
- Pagination آخر
- Response ناقص کنترلشده
میتوان Query Parameter آزمایشی اضافه کرد:
from time import sleep
@app.get("/scenario/orders")
def scenario_orders(
scenario: str = "success",
):
if scenario == "empty":
return {
"items": [],
"total_items": 0,
}
if scenario == "not_found":
raise HTTPException(
status_code=404,
detail="Order not found",
)
if scenario == "slow":
sleep(2)
return {
"items": [
dataset.orders[0].model_dump(
mode="json"
)
],
"total_items": 1,
}
این Endpoint فقط برای محیط Mock مناسب است و نباید با API واقعی مخلوط شود.
تولید داده از JSON Schema
اگر Schema دارید، داده باید با همان Contract تولید شود:
{
"type": "object",
"required": [
"id",
"name",
"price"
],
"properties": {
"id": {
"type": "string",
"pattern": "^P-[0-9]{6}$"
},
"name": {
"type": "string",
"minLength": 2
},
"price": {
"type": "integer",
"minimum": 0
}
}
}
معماری مناسب:
- JSON Schema Parse شود.
- Constraintهای قطعی با کد اجرا شوند.
- مدل فقط Context معنایی و سناریو پیشنهاد دهد.
- خروجی با همان Schema اعتبارسنجی شود.
تولید Mock Data از OpenAPI
OpenAPI میتواند منبع این اطلاعات باشد:
- Request Body
- Response Schema
- پارامترها
- Enum
- Required
- Example
- Status Code
مدل میتواند برای هر Endpoint سناریو پیشنهاد کند:
{
"operation_id": "get_order",
"scenarios": [
{
"name": "success",
"status_code": 200
},
{
"name": "not_found",
"status_code": 404
}
]
}
Status Code موجودنبوده نباید اختراع شود.
تولید Test Fixture کوچک با مدل
برای Fixture متنی محدود میتوان مستقیم از مدل استفاده کرد:
برای Schema زیر ۵ نمونه تیکت پشتیبانی فارسی تولید کن.
توزیع:
- ۲ تیکت پشتیبانی فنی
- ۱ سؤال صورتحساب
- ۱ درخواست قابلیت
- ۱ بازخورد مثبت
قواعد:
- خروجی JSON معتبر باشد.
- ID یکتا باشد.
- category فقط از Enum استفاده کند.
- متن هر تیکت حداکثر ۱۲۰ کلمه باشد.
سپس خروجی با Pydantic بررسی شود. برای حجم زیاد، مدل فقط Template و Plan بدهد.
داده مرزی را تصادفی نسازید
Edge Caseهای مهم بهتر است بهصورت قطعی ایجاد شوند:
EDGE_CASE_PRODUCTS = [
Product(
id="P-EDGE-001",
name="محصول بدون موجودی",
category="accessory",
unit_price=100000,
stock=0,
is_active=True,
),
Product(
id="P-EDGE-002",
name="محصول غیرفعال",
category="monitor",
unit_price=5000000,
stock=10,
is_active=False,
),
]
اگر Edge Case فقط به شانس سپرده شود، ممکن است در اجرای بعدی وجود نداشته باشد.
تضمین حداقل یک سناریو
اگر هر شهر باید سفارش موفق داشته باشد، ابتدا این رکوردهای تضمینی ساخته شوند و سپس داده تصادفی اضافه شود.
def ensure_completed_order_per_city(
plan,
customers,
products,
orders,
):
for city in plan.cities:
city_customer = next(
customer
for customer in customers
if customer.city == city
)
product = products[0]
item = OrderItem(
product_id=product.id,
product_name=product.name,
quantity=1,
unit_price=product.unit_price,
line_total=product.unit_price,
)
orders.append(
Order(
id=(
f"O-GUARANTEED-"
f"{len(orders) + 1}"
),
customer_id=city_customer.id,
status="completed",
created_at=plan.start_date,
items=[item],
total_amount=(
item.line_total
),
)
)
تعداد این رکوردها باید در order_count کل محاسبه شود تا Dataset بزرگتر از Plan نشود.
حفظ Referential Integrity
قواعد مهم:
order.customer_idباید در Customerها باشد.item.product_idباید در Productها باشد.- IDها باید یکتا باشند.
- مجموع سفارش از اقلام محاسبه شود.
- تعداد و قیمت با Line Total سازگار باشند.
- تاریخ سفارش در بازه Plan باشد.
- Enumها معتبر باشند.
این قواعد باید با کد بررسی شوند، نه با اعتماد به مدل.
داده برای Pagination
برای تست Pagination:
- تعداد رکورد از
page_sizeبیشتر باشد. - تعداد دقیقاً مضرب
page_sizeآزمایش شود. - تعداد یک واحد کمتر از مضرب آزمایش شود.
- تعداد یک واحد بیشتر از مضرب آزمایش شود.
- صفحه اول
- صفحه آخر
- صفحه بعد از آخر
page_size=1- حداکثر
page_size
Fixtureهای مناسب:
0, 1, 9, 10, 11, 19, 20, 21 رکورد
داده برای تست جستوجو
Dataset باید شامل موارد نزدیک باشد:
کیبورد مکانیکی
کیبورد بیسیم
کیبورد
کاور کیبورد
همچنین:
- فاصله اضافی
- نیمفاصله
- حروف فارسی و عربی مشابه
- متن کوتاه
- متن طولانی
- نام تکراری
- حروف انگلیسی
داده برای تست مرتبسازی
برای Sort باید این موارد وجود داشته باشند:
- مقدار مساوی
- مقدار صفر
- کمترین و بیشترین مقدار
- تاریخ مساوی
- مقدار خالی در صورت مجازبودن
- چند رکورد با کلید Sort یکسان
در صورت برابر بودن مقدار اصلی، Tie-breaker نیز باید مشخص باشد.
Snapshot یا Seed ثابت؟
Seed ثابت
Dataset با الگوریتم یکسان دوباره تولید میشود.
Snapshot
خروجی کامل در Git ذخیره و با نسخه مرجع مقایسه میشود.
روش پیشنهادی:
- Seed و Plan در Git ذخیره شوند.
- Dataset بسیار بزرگ در Git ذخیره نشود.
- Fixtureهای کوچک و مهم Snapshot شوند.
- نسخه Generator ثبت شود.
Metadata:
{
"generator_version": "1.0.0",
"seed": 1405,
"plan_hash": "HASH",
"record_counts": {
"customers": 200,
"products": 80,
"orders": 3000
}
}
Hash کردن Plan
import hashlib
import json
def calculate_plan_hash(
plan,
) -> str:
normalized = json.dumps(
plan.model_dump(
mode="json"
),
ensure_ascii=False,
sort_keys=True,
separators=(",", ":"),
)
return hashlib.sha256(
normalized.encode("utf-8")
).hexdigest()
اگر Plan، Seed یا نسخه Generator تغییر کند، خروجی نیز ممکن است تغییر کند.
تست Generator
فایل tests/test_generator.py:
from generator.data_generator import (
generate_dataset,
)
from generator.quality import (
validate_dataset,
)
def test_generation_is_reproducible(
sample_plan,
):
first = generate_dataset(
sample_plan
)
second = generate_dataset(
sample_plan
)
first_data = first.model_dump()
second_data = second.model_dump()
first_data["metadata"].pop(
"generated_at",
None,
)
second_data["metadata"].pop(
"generated_at",
None,
)
assert first_data == second_data
def test_all_order_relations_are_valid(
sample_plan,
):
dataset = generate_dataset(
sample_plan
)
report = validate_dataset(
sample_plan,
dataset,
)
assert report["valid"] is True
استفاده در Frontend
Frontend میتواند Base URL را در محیط توسعه تغییر دهد:
VITE_API_BASE_URL=http://localhost:8000
یا در Next.js:
NEXT_PUBLIC_API_BASE_URL=http://localhost:8000
این متغیر فقط URL عمومی Mock API است و نباید API Key درواره در متغیر عمومی قرار گیرد.
استفاده در Postman
پس از اجرای Mock API:
http://127.0.0.1:8000/openapi.json
این فایل را میتوان وارد Postman کرد و Collection ساخت. راهنمای عملی Postman در مقاله اتصال API درواره به Postman توضیح داده شده است.
ارزیابی AI Test Data Generator
Dataset ارزیابی Plan باید شامل این نیازها باشد:
- فروشگاه
- CRM
- تیکت پشتیبانی
- Pagination
- گزارش زمانی
- داده چندموجودیتی
- Enum
- داده خالی
- Edge Case
- Requirement مبهم
- Constraint متناقض
- Dataset کوچک
- Dataset حجیم
معیارها:
| معیار | توضیح |
|---|---|
| Schema Validity | تمام رکوردها با Schema سازگارند |
| Referential Integrity | رابطهها معتبرند |
| Distribution Accuracy | توزیع با Plan هماهنگ است |
| Edge Case Coverage | سناریوهای مرزی موجودند |
| Reproducibility | Seed یکسان خروجی یکسان میدهد |
| Arithmetic Accuracy | جمعها درستاند |
| Uniqueness | شناسه تکراری وجود ندارد |
| Requirement Alignment | Dataset هدف تست را پوشش میدهد |
| Generation Time | زمان تولید |
| Human Edit Rate | میزان اصلاح Plan |
انتخاب مدل مناسب
مدل برای این وظایف باید در موارد زیر مناسب باشد:
- درک Requirement فارسی
- طراحی Test Scenario
- پیشنهاد Edge Case
- تولید JSON معتبر
- پیروی از Schema
- تشخیص Constraint متناقض
- تولید متن فارسی طبیعی
- ساخت Plan، نه تولید حجیم ردیفها
برای Plan ساده، مدل سریع و اقتصادی کافی است. برای دامنههای پیچیده و موجودیتهای مرتبط، مدل قویتر ممکن است Plan دقیقتری ارائه دهد.
مدلها و قیمتهای بهروز را در صفحه مدلهای درواره ببینید.
مدیریت هزینه
مدل فقط Plan بسازد
هزاران ردیف با Faker و Python تولید شوند.
Cache کردن Plan
کلید Cache:
requirement_hash +
schema_version +
model_id +
prompt_version
استفاده مجدد از Fixtureها
سناریوهای تأییدشده دوباره تولید نشوند.
تولید متن محدود
فقط فیلدهای متنی نیازمند تنوع با مدل ساخته شوند.
اجرای Batch
اگر چند Template متنی لازم است، در یک درخواست ساختاریافته تولید شوند.
اشتباهات رایج
تولید تمام Dataset با مدل
برای حجم بالا از Generator قطعی استفاده کنید.
نداشتن Seed
Bug شکستخورده قابل بازتولید نخواهد بود.
داده کاملاً تصادفی
Edge Caseهای مهم باید تضمینشده باشند.
بررسینکردن Foreign Key
رابطهها باید پس از تولید اعتبارسنجی شوند.
اعتماد به مبلغ تولیدشده
Line Total و Order Total در Backend محاسبه شوند.
تغییر Test برای هماهنگی با داده
Specification تعیین میکند Dataset چه سناریویی باید بسازد.
استفاده از یک Dataset برای همه تستها
Unit Test، Integration Test، UI Test و Performance Test نیازهای متفاوتی دارند.
قرار دادن Mock API در مسیر Production
Mock Server باید محیط و استقرار جدا داشته باشد.
نقشه راه Production
مرحله اول: Plan
مدل فقط Plan و Edge Case پیشنهاد دهد.
مرحله دوم: Generator
داده با Seed و Faker تولید شود.
مرحله سوم: Quality Gate
Schema، رابطه، توزیع و جمعها بررسی شوند.
مرحله چهارم: Export
JSON، CSV و Fixtureهای کوچک ساخته شوند.
مرحله پنجم: Mock API
Frontend و Integration Test از API استفاده کنند.
مرحله ششم: CI
Generator و Quality Test در Pipeline اجرا شوند.
مرحله هفتم: Scenario Library
Planهای تأییدشده نسخهبندی و استفاده مجدد شوند.
چکلیست تولید داده تست
- هدف Dataset مشخص است.
- Schema تعریف شده است.
- Plan ساختاریافته است.
- Constraintها اعتبارسنجی میشوند.
- Seed ثابت وجود دارد.
- شناسهها یکتا هستند.
- Foreign Keyها معتبرند.
- مبالغ در کد محاسبه میشوند.
- توزیعها اندازهگیری میشوند.
- Edge Caseها تضمینشدهاند.
- داده عادی و نامعتبر جدا هستند.
- Generator نسخه دارد.
- Plan Hash ذخیره میشود.
- Mock API از Production جداست.
- Dataset Quality Test وجود دارد.
- API Key فقط در Backend نگهداری میشود.
پرسشهای متداول
داده مصنوعی یا Synthetic Data چیست؟
دادهای ساختگی است که بر اساس Schema، توزیع و قواعد مشخص تولید میشود تا سناریوهای واقعی یا مرزی را شبیهسازی کند.
آیا هوش مصنوعی میتواند داده تست بسازد؟
بله. AI میتواند Plan، Edge Case، Fixture کوچک و متنهای متنوع تولید کند. برای Dataset بزرگ بهتر است مدل Plan بسازد و Python دادهها را تولید کند.
چگونه داده تست فارسی تولید کنیم؟
از Faker با Locale فارسی مانند fa_IR، فهرست کنترلشده شهر و محصول و قواعد نرمالسازی استفاده کنید.
چگونه داده قابل تکرار بسازیم؟
برای Random و Faker یک Seed ثابت تعیین و نسخه Generator و Hash برنامه را ذخیره کنید.
Mock API چیست؟
سرویسی شبیه API واقعی است که Responseهای کنترلشده برای توسعه Frontend و تست Integration ارائه میدهد.
آیا میتوان Mock API را از OpenAPI ساخت؟
بله. Endpoint، Request، Response و Status Codeها از OpenAPI استخراج میشوند و Scenarioهای معتبر بر اساس آن ساخته میشوند.
چرا مدل نباید هزاران ردیف را مستقیماً بسازد؟
هزینه، ناپایداری ساختار، خطای رابطه، شناسه تکراری و بازتولیدناپذیری افزایش مییابد.
بهترین مدل برای تولید داده تست کدام است؟
مدلی مناسب است که Requirement فارسی و Structured Output را دقیق دنبال کند. مدلهای در دسترس را در صفحه مدلهای درواره مقایسه کنید.
API درواره چگونه متصل میشود؟
در Backend، base_url را روی https://api.darvareh.ir/v1 تنظیم کنید و Requirement و Schema را برای تولید Data Plan به مدل بفرستید.
جمعبندی
تولید داده تست با هوش مصنوعی زمانی قابل اعتماد است که مدل و Generator قطعی وظایف جداگانهای داشته باشند.
مدل هوش مصنوعی باید Requirement را درک کند، موجودیتها، توزیعها و Edge Caseها را در یک Plan ساختاریافته قرار دهد. Python و Faker باید بر اساس همان Plan، داده قابل تکرار تولید کنند. در پایان نیز Schema، روابط، شناسهها، جمع مبالغ و توزیعها باید با کد بررسی شوند.
در پروژه این مقاله یک AI Test Data Generator با Python، Faker، FastAPI، Pydantic و API درواره ساختیم. این ابزار Plan تولید میکند، داده فروشگاه فارسی میسازد، کیفیت آن را ارزیابی میکند و یک Mock API برای توسعه و تست ارائه میدهد.
برای شروع، در درواره ثبتنام و API Key دریافت کنید. سپس مدل مناسب را از صفحه مدلهای درواره انتخاب کرده و Generator را ابتدا با یک Schema کوچک و چند سناریوی مشخص آزمایش کنید.
مقالات مرتبط
- آموزش Structured Outputs و JSON Schema
- آموزش اتصال API درواره به Postman
- آموزش API درواره با cURL
- چگونه API هوش مصنوعی را به نرمافزار خود اضافه کنیم؟
- راهنمای ساخت API هوش مصنوعی آماده Production
- آموزش ارزیابی مدلهای هوش مصنوعی و Evals
- ساخت چتبات با Next.js، React و API درواره
- راهنمای انتخاب بهترین API هوش مصنوعی
برای مطالعه شرایط استفاده و محدودیتهای مسئولیت، صفحه «سلب مسئولیت» را مشاهده کنید.