Розробка AI-генерації юніт-тестів

Проектуємо та впроваджуємо системи штучного інтелекту: від прототипу до production-ready рішення. Наша команда поєднує експертизу в машинному навчанні, дата-інжинірингу та MLOps, щоб AI працював не в лабораторії, а в реальному бізнесі.
Показано 1 з 1Усі 1566 послуг
Розробка AI-генерації юніт-тестів
Середній
~1-2 тижні
Часті запитання

Напрямки AI-розробки

Етапи розробки AI-рішення

Останні роботи

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1284
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1196
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    901
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1119
  • image_logo-advance_0.webp
    Розробка логотипу компанії B2B Advance
    586
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    853

AI-генерація юніт-тестів

Написання тестів — найнелюбіша частина розробки: зрозуміло, що потрібно, нудно робити вручну. AI справляється з генерацією типових тест-кейсів краще, ніж розробники в режимі "надо добити coverage". Задача системи — не просто покрити рядки коду, а сгенерувати тести, які перевіряють реальне поведення: edge cases, граничні значення, обробку помилок.

Архітектура генератора тестів

from anthropic import Anthropic
import ast
import inspect
from pathlib import Path
from typing import Optional
import subprocess

client = Anthropic()

class TestGenerator:

    def __init__(self, project_root: str):
        self.project_root = project_root

    def extract_function_info(self, source_code: str, function_name: str) -> dict:
        """Витягує метаданні функції через AST"""
        tree = ast.parse(source_code)

        for node in ast.walk(tree):
            if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
                if node.name == function_name:
                    return {
                        "name": node.name,
                        "args": [arg.arg for arg in node.args.args],
                        "decorators": [ast.unparse(d) for d in node.decorator_list],
                        "is_async": isinstance(node, ast.AsyncFunctionDef),
                        "has_return": any(
                            isinstance(n, ast.Return) and n.value
                            for n in ast.walk(node)
                        ),
                        "raises": [
                            ast.unparse(n.exc) for n in ast.walk(node)
                            if isinstance(n, ast.Raise) and n.exc
                        ],
                        "source": ast.unparse(node),
                    }
        return {}

    def find_related_tests(self, source_file: str) -> str:
        """Знаходить існуючі тести для розуміння стилю"""
        source_path = Path(source_file)
        # Шукаємо test_*.py або *_test.py
        test_candidates = [
            source_path.parent / f"test_{source_path.name}",
            source_path.parent.parent / "tests" / f"test_{source_path.name}",
            source_path.parent / "tests" / f"test_{source_path.name}",
        ]

        for test_file in test_candidates:
            if test_file.exists():
                return test_file.read_text()[:2000]
        return ""

    def generate_tests(
        self,
        source_file: str,
        function_name: Optional[str] = None,
    ) -> str:
        """Генерує тести для файла або конкретної функції"""
        source_code = Path(source_file).read_text()
        existing_tests = self.find_related_tests(source_file)

        # Якщо вказана функція — фокусуємось на ній
        if function_name:
            func_info = self.extract_function_info(source_code, function_name)
            context = f"Function to test:\n```python\n{func_info.get('source', '')}\n```"
        else:
            context = f"File to test:\n```python\n{source_code[:4000]}\n```"

        existing_context = ""
        if existing_tests:
            existing_context = f"\nExisting test style (follow this pattern):\n```python\n{existing_tests}\n```"

        response = client.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=4096,
            system="""You are a senior developer writing pytest tests.
Rules:
- Test behavior, not implementation
- One test = one check (AAA: Arrange, Act, Assert)
- Name tests as: test_<function>_<scenario>_<expectation>
- Cover: happy path, edge cases, errors/exceptions, boundary values
- Use pytest.mark.parametrize for similar tests
- For async functions — pytest-asyncio
- Mock external dependencies via pytest-mock""",
            messages=[{
                "role": "user",
                "content": f"""{context}{existing_context}

Generate complete test file with pytest. Return only code, no explanations."""
            }]
        )

        return response.content[0].text

Параметризовані тести з граничними значеннями

    def generate_parametrized_tests(
        self,
        function_source: str,
        function_signature: str,
    ) -> str:
        """Генерує параметризовані тести з граничними значеннями"""

        response = client.messages.create(
            model="claude-sonnet-4-5",
            max_tokens=2048,
            messages=[{
                "role": "user",
                "content": f"""Generate pytest.mark.parametrize test for this function
with boundary values and edge cases.

```python
{function_source}

Signature: {function_signature}

Response format — Python code only:

@pytest.mark.parametrize("input,expected", [
    # happy path
    # edge cases
    # boundary values
    # error cases (pytest.raises)
])
def test_<function_name>(input, expected):
    ...
```"""
            }]
        )

        return response.content[0].text

    def run_and_fix(self, test_file: str, source_file: str, max_attempts: int = 3) -> str:
        """Запускає тести та ітеративно виправляє помилки"""
        test_content = Path(test_file).read_text()

        for attempt in range(max_attempts):
            result = subprocess.run(
                ["python", "-m", "pytest", test_file, "-v", "--tb=short"],
                capture_output=True, text=True, timeout=60
            )

            if result.returncode == 0:
                return test_content  # Всі тести пройшли

            # Виправляємо помилки
            response = client.messages.create(
                model="claude-sonnet-4-5",
                max_tokens=4096,
                messages=[{
                    "role": "user",
                    "content": f"""Tests failed. Fix the test file.

Test file:
```python
{test_content}

Errors:

{result.stdout[-2000:]}

Return fixed complete test file.""" }] )

        test_content = response.content[0].text
        Path(test_file).write_text(test_content)

    return test_content

