Рассмотрите тест, который проходит даже при неверном результате функции. В чём механизм ложноположительного теста?
function discount(price, percent) {
return price - price * percent / 100;
}
test("applies discount", () => {
discount(100, 20);
});
Тест ложноположительный, потому что он вызывает функцию, но не сравнивает результат с ожидаемым значением. Тестовый фреймворк обычно считает такой тест успешным, если во время выполнения не возникло исключение, поэтому функция может вернуть неверный результат, а тест всё равно завершится успешно.
При переходе от ручных проверок к автоматизированным тестам стало важно не только выполнить сценарий, но и формально зафиксировать ожидаемый результат. Для этого появился принцип использования тестового оракула — правила или эталона, с которым сравнивают фактическое поведение системы.
Без оракула автоматизация превращается в запуск кода без проверки корректности. Такой тест может подтверждать только отсутствие исключения, но не соответствие требованиям.
В примере функция должна вернуть 80 для цены 100 и скидки 20%. Однако тест не содержит ни проверки возвращаемого значения, ни другой проверки состояния или побочного эффекта.
Если функция будет возвращать 70, 100, NaN или вообще игнорировать аргументы, тест всё равно пройдёт, пока вызов не завершится исключением. Это создаёт ложное чувство покрытия и позволяет дефекту попасть в рабочую систему.
Тест должен содержать явное утверждение ожидаемого поведения:
Здесь тестовый оракул — значение 80, выведенное из требования: цена после скидки равна исходной цене минус процент скидки. Если функция вернёт другое значение, утверждение завершится ошибкой.
Важно проверять именно значимый результат, а не формальный признак выполнения. Например, проверка expect(actual).toBeDefined() обнаружит только отсутствие undefined, но пропустит неправильное число. Чем слабее утверждение, тем больше дефектов остаётся незамеченными.
Ожидаемое значение не должно бездумно вычисляться тем же алгоритмом, что и фактическое. Если тест повторит ту же ошибку реализации, он может подтвердить неверный результат. Для простых расчётов лучше использовать независимое заранее известное значение, а для сложной логики — отдельную спецификацию, эталонную реализацию или проверку инвариантов.
Проверка отсутствия исключения сама по себе уместна, когда требование состоит именно в том, что операция должна быть допустимой для заданного входа. Но для функции, которая возвращает вычисляемое значение, этого недостаточно: нужно проверить значение, состояние или наблюдаемый побочный эффект.
В интернет-магазине команда добавила автоматический тест расчёта скидки. Сначала тест только вызывал сервис, потому что разработчики хотели убедиться, что новый код не вызывает исключений. После изменения формулы скидка стала рассчитываться от уже уменьшенной цены, но набор тестов продолжил проходить.
Рассматривались два варианта. Можно было оставить проверку отсутствия исключения: она проста и устойчива к изменению конкретных чисел, но почти не защищает бизнес-правило. Второй вариант — проверять точную сумму для нескольких заранее выбранных сценариев: он лучше обнаруживает ошибки, хотя требует поддерживать ожидаемые значения при изменении требований.
Выбрали второй вариант и добавили независимые проверки для обычной скидки, нулевой скидки и максимального допустимого процента. В результате ошибка в формуле была обнаружена до релиза, а тесты стали проверять бизнес-результат, а не только техническую исполнимость вызова.
null или undefined?Нет. Такая проверка подтверждает лишь наличие некоторого результата, но не его корректность. Неверное число, пустая строка или объект с ошибочными полями могут пройти такой тест. Утверждение должно соответствовать проверяемому требованию и контролировать существенные свойства результата.
Одинаковая ошибка может попасть и в продуктивный код, и в тест. Например, если и функция, и тест неправильно применяют процент, фактическое и ожидаемое значения совпадут, хотя оба неверны. Поэтому ожидаемый результат желательно получать из требования, независимого расчёта, эталонных данных или проверяемого инварианта.
Да, но только для узкой цели — например, чтобы проверить отсутствие исключения, успешное завершение операции или совместимость вызова. Однако такая цель должна быть явно сформулирована, а тест не следует выдавать за проверку правильности результата. Для вычислений, изменений состояния и бизнес-правил отсутствие утверждения обычно означает недостаточное покрытие поведения.