AI-автогенерация тестовых данных
Реалистичные тестовые данные — не просто user_1, [email protected], password123. Для нормального покрытия нужны данные с правильными распределениями, граничными значениями, нестандартными символами и корректными связями между таблицами. Вручную это делают плохо: либо данных мало, либо они нереалистичны.
AI-генератор создаёт тестовые данные трёх типов: структурированные (по схеме БД), семантические (реалистичный контент на основе домена) и аномальные (граничные случаи для негативных тестов).
Генерация по схеме с семантическим контекстом
from faker import Faker
from langchain_openai import ChatOpenAI
from pydantic import BaseModel
import json
import random
fake = Faker("ru_RU")
class TestDataGenerator:
SEMANTIC_PROMPT = """Сгенерируй {count} реалистичных записей для таблицы.
Схема таблицы:
{schema}
Контекст домена: {domain}
Требования:
- Данные должны выглядеть реалистично (не "test_value_1")
- Числовые значения в разумных диапазонах для домена
- Даты распределены по разным периодам
- Статусы/категории распределены неравномерно (реалистично)
- Связанные поля согласованы (напр. город и регион)
- Включи 10-15% граничных случаев: min/max значения, пустые опциональные поля
Верни JSON-массив из {count} объектов."""
def __init__(self):
self.llm = ChatOpenAI(model="gpt-4o", temperature=0.8) # высокая температура для разнообразия
def generate_for_table(
self,
schema: dict,
domain: str,
count: int = 100
) -> list[dict]:
result = self.llm.invoke(
self.SEMANTIC_PROMPT.format(
count=min(count, 50), # батчи по 50
schema=json.dumps(schema, ensure_ascii=False, indent=2),
domain=domain
)
)
batch = json.loads(result.content)
# Если нужно больше 50 — генерируем батчами
if count > 50:
all_data = batch
while len(all_data) < count:
more = self.generate_for_table(schema, domain, min(50, count - len(all_data)))
all_data.extend(more)
return all_data[:count]
return batch
def generate_boundary_cases(self, schema: dict) -> list[dict]:
"""Генерирует граничные случаи для негативных тестов"""
boundary_prompt = f"""Создай граничные и невалидные тестовые данные для схемы.
Схема:
{json.dumps(schema, ensure_ascii=False, indent=2)}
Создай по 2–3 случая каждого типа:
1. Пустые значения (null, "", [])
2. Максимальные значения (max int, max string length)
3. Минимальные значения (min int, 0, negative)
4. Специальные символы: <, >, &, ', ", \\n, \\t, emoji 🎉
5. SQL-injection строки (для проверки sanitization)
6. Очень длинные строки (>1000 символов)
7. Неверные типы (строка вместо числа и т.п.)
Верни JSON с пометкой expected_behavior для каждого случая."""
return json.loads(
self.llm.invoke(boundary_prompt).content
)
Генерация связанных данных (foreign keys)
class RelationalDataGenerator:
"""Генерирует согласованные данные для нескольких связанных таблиц"""
def generate_dataset(
self,
schema: dict, # {tables: [{name, columns, fk_relations}]}
counts: dict # {table_name: count}
) -> dict[str, list[dict]]:
"""
Генерирует данные с учётом foreign key constraints.
Порядок: сначала parent-таблицы, потом child.
"""
result = {}
order = self._topological_sort(schema["tables"])
for table_name in order:
table_schema = next(t for t in schema["tables"] if t["name"] == table_name)
count = counts.get(table_name, 10)
# Для FK-полей используем существующие ID из уже сгенерированных таблиц
fk_pools = {}
for fk in table_schema.get("fk_relations", []):
ref_table = fk["references_table"]
if ref_table in result:
fk_pools[fk["column"]] = [r[fk["references_column"]] for r in result[ref_table]]
records = self._generate_with_fk(table_schema, count, fk_pools)
result[table_name] = records
return result
def _generate_with_fk(
self,
table_schema: dict,
count: int,
fk_pools: dict
) -> list[dict]:
records = []
for _ in range(count):
record = {}
for col in table_schema["columns"]:
if col["name"] in fk_pools:
# Берём случайный существующий ID
record[col["name"]] = random.choice(fk_pools[col["name"]])
else:
record[col["name"]] = self._generate_field_value(col)
records.append(record)
return records
Faker + AI для реалистичных доменных данных
# Специализированные провайдеры Faker для разных доменов
from faker.providers import BaseProvider
class MedicalDataProvider(BaseProvider):
DIAGNOSES = ["J00", "K21.0", "I10", "E11", "M79.3"] # МКБ-10
MEDICATIONS = ["Амоксициллин 500мг", "Метформин 850мг", "Лизиноприл 10мг"]
def diagnosis_code(self) -> str:
return self.random_element(self.DIAGNOSES)
def medication(self) -> str:
return self.random_element(self.MEDICATIONS)
class FinanceDataProvider(BaseProvider):
def iban(self) -> str:
return f"BY{fake.numerify('##')}ALFA{fake.numerify('################')}"
def transaction_amount(self) -> float:
# Реалистичное распределение: много малых транзакций, мало крупных
weights = [0.5, 0.3, 0.15, 0.05]
ranges = [(1, 100), (100, 1000), (1000, 10000), (10000, 100000)]
chosen_range = random.choices(ranges, weights=weights)[0]
return round(random.uniform(*chosen_range), 2)
Anonymization: преобразование production-данных в тестовые
class DataAnonymizer:
"""Анонимизирует реальные данные для использования в тестах"""
PII_FIELDS = {
"email": lambda v: fake.email(),
"phone": lambda v: fake.phone_number(),
"name": lambda v: fake.name(),
"inn": lambda v: fake.numerify("############"),
"passport": lambda v: f"{fake.numerify('####')} {fake.numerify('######')}",
"ip_address": lambda v: fake.ipv4_private(),
"address": lambda v: fake.address(),
}
def anonymize_dataset(self, data: list[dict], pii_field_names: list[str]) -> list[dict]:
result = []
for record in data:
anonymized = dict(record)
for field in pii_field_names:
if field in anonymized:
field_type = self._detect_field_type(field)
if field_type in self.PII_FIELDS:
anonymized[field] = self.PII_FIELDS[field_type](anonymized[field])
result.append(anonymized)
return result
Кейс: e-commerce платформа, тестовое окружение с 50 000 строк production-данных (анонимизированных). После внедрения AI-генератора: тестовый датасет расширен до 500 000 записей с реалистичными распределениями. Найдены 3 производительностных бага (slow queries) которые не воспроизводились на малом датасете. Граничные данные выявили ошибку в обработке сумм > 1 000 000 руб. (integer overflow в старом PHP-коде).
Сроки: генератор структурированных данных: 1–2 недели; с анонимизацией и relational generation: 3–4 недели.







