AI-система тестирования и QA программного обеспечения
Покрытие кода тестами на 80% звучит хорошо, пока не смотришь на то, что именно покрыто: happy path, очевидные кейсы, но не граничные условия, не интеграции между компонентами, не edge cases с неожиданными данными. AI-система QA решает не проблему «нет тестов», а проблему «тесты есть, но они не ловят то, что нужно».
Компоненты AI-системы тестирования
[Code Analysis] [Requirement Analysis]
AST парсинг NLP из Jira/Confluence
↓ ↓
[Test Generation Engine]
Unit | Integration | E2E | API
↓
[Test Prioritization]
Change Impact Analysis → запускать нужные тесты, не все
↓
[Result Analysis]
Failure Classification + Root Cause Suggestion
↓
[Coverage Intelligence]
Семантические пробелы в покрытии
AI-анализ покрытия: поиск семантических пробелов
Традиционный coverage (Istanbul, JaCoCo) считает строки. Проблема: 100% line coverage не означает, что протестированы все бизнес-сценарии.
from langchain_openai import ChatOpenAI
import ast
import textwrap
class SemanticCoverageAnalyzer:
"""Анализирует семантические пробелы в тестовом покрытии"""
ANALYSIS_PROMPT = """Проанализируй функцию и существующие тесты.
Определи, какие бизнес-сценарии и граничные условия НЕ покрыты.
Функция:
```python
{function_code}
Существующие тесты:
{existing_tests}
Определи непокрытые сценарии:
- Граничные значения (empty string, None, 0, max int, negative)
- Комбинации параметров
- Сценарии ошибок (exceptions, invalid input)
- Конкурентные доступы (если применимо)
- Бизнес-правила в условиях
Для каждого: опиши сценарий + почему он важен + возможный баг если не тестировать. Верни JSON: {{gaps: [{{scenario, importance, potential_bug}}]}}"""
def __init__(self):
self.llm = ChatOpenAI(model="gpt-4o", temperature=0.1)
def analyze_function_coverage(
self,
function_source: str,
test_source: str
) -> list[dict]:
result = self.llm.invoke(
self.ANALYSIS_PROMPT.format(
function_code=function_source,
existing_tests=test_source
)
)
import json
return json.loads(result.content)["gaps"]
def extract_functions_from_module(self, source: str) -> list[dict]:
"""Извлекает функции из Python-модуля через AST"""
tree = ast.parse(source)
functions = []
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
func_source = ast.get_source_segment(source, node)
complexity = self._calculate_cyclomatic_complexity(node)
functions.append({
"name": node.name,
"source": func_source,
"complexity": complexity,
"line_start": node.lineno
})
return sorted(functions, key=lambda x: x["complexity"], reverse=True)
def _calculate_cyclomatic_complexity(self, node) -> int:
"""Цикломатическая сложность — приоритет для тестирования"""
complexity = 1
for child in ast.walk(node):
if isinstance(child, (ast.If, ast.While, ast.For, ast.ExceptHandler,
ast.With, ast.Assert)):
complexity += 1
elif isinstance(child, ast.BoolOp):
complexity += len(child.values) - 1
return complexity
### Тест-генератор с мутационным тестированием
```python
class AITestGenerator:
UNIT_TEST_PROMPT = """Сгенерируй pytest unit-тесты для функции.
Функция:
{function_code}
Непокрытые сценарии (сфокусируйся на них):
{gaps}
Требования:
- Используй pytest + pytest-mock
- Параметризуй через @pytest.mark.parametrize где применимо
- Для каждого теста: Arrange-Act-Assert
- Тесты на граничные значения
- Тесты на ошибочные входные данные
- Мок для внешних зависимостей
Верни только код, без объяснений."""
async def generate_unit_tests(
self,
function_source: str,
gaps: list[dict]
) -> str:
gaps_text = "\n".join([
f"- {g['scenario']}: {g['importance']}"
for g in gaps[:5] # топ-5 по важности
])
result = await self.llm.ainvoke(
self.UNIT_TEST_PROMPT.format(
function_code=function_source,
gaps=gaps_text
)
)
return result.content
async def run_mutation_testing(self, source_file: str, test_file: str) -> dict:
"""Запускает мутационное тестирование через mutmut"""
import subprocess
result = subprocess.run(
["mutmut", "run", f"--paths-to-mutate={source_file}",
f"--tests-dir={test_file}"],
capture_output=True, text=True
)
# Анализируем выживших мутантов (тесты не поймали изменение)
survived = self._parse_survived_mutants(result.stdout)
if survived:
additional_tests = await self._generate_for_mutants(survived, source_file)
return {"survived_count": len(survived), "additional_tests": additional_tests}
return {"survived_count": 0, "mutation_score": "100%"}
Интеграция в CI/CD
# .github/workflows/ai-qa.yml
name: AI QA Analysis
on:
pull_request:
types: [opened, synchronize]
jobs:
ai-test-analysis:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # нужен для diff
- name: Analyze changed files
run: |
git diff origin/main...HEAD --name-only --diff-filter=AM | \
grep "\.py$" > changed_files.txt
- name: Run AI coverage analysis
run: |
python qa_system/analyze_coverage.py \
--changed-files changed_files.txt \
--generate-missing-tests \
--output coverage_report.json
- name: Comment PR with AI findings
uses: actions/github-script@v7
with:
script: |
const report = require('./coverage_report.json')
const comment = formatReport(report)
github.rest.issues.createComment({
issue_number: context.issue.number,
body: comment
})
Кейс: backend сервис на Python (FastAPI), 45 000 строк кода, 380 тестов. Coverage: 74%. AI-анализ выявил 89 семантических пробелов (не строчных — сценарных), из которых 34 помечены как высокоприоритетные. Сгенерировано 67 дополнительных тестов. При прогоне: 8 из 67 тестов упали — нашли реальные баги в обработке граничных условий (None в агрегации, отрицательные количества в заказе, пустой список при сортировке).
Сроки
- Анализ покрытия + генерация unit-тестов: 3–4 недели
- Полная QA-система с CI/CD интеграцией: 8–10 недель
- Мутационное тестирование и E2E: +2–3 недели







