Что означает 100% тестовое покрытие?

Для большинства инструментов покрытия кода, которые я использовал, это означает, что все операторы и условия, написанные в проекте, выполняются автоматическим тестом любого типа хотя бы один раз.

Это не означает, что эти тесты проверяют все возможные результаты вашего кода или рассматривают все крайние случаи - указанные проверки должны быть написаны вместе с тестами на основе бизнес-правил и спецификаций. Но в сценарии с полностью закрытым кодом вы могли хотя бы один раз выполнить проверки в каждой части кода.

Итак, как добиться 100% покрытия?

Полный ответ можно найти и для многих других сообщений, но я лично стараюсь написать здоровую комбинацию модульных и функциональных тестов, которые я примерно определю как:

  • Модульные тесты: более быстрое выполнение, дешевая установка. Обычно вы пишете модульный тест для операторов и условий небольшой части кода;
  • Функциональные тесты: медленнее, дорого настраивается. Обычно вы пишете функциональные тесты для критических частей кода, которые требуют таких действий, как настройка / доступ к базе данных, проверка конечных точек или взаимодействие между несколькими уровнями.

Но как визуализировать каждый сценарий, охватываемый тестами, и сколько тестов мне нужно написать, чтобы достичь 100% покрытия? Для этого вы должны визуализировать условия и циклы внутри вашего кода.

Сценарий no1: код без условий или циклов

Допустим, вы разрабатываете функцию (на Python) вроде:

def my_function(a, b):
    c = a + b
    d = c / b
    e = d * a
    return e

Поскольку в этом коде нет условий или циклов, записанных в нем, существует только один возможный путь выполнения, указанный в этой реализации с любыми двумя входными данными. Это означает, что вам нужен один тест, выполняющий эту функцию, чтобы покрыть 100% ее строк, независимо от любых проверок, которые вы делаете в отношении ее выходных данных.

Сценарий # 2: код с условиями

Теперь представим функцию, написанную следующим образом:

def my_conditional_function(a, b):
    if b == 0:
        return False
    return a % b == 0

Любой условный оператор (например, if выше) генерирует два пути выполнения:

  • Один, где условие истинно, и вы выполняете внутри оператора;
  • Тот, где условие ложно, и вы не работаете внутри оператора.

Из-за этой бифуркации каждое добавляемое новое условие требует, чтобы вы написали как минимум один новый тест, чтобы охватить его.

Некоторые примечания на стороне:

  • Новый оператор if/else в вашем коде устанавливает только один новый возможный путь выполнения (один из операторов может рассматриваться как неизбежный для существующих тестов), поэтому для покрытия нового if/else требуется только один новый тест;
  • Исключения можно рассматривать как условные сценарии. Учитывая оператор try/except в Python, вам понадобится один новый тест для каждого except пути, который вы написали. Такие операторы, как finally/else, могут рассматриваться как неизбежные с точки зрения выполнения, поэтому писать новые тесты не требуется.

Сценарий no 3: код с циклами

Циклы могут быть представлены в двух разных формах: условные циклы и безусловные циклы. Безусловный цикл - это цикл, в котором вы не можете избежать его выполнения, но у вас есть повторяющееся действие, например:

def my_unconditional_loop(a):
    for vowel in ['a', 'e', 'i', 'o', 'u']:
        print(a + vowel)

Ваш код не может избежать прохождения этого for цикла, поэтому, если вы добавите его в свой код, вам не нужно будет писать новый тест, чтобы покрыть его.

условный цикл - это цикл, в котором вы не можете запускать свой код в зависимости от его входных данных. Например:

def my_conditional_loop(some_list):
    for element in some_list:
        print(element + 1)

В этом сценарии, в зависимости от того, как определяется some_list (например, это пустой список), вы можете избежать прохождения кода внутри цикла for. Для нового условного цикла в вашем коде требуется по крайней мере один новый тест.

Пример # 1: генератор Фибоначчи (рекурсивный)

А как насчет рекурсивных функций? Давайте воспользуемся генератором чисел Фибоначчи со следующими утверждениями:

  • У вас -1, если n < 0;
  • У вас 0, если n == 0;
  • У вас 1, если n == 1;
  • Для n >= 2 результатом является сумма двух предыдущих чисел Фибоначчи.

Для рекурсивной реализации, подобной следующей:

def recursive_fibonacci(n):
    if n < 0:
        return -1
    if n == 0:
        return 0
    if n == 1:
        return 1
    return recursive_fibonacci(n-1) + recursive_fibonacci(n-2)

У вас есть три if оператора, требующие по крайней мере одного теста, и один оператор return, требующий другого теста. На практике вы можете получить 100% покрытие этой функции, написав четыре тестовых примера:

  • Один, где n < 0 (например, -1);
  • Один, где n == 0;
  • Один, где n == 1;
  • Тот, где n == 2.

Вы могли бы объединить условия для 0 и 1 в одно и сэкономить тест:

def simplified_recursive_fibonacci(n):
    if n < 0:
        return -1
    if n < 2:
        return n  # Covers for both 0 and 1
    return (
        simplified_recursive_fibonacci(n-1) +
        simplified_recursive_fibonacci(n-2)
    )

Поскольку теперь у вас есть только одно условие для 0 и 1, вам нужно написать только один тест вместо двух, чтобы охватить этот случай. Всего вам теперь нужно три теста, чтобы покрыть 100% строк.

Пример # 2: генератор Фибоначчи (одинарный цикл)

Однопетлевой эквивалент приведенной выше функции можно записать следующим образом:

def single_loop_fibonacci(n):
    if n < 0:
        return -1
    n1 = 1  # The Fibonacci number when n == -1
    n2 = 0  # The Fibonacci number when n == 0
    n3 = 0  # Copy of the previous number
    for _ in range(n):
        n3 = n1 + n2
        n1 = n2
        n2 = n3
    return n3

Вам нужно всего три теста, чтобы охватить 100% строк:

  • Тот, который проходит через первое условие (n < 0, как -1);
  • Тот, который не проходит цикл (n == 0);
  • Тот, который проходит цикл (n >= 1).

Забрать

Если вы стремитесь к 100% охвату ваших тестов, вам нужно знать о возможных путях разветвления вашего кода:

  • Для выполнения функции без циклов или условий требуется один тест, чтобы покрыть ее;
  • Каждое добавление условного оператора (if) требует одного нового теста для покрытия этого нового условия;
  • Если вам нужно добавить оператор try/except в свой код, вам понадобится как минимум один новый тест для каждого except оператора;
  • Если вы добавляете в свой код безусловный / неизбежный цикл, вам не нужно писать новый тест, чтобы охватить его;
  • Если вы добавляете в код условный цикл, вам понадобится как минимум один новый тест, один из которых выполняется внутри цикла, а другой - нет.

Вот и все! Не стесняйтесь комментировать другие примеры и сценарии, которые я мог пропустить, и удачного тестирования :)

Заинтересованы в сотрудничестве с технологической командой Loggi?
Мы ищем новых лесорубов для работы в Бразилии и Португалии. Ознакомьтесь с нашими вакансиями здесь!