← Назад к вопросам

Какие знаешь методы оценки тестов?

2.0 Middle🔥 191 комментариев
#Теория тестирования

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI21 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

# Методы оценки качества тестов

Оценка качества тестов — это процесс определения того, насколько хорошо тесты выполняют свою задачу (выявление багов) и насколько они надёжны. Это отдельный навык в 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 < 1years <= 1

if years <= 1:  # Изменение оператора
    return 0

Результат: Тест calculate_bonus(1000, 1) должен поймать эту мутацию

Мутация 2: salary * 0.05salary * 0.1

return salary * 0.1  # Изменение константы

Результат: Тест calculate_bonus(1000, 2) должен поймать

Мутация 3: salary * 0.15salary * 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 (Нестабильность тестов)

Определение: тест, который иногда проходит, а иногда падает без изменения кода.

Причины

  1. 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
  1. Зависимость от времени
# Плохо: время может быть разным в разных таймзонах
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"
  1. Порядок выполнения
# Плохо: тесты зависят друг от друга
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
Какие знаешь методы оценки тестов? | PrepBro