Что означает 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?
Мы ищем новых лесорубов для работы в Бразилии и Португалии. Ознакомьтесь с нашими вакансиями здесь!