Если каждый путь через программу проверен, гарантирует ли это поиск всех ошибок?
Если нет, то почему? Как вы могли бы пройти через все возможные комбинации программных потоков и не найти проблему, если таковая существует?
Я стесняюсь предположить, что «все ошибки» могут быть найдены, но, может быть, это потому, что охват пути не практичен (так как он комбинаторный), поэтому он никогда не испытывался?
Примечание: эта статья дает краткое изложение типов покрытия, как я думаю о них.
Ответы:
нет
Потому что даже если вы тестируете все возможные пути , вы все равно не проверили их со всеми возможными значениями или всеми возможными комбинациями значений . Например (псевдокод):
источник
В дополнение к ответу Мэйсона , есть еще одна проблема: покрытие не говорит вам, какой код был протестирован, оно говорит вам, какой код был выполнен .
Представьте, что у вас есть тестовый набор со 100% охватом пути. Теперь удалите все утверждения и снова запустите тестовый набор. Вуаля, тестовый пакет по-прежнему имеет 100% охват пути, но он абсолютно ничего не тестирует.
источник
ON ERROR GOTO
это тоже путь, как и Сиif(errno)
.Вот более простой пример для округления. Рассмотрим следующий алгоритм сортировки (на Java):
Теперь давайте проверим:
Теперь предположим, что (A) этот конкретный вызов
sort
возвращает правильный результат, (B) все пути кода были покрыты этим тестом.Но, очевидно, программа на самом деле не сортирует.
Отсюда следует, что охват всех путей кода недостаточен, чтобы гарантировать отсутствие ошибок в программе.
источник
Рассмотрим
abs
функцию, которая возвращает абсолютное значение числа. Вот тест (Python, представьте себе тестовый фреймворк):Эта реализация верна, но она покрывает только 60% кода:
Эта реализация неверна, но она получает 100% покрытие кода:
источник
def abs(x): if x == -3: return 3 else: return 0
Вы могли бы исключитьelse: return 0
часть и получить 100% покрытие, но функция была бы по существу бесполезной, даже если она проходит модульный тест.Еще одно дополнение к ответу Мейсона , поведение программы может зависеть от среды выполнения.
Следующий код содержит Use-After-Free:
Этот код является неопределенным поведением, в зависимости от конфигурации (выпуск | отладка), ОС и компилятора, он будет приводить к различным поведениям. Не только покрытие пути не гарантирует, что вы найдете UAF, но и ваш набор тестов, как правило, не охватывает различные возможные варианты поведения UAF, которые зависят от конфигурации.
С другой стороны, даже если бы охват пути гарантировал обнаружение всех ошибок, маловероятно, что это может быть достигнуто на практике в любой программе. Рассмотрим следующее:
Если ваш набор тестов может сгенерировать все пути для этого, то поздравляем вас с криптографией.
источник
cryptohash
, трудно сказать, что такое «достаточно маленький». Может быть, это займет два дня, чтобы завершить на суперкалькулятор. Но да,int
может оказаться немногоshort
.Из других ответов ясно, что 100% покрытие кода в тестах не означает 100% правильность кода или даже то, что все ошибки, которые могут быть обнаружены при тестировании, будут найдены (не говоря уже об ошибках, которые ни один тест не смог уловить).
Другой способ ответить на этот вопрос один из практики:
В реальном мире и даже на вашем собственном компьютере существует множество программных продуктов, которые разрабатываются с использованием набора тестов, обеспечивающих 100% охват, и в которых все еще есть ошибки, в том числе ошибки, которые можно выявить при более качественном тестировании.
В связи с этим возникает вопрос:
Инструменты покрытия кода помогают определить области, которые никто не проверял. Это может быть хорошо (код явно корректен даже без тестирования), его невозможно решить (по какой-то причине путь не может быть найден), или это может быть расположение большой вонючей ошибки либо сейчас, либо после будущих модификаций.
В некотором смысле проверка правописания сопоставима: что-то может «пройти» проверку правописания и быть написано с ошибками таким образом, чтобы соответствовать слову в словаре. Или это может "потерпеть неудачу", потому что правильных слов нет в словаре. Или это может пройти и быть полной ерундой. Проверка орфографии - это инструмент, который помогает вам определить места, которые вы, возможно, пропустили при чтении корректуры, но так же, как он не может гарантировать полное и правильное чтение корректуры, поэтому охват кода не может гарантировать полное и правильное тестирование.
И, конечно же, неправильный способ использовать проверку орфографии отлично подходит для каждого предложения, которое он предлагает, так что уклонение становится хуже, чем если бы мы оставили это в кредит.
С охватом кода может быть соблазнительно, особенно если у вас почти идеальный 98%, заполнить дела, чтобы найти оставшиеся пути.
Это равносильно исправлению с проверкой орфографии, шить, что это все слова погода или узел, это все подходящие слова. В результате получается беспорядок.
Однако, если вы рассмотрите, какие тесты действительно нужны непокрытым путям, инструмент покрытия кода выполнит свою работу; не в том, чтобы пообещать вам правильность, а в том, чтобы указать на некоторую работу, которую нужно было сделать.
источник
Путь покрытия не может сказать вам, были ли реализованы все необходимые функции. Отсутствие функции является ошибкой, но покрытие пути не обнаружит ее.
источник
Часть проблемы в том , что 100% охват только гарантирует , что код будет корректно работать после одноразового выполнения . Некоторые ошибки, такие как утечки памяти, могут не проявляться или вызывать проблемы после однократного выполнения, но со временем это вызовет проблемы для приложения.
Например, скажем, у вас есть приложение, которое подключается к базе данных. Возможно, в одном методе программист забывает закрыть соединение с базой данных, когда они сделали с их запросом. Вы можете выполнить несколько тестов по этому методу и не найти ошибок с его функциональностью, но ваш сервер базы данных может столкнуться со сценарием, в котором отсутствуют доступные соединения, потому что этот конкретный метод не закрывал соединение, когда это было сделано, и открытые соединения должны сейчас тайм-аут.
источник
times_two(x) = x + 2
, это будет полностью покрыто набором тестовassert(times_two(2) == 4)
, но это все еще очевидно глючный код! Нет необходимости в утечках памяти :)Как уже было сказано, ответ НЕТ.
Помимо того, что говорится, есть ошибки, появляющиеся на разных уровнях, которые нельзя проверить с помощью юнит-тестов. Просто упомянуть несколько:
источник
Что это значит для каждого пути, который будет проверен?
Другие ответы великолепны, но я просто хочу добавить, что условие «каждый путь через программу проверяется» само по себе неопределенно.
Рассмотрим этот метод:
Если вы напишите тест, который утверждает
add(1, 2) == 3
, инструмент покрытия кода скажет вам, что каждая строка выполняется. Но вы на самом деле ничего не утверждали о глобальном побочном эффекте или бесполезном назначении. Эти строки выполнены, но на самом деле не были проверены.Тестирование мутаций поможет найти такие проблемы. Инструмент тестирования мутаций будет иметь список предопределенных способов «изменить» код и посмотреть, пройдут ли еще тесты. Например:
+=
к-=
. Эта мутация не приведет к провалу теста, поэтому она докажет, что ваш тест не имеет ничего значительного в отношении глобального побочного эффекта.По сути, мутационные тесты - это способ проверить ваши тесты . Но точно так же, как вы никогда не будете проверять фактическую функцию со всеми возможными наборами входов, вы никогда не будете запускать каждую возможную мутацию, поэтому, опять же, это ограничено.
Каждый тест, который мы можем сделать, - это эвристика для перехода к программам без ошибок. Ничто не совершенно.
источник
Ну ... да, на самом деле, если каждый путь «через» программу проверен. Но это означает, что каждый возможный путь через все пространство всех возможных состояний, которые может иметь программа, включая все переменные. Даже для очень простой статически скомпилированной программы, скажем, старого средства вычисления чисел Фортрана, это невозможно, хотя, по крайней мере, это можно себе представить: если у вас есть только две целочисленные переменные, вы в основном имеете дело со всеми возможными способами соединения точек на двумерная сетка; это на самом деле очень похоже на коммивояжера. Для n таких переменных вы имеете дело с n- мерным пространством, поэтому для любой реальной программы задача совершенно невыполнима.
Хуже того: для серьезных вещей у вас есть не только фиксированное количество примитивных переменных, но и переменные на лету создаются в вызовах функций или имеют переменные размера ... или что-то в этом роде, насколько это возможно на языке, полном Тьюринга. Это делает пространство состояний бесконечномерным, разрушая все надежды на полное покрытие, даже учитывая невероятно мощное испытательное оборудование.
Тем не менее ... на самом деле все не так уж безрадостно. Это является возможным proove целых программ , чтобы быть правильными, но вам придется отказаться от нескольких идей.
Во-первых, настоятельно рекомендуется перейти на декларативный язык. По какой-то причине императивные языки всегда были самыми популярными, но то, как они смешивают алгоритмы с реальными взаимодействиями, крайне затрудняет даже то, что вы подразумеваете под «правильным».
Гораздо проще в чисто функциональных языках программирования: они имеют четкое различие между действительно интересными свойствами математических функций и нечеткими взаимодействиями в реальном мире, о которых вы не можете ничего сказать. Для функций очень просто указать «правильное поведение»: если для всех возможных входных данных (из типов аргументов) получен соответствующий желаемый результат, то функция ведет себя корректно.
Теперь вы говорите, что это все еще неразрешимо ... в конце концов, пространство всех возможных аргументов в целом также бесконечномерно. Правда - хотя для одной функции даже наивное тестирование покрытия ведет вас гораздо дальше, чем вы могли бы надеяться в императивной программе! Тем не менее, есть невероятно мощный инструмент, который меняет игру: универсальный количественный / параметрический полиморфизм . По сути, это позволяет вам писать функции для очень общих типов данных с гарантией того, что, если он работает для простого примера данных, он будет работать для любого возможного ввода вообще.
По крайней мере, теоретически. Нелегко найти правильные типы, которые на самом деле настолько общие, что вы можете полностью доказать это - обычно вам нужен язык с зависимой типизацией , и их, как правило, довольно сложно использовать. Но написание в функциональном стиле только с параметрическим полиморфизмом уже повышает ваш «уровень безопасности» на огромном уровне - вы не обязательно найдете все ошибки, но вам придется скрывать их достаточно хорошо, чтобы компилятор их не заметил!
источник