AI-автогенерація unit-тестів
Unit-тести пишуть усі, але мало хто робить це систематично — особливо для успадкованого коду, де немає тестів і незрозуміло з чого починати. AI-генератор створює pytest/Jest/JUnit тести безпосередньо з вихідного коду, аналізуючи логіку через AST та виявляючи сценарії, які людина пропустила б.
Python: генерація pytest через AST + LLM
import ast
import inspect
from langchain_openai import ChatOpenAI
from pathlib import Path
class UnitTestGenerator:
PYTEST_PROMPT = """Генеруй pytest unit-тести для функції.
Код функції:
```python
{function_code}
Залежності модуля: {imports}
Аналіз через AST:
- Цикломатична складність: {complexity}
- Гілки умов: {branches}
- Виклики зовнішніх залежностей: {external_calls}
Вимоги до тестів:
- Використовуй @pytest.mark.parametrize для наборів даних
- Mock зовнішні залежності через pytest-mock (mocker.patch)
- Тестуй всі гілки: кожну умову if/elif/else
- Тестуй raises: для кожного raise в коді
- Використовуй fixtures для переважених об'єктів
- Імена тестів: test_{function_name}_{scenario} (напр. test_calculate_tax_zero_income)
Повернення тільки коду тестів з import-секцією."""
def __init__(self):
self.llm = ChatOpenAI(model="gpt-4o", temperature=0.1)
def generate_tests_for_file(self, source_path: str) -> str:
source = Path(source_path).read_text(encoding="utf-8")
tree = ast.parse(source)
all_tests = []
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
if node.name.startswith("_"):
continue # пропускаємо приватні методи
func_source = ast.get_source_segment(source, node)
analysis = self._analyze_function(node, source)
tests = self._generate_function_tests(func_source, analysis, source)
all_tests.append(tests)
return self._merge_test_files(all_tests, source_path)
def _analyze_function(self, node, source: str) -> dict:
"""AST-аналіз функції перед генерацією"""
branches = []
external_calls = []
raises = []
for child in ast.walk(node):
if isinstance(child, ast.If):
cond = ast.get_source_segment(source, child.test)
branches.append(cond)
elif isinstance(child, ast.Call):
if isinstance(child.func, ast.Attribute):
call = f"{ast.get_source_segment(source, child.func.value)}.{child.func.attr}"
external_calls.append(call)
elif isinstance(child, ast.Raise):
if child.exc:
raises.append(ast.get_source_segment(source, child.exc))
return {
"complexity": self._cyclomatic_complexity(node),
"branches": branches[:5], # топ-5
"external_calls": list(set(external_calls))[:5],
"raises": raises
}
def _generate_function_tests(self, func_code: str, analysis: dict, source: str) -> str:
imports = self._extract_imports(source)
result = self.llm.invoke(
self.PYTEST_PROMPT.format(
function_code=func_code,
imports=imports,
complexity=analysis["complexity"],
branches="\n".join(analysis["branches"]),
external_calls="\n".join(analysis["external_calls"])
)
)
return result.content
### TypeScript/Jest генерація
```python
JEST_PROMPT = """Генеруй Jest unit-тести для TypeScript функції.
```typescript
{function_code}
Вимоги:
- Використовуй describe/it блоки
- jest.fn() для моків
- beforeEach для setup
- expect().toBe() / toEqual() / toThrow()
- Тест має імпортувати тільки потрібне
- Не використовуй any — тільки строга типізація в тестах
Покриття: успішні сценарії, граничні значення, помилки. Повернення тільки TypeScript код."""
async def generate_jest_tests(self, ts_function: str) -> str:
result = await self.llm.ainvoke(
self.JEST_PROMPT.format(function_code=ts_function)
)
return result.content
### Автоматичне виявлення проблем у сгенерованих тестах
Сгенеровані тести іноді мають синтаксичні помилки або неправильні assertions. Додаємо validation loop:
```python
import subprocess
class TestValidator:
def validate_and_fix(self, test_code: str, source_file: str) -> str:
"""Запускає тести та фіксить помилки в циклі"""
temp_test_file = "/tmp/test_generated.py"
Path(temp_test_file).write_text(test_code)
for attempt in range(3):
result = subprocess.run(
["pytest", temp_test_file, "-x", "--tb=short",
f"--rootdir={Path(source_file).parent}"],
capture_output=True, text=True, timeout=60
)
if result.returncode == 0:
break
# Фіксимо помилки через LLM
fix_prompt = f"""Виправ pytest тести, які не проходять.
Тести:
{test_code}
Помилка:
{result.stdout[-2000:]}
Повернення виправлених тестів (тільки код)."""
test_code = self.llm.invoke(fix_prompt).content
Path(temp_test_file).write_text(test_code)
return test_code
Інтеграція через pre-commit hook
# .pre-commit-config.yaml
repos:
- repo: local
hooks:
- id: ai-test-generator
name: AI Unit Test Generator
entry: python scripts/generate_tests.py
language: python
pass_filenames: true
types: [python]
stages: [push] # тільки при push, не при кожному commit
Кейс: Python-сервіс обробки платежів, 12 000 рядків коду, 0 unit-тестів (legacy). Запустили генератор на всю кодову базу: 340 тестів за 45 хвилин. Після validation loop: 298 пройшли без змін, 42 потребували 1–2 ітерації фіксу. З 298 робочих тестів — 11 впали на реальному коді, виявивши баги: некоректна обробка від'ємних сум, помилка при порожному списку транзакцій, неправильна timezone в розрахунку дедлайну.
Строки: генератор для однієї мови (Python/TypeScript) з validation loop: 2–3 тижні; мультимовний з CI/CD інтеграцією: 4–5 тижнів.