### Mutation Testing для оцінки якості тестів

Сгенеровані тести важливо перевірити — покривають вони реальні баги?

```python
import mutmut
from pathlib import Path

def evaluate_test_quality(source_file: str, test_file: str) -> dict:
    """Запускає mutation testing для оцінки якості тестів"""

    result = subprocess.run(
        ["mutmut", "run", f"--paths-to-mutate={source_file}", f"--tests-dir={test_file}"],
        capture_output=True, text=True, timeout=300
    )

    # Парсимо результат
    survived = 0
    killed = 0
    for line in result.stdout.splitlines():
        if "survived" in line.lower():
            survived += 1
        elif "killed" in line.lower():
            killed += 1

    total = survived + killed
    mutation_score = killed / total if total > 0 else 0

    return {
        "mutation_score": mutation_score,
        "killed_mutants": killed,
        "survived_mutants": survived,
        "verdict": "excellent" if mutation_score > 0.8 else "good" if mutation_score > 0.6 else "needs_improvement"
    }

Інтеграція з pytest-cov та автоматичний звіт

def generate_coverage_report(test_file: str, source_file: str) -> dict:
    """Запускає тести з coverage та повертає звіт"""

    result = subprocess.run(
        [
            "python", "-m", "pytest", test_file,
            f"--cov={source_file}",
            "--cov-report=json:coverage.json",
            "--cov-report=term-missing",
            "-v"
        ],
        capture_output=True, text=True
    )

    import json
    try:
        with open("coverage.json") as f:
            coverage_data = json.load(f)

        uncovered_lines = []
        for file_data in coverage_data.get("files", {}).values():
            uncovered_lines.extend(file_data.get("missing_lines", []))

        return {
            "coverage_percent": coverage_data.get("totals", {}).get("percent_covered", 0),
            "uncovered_lines": uncovered_lines,
            "passed": result.returncode == 0,
        }
    except FileNotFoundError:
        return {"coverage_percent": 0, "passed": False}

CLI для команди

import click

@click.command()
@click.argument("source_file")
@click.option("--function", "-f", help="Specific function to test")
@click.option("--output", "-o", help="Output test file path")
@click.option("--run/--no-run", default=True, help="Run tests after generation")
def generate(source_file: str, function: str, output: str, run: bool):
    """Генерує юніт-тести для Python файла"""

    generator = TestGenerator(".")
    tests = generator.generate_tests(source_file, function)

    if not output:
        source_path = Path(source_file)
        output = str(source_path.parent / f"test_{source_path.name}")

    Path(output).write_text(tests)
    click.echo(f"Tests written to {output}")

    if run:
        click.echo("Running tests...")
        fixed_tests = generator.run_and_fix(output, source_file)
        coverage = generate_coverage_report(output, source_file)
        click.echo(f"Coverage: {coverage['coverage_percent']:.1f}%")

if __name__ == "__main__":
    generate()

Практичний кейс: legacy Python-сервіс без тестів

Задача: 8000 рядків Python-коду, 0% coverage, рефакторинг неможливий без тестів.

Процес:

  1. Автоматичний аналіз всіх .py файлів через AST
  2. Генерація тестів по файлах (batch, 5 файлів паралельно)
  3. Авто-запуск та fix цикл (до 3 ітерацій)
  4. Ручний просмотр тестів з coverage < 60%

Результати за 2 тижні:

  • Сгенеровано 847 тест-функцій
  • Coverage: 0% → 71%
  • Знайдено 12 реальних багів у процесі генерації (AI помітив невідповідність поведінки та типів)
  • 94% сгенерованих тестів пройшли без правок
  • 6% потребували ручної доробки (складні mock-залежності)

Mutation score итогових тестів: 0.74 (хорошо, aber не excellent — деякі edge cases AI не покрив).

Терміни

  • Базовий генератор (один файл, выгрузка коду): 1–2 дні
  • Авто-запуск та fix-цикл: 2–3 дні
  • Інтеграція в CI/CD з coverage gate: 1 тиждень
  • Повний pipeline для legacy кодової бази: 2–3 тижні