Краткое содержание
Программа, которая компилируется и выводит что-то правдоподобное, от этого ещё не протестирована. Этот урок учит тестировать методично: выводить тестовые случаи из спецификации, по областям и границам; собирать их в программу автоматических тестов, которая подсчитывает неудачи и сообщает о них; утверждать с помощью assert то, что не может произойти, если программа корректна, а с помощью static_assert, при компиляции, то, что должна вычислять функция constexpr; и запускать тесты с санитайзерами, которые обнаруживают неопределённое поведение, скрытое за правильным результатом. Всё опирается на одно предостережение: тест может показать наличие ошибки, но никогда её отсутствие.
Цели обучения
- Выводить тестовые случаи из спецификации функции: разбиения её входных данных, границы между ними и особые случаи, с результатом, которого постусловие требует в каждом из них.
- Писать программу автоматических тестов, которая сравнивает полученные результаты с ожидаемыми, сообщает о каждой неудаче, не останавливаясь, и завершается сводкой и кодом возврата, который её отражает.
- Использовать
assertдля контроля предусловий и инвариантов и объяснять, почему он исчезает при компиляции с-DNDEBUGи почему его условие не должно иметь побочных эффектов. - Использовать
static_assertс функциямиconstexpr, чтобы тестировать результаты при компиляции, и запускать тесты с санитайзерами, чтобы обнаруживать неопределённое поведение, которого результаты не выявляют.
Что значит тестировать
Тестировать программу значит запускать её на выбранных входных данных и сравнивать то, что она делает, с тем, что она должна делать. Каждый тестовый случай состоит из двух частей: входных данных, удовлетворяющих предусловию, и результата, которого постусловие требует для них, вычисленного заранее и другим путём (вручную, по другой формуле, по известной таблице). Без ожидаемого результата нет теста, есть лишь запуск: посмотреть на вывод и найти его «правдоподобным» значит получить впечатление, а не провести сравнение.
Однако функция с одним аргументом int допускает более четырёх миллиардов входных значений, а функция с двумя аргументами, около пар; никакой набор тестов не перебирает их все. Если это так, тесты исследуют ничтожную часть возможных случаев, и из того, что все они пройдены, не следует, что функция корректна. Эдсгер Дейкстра сформулировал это в 1970 году: тесты могут показать наличие ошибок, но никогда их отсутствие. Это то же различие, которое курс «Математическая логика I» проводит между перебором отдельных случаев и доказательством: убедиться, что формула истинна для некоторых значений, не значит доказать, что она истинна для всех, тогда как доказательство (в этом курсе, инвариант урока Проектирование правильных циклов) охватывает все случаи сразу. Тесты не заменяют этого рассуждения: они находят ошибки, которые оно упустило.
Отсюда следует критерий выбора случаев. Если тесты не могут подтвердить корректность, их ценность в том, чтобы находить ошибки, и хороший тестовый случай тот, у которого больше шансов не пройти. Случай, который автор выбрал, потому что знал, что тот сработает, ничего не сообщает. Следующая программа показывает это на функции, которая должна возвращать наибольший элемент непустого вектора.
#include <print>#include <vector>// Наибольший элемент списка данных.// Предусловие: values не пуст.// Постусловие: возвращает наибольший элемент values.int largest(const std::vector<int>& values){ int best{0}; for (const int v : values) { if (v > best) { best = v; } } return best;}int main(){ std::println("{}", largest({3, 9, 4})); std::println("{}", largest({7})); std::println("{}", largest({1, 2, 3, 4})); std::println("{}", largest({-5, -2, -8}));}9740Первые три случая любой пишет в первую очередь, и функция их проходит: 9, 7 и 4 верны. Четвёртый даёт 0, значение, которого даже нет в векторе, тогда как должен был дать . Ошибка в строке 9: best начинается с 0, а не с первого элемента, так что функция корректна, только если какой-нибудь элемент положителен или равен нулю. Типичные случаи разделяли это свойство, хотя никто его не выбирал; четвёртый нарушает его намеренно. Исправление: int best{values[0]};, что предусловие делает законным, потому что гарантирует существование values[0].
Загрузка содержимого…