Я не могу указать на хороший онлайн-ресурс (статьи в английской Википедии по этим темам, как правило, нереальны), но я могу подвести итог услышанной лекции, которая также охватывала основы теории тестирования.
Режимы тестирования
Существуют разные классы тестов, такие как юнит-тесты или интеграционные тесты . В модульном тесте утверждается, что согласованный кусок кода (функция, класс, модуль), взятый сам по себе, работает, как и ожидалось, тогда как интеграционный тест утверждает, что несколько таких кусков правильно работают вместе.
Тестовый пример - это известная среда, в которой исполняется часть кода, например, с использованием конкретного тестового ввода или путем насмешки над другими классами. Поведение кода затем сравнивается с ожидаемым поведением, например, с конкретным возвращаемым значением.
Тест может доказать только наличие ошибки, но не отсутствие всех ошибок. Тесты устанавливают верхнюю границу правильности программы.
Покрытие кода
Чтобы определить метрики покрытия кода, исходный код может быть преобразован в граф потока управления, где каждый узел содержит линейный сегмент кода. Управление передается между этими узлами только в конце каждого блока и всегда является условным (если условие, то переход к узлу A, иначе переход к узлу B). Граф имеет один начальный узел и один конечный узел.
- На этом графике охват операторов - это отношение всех посещенных узлов ко всем узлам. Полное покрытие заявления недостаточно для тщательного тестирования.
- Покрытие ветви - это отношение всех посещенных ребер между узлами в CFG ко всем ребрам. Это недостаточно проверяет петли.
- Покрытие пути - это отношение всех посещенных путей ко всем путям, где путь - это любая последовательность ребер от начального до конечного узла. Проблема состоит в том, что с циклами может быть бесконечное число путей, поэтому полное покрытие пути практически невозможно проверить.
Поэтому часто полезно проверять покрытие условий .
- В простом покрытии условий каждое атомарное условие когда-то истинно, а когда-то ложно, но это не гарантирует полное покрытие выписки.
- При многократном покрытии условий атомные условия принимают все комбинации
true
и false
. Это подразумевает полное покрытие филиала, но довольно дорого. Программа может иметь дополнительные ограничения, которые исключают определенные комбинации. Этот метод хорош для получения покрытия ветви, может найти мертвый код, но не может найти ошибки, возникающие из-за неправильного состояния.
- В минимальном множественном покрытии условий каждое атомарное и составное условие однажды имеет значение true и false. Это все еще подразумевает полное покрытие филиала. Это подмножество нескольких условий покрытия, но требует меньше тестовых случаев.
При построении тестового ввода с использованием покрытия условий следует учитывать короткое замыкание. Например,
function foo(A, B) {
if (A && B) x()
else y()
}
должен быть проверен с foo(false, whatever)
, foo(true, false)
и foo(true, true)
для полного минимального покрытия нескольких условий.
Если у вас есть объекты, которые могут находиться в нескольких состояниях, то тестирование всех переходов состояний, аналогичных потокам управления, представляется целесообразным.
Есть несколько более сложных метрик покрытия, но они в целом похожи на метрики, представленные здесь.
Это методы тестирования белого ящика , которые могут быть частично автоматизированы. Обратите внимание, что набор модульных тестов должен стремиться обеспечить высокий охват кода любой выбранной метрикой, но 100% не всегда возможно. Особенно сложно протестировать обработку исключений, когда ошибки должны быть введены в определенные места.
Функциональные тесты
Затем существуют функциональные тесты, которые утверждают, что код придерживается спецификации, рассматривая реализацию как черный ящик. Такие тесты полезны как для модульных, так и для интеграционных тестов. Поскольку невозможно выполнить тестирование со всеми возможными входными данными (например, тестирование длины строки со всеми возможными строками), полезно сгруппировать входные данные (и выходные данные) в эквивалентные классы - если length("foo")
это правильно, foo("bar")
вероятно, будет работать также. Для каждой возможной комбинации между эквивалентными входными и выходными классами выбирается и тестируется как минимум один представительный вход.
Нужно дополнительно проверить
- крайние случаи
length("")
, foo("x")
, length(longer_than_INT_MAX)
,
- значения, которые разрешены языком, но не контрактом функции
length(null)
, и
- возможные нежелательные данные
length("null byte in \x00 the middle")
...
Для чисел это означает тестирование 0, ±1, ±x, MAX, MIN, ±∞, NaN
, а для сравнений с плавающей запятой - два соседних с плавающей точкой. В качестве еще одного дополнения, случайные значения теста могут быть выбраны из классов эквивалентности. Чтобы облегчить отладку, стоит записать использованное семя…
Нефункциональные тесты: нагрузочные тесты, стресс-тесты
К программному обеспечению предъявляются нефункциональные требования, которые также должны быть проверены. К ним относятся испытания на определенных границах (нагрузочные тесты) и за их пределами (стресс-тесты). Для компьютерной игры это может быть утверждение минимального количества кадров в секунду в нагрузочном тесте. Веб-сайт может быть подвергнут стресс-тестированию для определения времени отклика, когда серверы подвергаются удвоенному количеству посетителей. Такие тесты актуальны не только для целых систем, но и для отдельных объектов - как хеш-таблица ухудшается с миллионом записей?
Другие виды тестов - это тесты всей системы, в которых моделируются сценарии, или приемочные тесты, подтверждающие выполнение контракта на разработку.
Не тестирующие методы
Отзывы
Существуют не тестирующие методы, которые можно использовать для обеспечения качества. Примерами являются пошаговые руководства, официальные обзоры кода или парное программирование. Хотя некоторые детали могут быть автоматизированы (например, с помощью линтеров), они обычно требуют много времени. Однако обзоры кода опытными программистами имеют высокую скорость обнаружения ошибок и особенно ценны при разработке, где автоматическое тестирование невозможно.
Когда обзоры кода настолько хороши, почему мы до сих пор пишем тесты? Большим преимуществом наборов тестов является то, что они могут работать (в основном) автоматически и поэтому очень полезны для регрессионных тестов .
Формальная проверка
Формальная проверка идет и доказывает определенные свойства кода. Ручная проверка в основном жизнеспособна для критических частей, в меньшей степени для целых программ. Доказательства ставят нижнюю границу правильности программы. Доказательства могут быть в определенной степени автоматизированы, например, с помощью статической проверки типов.
Определенные инварианты могут быть явно проверены с помощью assert
операторов.
Все эти методы имеют свое место и дополняют друг друга. TDD пишет функциональные тесты заранее, но тесты могут оцениваться по их метрикам покрытия после реализации кода.
Написание тестируемого кода означает написание небольших блоков кода, которые можно тестировать отдельно (вспомогательные функции с подходящей степенью детализации, принцип единой ответственности). Чем меньше аргументов принимает каждая функция, тем лучше. Такой код также пригоден для вставки фиктивных объектов, например, путем внедрения зависимостей.
double pihole(double value) { return (value - Math.PI) / (value - Math.PI); }
который я узнал от своего учителя математики . Этот код имеет ровно одну дыру , которая не может быть обнаружена автоматически при тестировании черного ящика. В математике нет такой дыры. В исчислении вы можете закрыть дыру, если односторонние ограничения равны.