Какие знаешь методы оценки тестов?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
# Методы оценки качества тестов
Оценка качества тестов — это процесс определения того, насколько хорошо тесты выполняют свою задачу (выявление багов) и насколько они надёжны. Это отдельный навык в QA.
1. Code Coverage (Покрытие кода)
Определение: процент строк/веток кода, которые были выполнены во время тестирования.
Типы coverage
a) Line Coverage (строки)
Процент строк кода, которые были выполнены.
def calculate_discount(amount: float, is_vip: bool) -> float:
if is_vip: # Line 1
return amount * 0.9 # Line 2 (может не выполниться)
return amount # Line 3
# Тест
def test_regular_user():
assert calculate_discount(100, False) == 100 # Выполнил Line 1, 3
# Line 2 не выполнена
# Coverage: 2/3 = 66.7%
b) Branch Coverage (ветки)
Процент возможных путей выполнения кода.
# Два пути: is_vip=True и is_vip=False
# Нужны оба теста для 100% branch coverage
def test_vip_user():
assert calculate_discount(100, True) == 90 # Путь 1
def test_regular_user():
assert calculate_discount(100, False) == 100 # Путь 2
# Coverage: 2/2 = 100%
c) Statement vs. Condition Coverage
def process_order(amount: float, is_vip: bool, has_coupon: bool) -> float:
if amount > 100 and (is_vip or has_coupon): # Условие с AND, OR
return amount * 0.85
return amount
# Statement: проверили одну ветку
def test_basic():
assert process_order(150, True, False) == 127.5
# Condition: нужно проверить все комбинации AND/OR
def test_all_conditions():
# amount > 100 = True, is_vip = True, has_coupon = False
assert process_order(150, True, False) == 127.5
# amount > 100 = True, is_vip = False, has_coupon = True
assert process_order(150, False, True) == 127.5
# amount > 100 = True, is_vip = False, has_coupon = False
assert process_order(150, False, False) == 150
# amount <= 100
assert process_order(50, True, True) == 50
Инструменты для измерения coverage
# Python: pytest-cov
pip install pytest-cov
pytest --cov=app --cov-report=html --cov-report=term-missing
# Вывод:
# app/calculator.py 85% (missed lines: 12, 15, 23)
# app/models.py 92%
# TOTAL 88%
Coverage в CI/CD
# .github/workflows/test.yml
name: Tests
on: [push]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Run tests with coverage
run: |
pip install pytest pytest-cov
pytest --cov=app --cov-report=xml
- name: Upload coverage
uses: codecov/codecov-action@v2
with:
files: ./coverage.xml
fail_ci_if_error: true # Fail если coverage ниже threshold
Общий порог: 80-90% (зависит от проекта)
2. Mutation Testing
Определение: инструмент вносит небольшие изменения (мутации) в production код и проверяет, поймут ли это тесты.
Если тесты не ловят мутацию — это признак слабого теста.
Пример
# Оригинальный код
def calculate_bonus(salary: float, years: int) -> float:
if years < 1:
return 0
if years < 5:
return salary * 0.05
if years < 10:
return salary * 0.1
return salary * 0.15
# Тест
def test_bonus_calculation():
assert calculate_bonus(1000, 0) == 0
assert calculate_bonus(1000, 2) == 50
assert calculate_bonus(1000, 7) == 100
assert calculate_bonus(1000, 15) == 150
Мутации, которые может внести mutmut
Мутация 1: years < 1 → years <= 1
if years <= 1: # Изменение оператора
return 0
Результат: Тест calculate_bonus(1000, 1) должен поймать эту мутацию
Мутация 2: salary * 0.05 → salary * 0.1
return salary * 0.1 # Изменение константы
Результат: Тест calculate_bonus(1000, 2) должен поймать
Мутация 3: salary * 0.15 → salary * 0
return salary * 0 # Изменение оператора
Результат: Тест calculate_bonus(1000, 15) должен поймать
Использование
# Установка
pip install mutmut
# Запуск
mutmut run
# Результаты
mutmut results
# Исходное:
# mutant 1: Mutation Tests 15/15
# mutant 2: Mutation Tests 15/15
# mutant 3: Mutation Tests 15/15
# Вывод: все мутации пойманы (убиты тестами)
Kill Rate: процент убитых мутаций
- 100% kill rate = все тесты сильные
- < 80% kill rate = есть слабые места в тестах
3. Test Execution Speed
Проблема: медленные тесты никто не запускает часто, пропускают баги.
Метрики
# Время выполнения
# Хорошо: < 5 минут (всю сьют)
# Приемлемо: < 15 минут
# Плохо: > 1 часа
pytest --durations=10 # 10 самых долгих тестов
Результат:
test_database_connection 45.23s
test_api_integration 23.45s
test_file_upload 12.34s
Оптимизация
# Плохо: каждый тест создаёт новый браузер (медленно)
@pytest.fixture
def driver():
driver = webdriver.Chrome() # Setup для каждого теста
yield driver
driver.quit() # Teardown
# Хорошо: один браузер для модуля
@pytest.fixture(scope="module")
def driver():
driver = webdriver.Chrome()
yield driver
driver.quit()
# Результат: ускорение в 10+ раз
4. Flakiness (Нестабильность тестов)
Определение: тест, который иногда проходит, а иногда падает без изменения кода.
Причины
- Race conditions
# Плохо: нет ожидания
def test_login():
driver.find_element(By.ID, "login").send_keys("user")
driver.find_element(By.ID, "password").send_keys("pass")
driver.find_element(By.CSS_SELECTOR, "button").click()
# Элемент может ещё не загрузиться!
assert "Welcome" in driver.page_source
# Хорошо: ждём явно
def test_login_fixed():
driver.find_element(By.ID, "login").send_keys("user")
driver.find_element(By.ID, "password").send_keys("pass")
driver.find_element(By.CSS_SELECTOR, "button").click()
# Ждём элемента максимум 10 секунд
WebDriverWait(driver, 10).until(
EC.presence_of_element_located((By.TEXT, "Welcome"))
)
assert True
- Зависимость от времени
# Плохо: время может быть разным в разных таймзонах
def test_schedule():
assert get_current_time() == "09:00" # Может быть "08:00" на другом сервере
# Хорошо: мокировать время
from unittest.mock import patch
from datetime import datetime
@patch('app.time.now')
def test_schedule_fixed(mock_now):
mock_now.return_value = "09:00"
assert get_current_time() == "09:00"
- Порядок выполнения
# Плохо: тесты зависят друг от друга
def test_1():
create_user("alice") # Создаёт состояние
def test_2():
user = get_user("alice") # Зависит от test_1
assert user is not None
# Хорошо: каждый тест независим
@pytest.fixture
def clean_database():
clear_db()
yield
clear_db()
def test_1(clean_database):
create_user("alice")
assert get_user("alice") is not None
def test_2(clean_database):
# Уверены, что БД чистая
assert get_user("alice") is None
Детектирование flaky tests
# pytest-rerunfailures: перезапустить упавший тест
pip install pytest-rerunfailures
pytest --reruns 3 # Перезапустить 3 раза, если упал
# Если тест упадёт снова → это flaky
5. Test Maintainability
Вопросы для оценки:
- Легко ли добавить новый тест? (< 10 минут)
- Легко ли изменить существующий? (< 5 минут)
- Тесты используют DRY принцип?
- Понятные ли имена тестов?
# Плохо: сложно поддерживать
def test_1():
driver.get("http://example.com")
driver.find_element(By.ID, "user_field").send_keys("john")
driver.find_element(By.ID, "pass_field").send_keys("123")
driver.find_element(By.XPATH, "//button[@type='submit']").click()
time.sleep(2)
assert driver.find_element(By.CLASS_NAME, "welcome_message")
# Хорошо: Page Object + понятные названия
class LoginPage:
def __init__(self, driver):
self.driver = driver
def login_as(self, username: str, password: str):
self.driver.find_element(By.ID, "user_field").send_keys(username)
self.driver.find_element(By.ID, "pass_field").send_keys(password)
self.driver.find_element(By.XPATH, "//button[@type='submit']").click()
def is_logged_in(self) -> bool:
return bool(self.driver.find_element(By.CLASS_NAME, "welcome_message"))
def test_user_can_login_with_correct_credentials():
page = LoginPage(driver)
page.login_as("john", "123")
assert page.is_logged_in()
6. Defect Detection Rate
Определение: сколько процентов реальных багов в production были поймены тестами?
Дефектов поймено тестами: 85 / Всего дефектов в production: 100 = 85% DDR
Расчет
# Данные за месяц
bugs_detected_in_qa = 85 # Найдено в QA тестами
bugs_found_in_prod = 15 # Пропущено в production
total_bugs = bugs_detected_in_qa + bugs_found_in_prod # 100
detection_rate = bugs_detected_in_qa / total_bugs * 100
print(f"Detection Rate: {detection_rate}%") # 85%
Цель: > 90% (то есть пропустить < 10% багов)
7. Test ROI (Return On Investment)
Расчет:
РОИ = (Стоимость багов, пойманных тестами - Стоимость написания тестов) / Стоимость написания тестов
Пример:
Время на написание UI тестов: 40 часов = $2000
Окладът квалифицированного инженера: $50/час
Баги, поймано тестами:
- 3 критичных баги = 3 × $5000 = $15000 (стоимость production incident)
- 10 обычных багов = 10 × $500 = $5000
Общо спасено: $20000
РОИ = ($20000 - $2000) / $2000 = 900%
Это означает, что каждый $1, потраченный на тесты, возвращает $9
Заключение: Как я оцениваю качество тестов
Комплексный подход:
# 1. Coverage > 80%
pytest --cov=app --cov-report=term-missing
# 2. Mutation score > 85%
mutmut run --paths-to-mutate=app
# 3. Execution time < 10 минут
pytest --durations=10
# 4. Flakiness < 2%
pytest --reruns 3 # Если падает чаще — это flaky
# 5. Code quality (чистота, читаемость)
# - Page Object Pattern используется
# - DRY принцип соблюдается
# - Тесты независимы
# 6. Detection Rate > 90%
# Отслеживать через метрики: bugs_in_qa / total_bugs
Красные флаги:
- Coverage < 60% — мало покрытия
- Mutation score < 70% — слабые тесты
- Execution > 30 минут — нужна оптимизация
-
10% flaky тестов — нестабильность
- Тесты падают при перепроверке (re-run) — race conditions