AI-система тестування та QA програмного забезпечення
Покриття коду тестами на 80% звучить добре, поки не дивишся на те, що саме покрито: happy path, очевидні кейси, але не граничні умови, не інтеграції між компонентами, не edge cases з неочікуваними даними. AI-система QA вирішує не проблему "немає тестів", а проблему "тести є, але вони не ловлять те, що потрібно".
Компоненти AI-системи тестування
[Аналіз коду] [Аналіз вимог]
AST парсинг NLP з Jira/Confluence
↓ ↓
[Движок генерації тестів]
Unit | Integration | E2E | API
↓
[Пріоритизація тестів]
Change Impact Analysis → запускати потрібні тесты, не все
↓
[Аналіз результатів]
Класифікація відмов + Пропозиція root cause
↓
[Інтелект покриття]
Семантичні прогалини в покритті
AI-аналіз покриття: пошук семантичних прогалин
Традиційне покриття (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
- Тести на граничні значення
- Тесті на помилкові вхідні дані
- Mock для зовнішніх залежностей
Повернення тільки коду, без пояснень."""
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 тижні







