Урок 73. Введение в тестирование кода

   ⁄ 

 Обновлено 3 Авг 2017

  ⁄   

Итак, вы написали программу, она компилируется, и даже работает! Что дальше?

Есть несколько вариантов. Если вы написали программу лишь бы один раз запустить и забыть, то дальше ничего делать не нужно. Здесь не столь важно, что ваша программа может некорректно работать в некоторых случаях — если при первом запуске она работает, так как вы и ожидали, и если вы дальше запускать и использовать её не планируете, тогда всё — финиш.

Если ваша программа полностью линейна (не имеет условных выражений, т.е. операторов if или switch), не принимает входных данных и выводит правильный результат, тогда всё готово также. В этом случае вы уже протестировали всю программу, запустив ее один раз и сверив результат.

Но если же вы написали программу, которую собираетесь запускать много раз, которая имеет циклы с условными выражениями и принимает пользовательский ввод, тогда здесь уже немного по-другому. Возможно, вы написали функцию, которую хотите повторно использовать в других программах. Возможно, вы даже намереваетесь распространять эту программу для других людей. В таком случае вам действительно нужно будет проверить, как ваша программа работает в самых разных условиях.

Только потому, что она корректно сработала с одними значениями пользовательского ввода – совсем не значит, что она будет работать со всеми другими значениями того же ввода.

Тестирование программного обеспечения (a.k.a. проверка программного обеспечения) — это процесс определения работоспособности программного обеспечение согласно ожиданиям.

Суть тестирования

Прежде чем будем говорить о некоторых практических способах тестирования вашего кода, давайте поговорим о том, почему комплексное тестирование может быть сложным.

Рассмотрим простую программу:

Учитывая 4-байтовый тип int и его диапазон значений, то для тестирования всех возможных значений нам потребуется 18 446 744 073 709 551 616 (~ 18 квинтиллионов) раз запустить эту программу. Ясно, что это абсурд.

Каждый раз, когда мы запрашиваем пользовательский ввод или используем условное выражение в программе — мы увеличиваем в разы количество возможных способов, по которым наша программа может выполняться. Для всех программ, кроме простейших, тестировать каждую комбинацию входных данных, да и еще вручную – как то не логично, вам не кажется?

Сейчас ваша интуиция должна подсказывать вам, что для того, чтобы убедиться в полной работоспособности программы выше не нужно будет её запускать 18 квинтиллионов раз. Вы можете прийти к выводу, что если код выполняется, когда выражение x > y равно true при одной паре значений x и y, то код должен правильно работать и с любыми другими парами x и y, где x > y. Учитывая это, становится очевидным, что для тестирования программы нам потребуется запустить её всего лишь три раза (по одному для каждого кейса: x > y, x < y, x = y), чтобы убедиться, что она работает корректно. Есть и другие трюки, позволяющие упростить процесс тестирования кода.

Про методологии тестирования можно долго рассказывать — на самом деле мы могли бы даже написать об этом целую главу. Но так как эта тема не очень специфическая для C++, то мы будем придерживаться краткого и понятного введения, охватываемого с точки зрения вас как разработчика, который тестирует свой собственной код. В следующих нескольких подзаголовках мы расскажем вам о некоторых практических вещах, о которых вам следует знать.

Неофициальное тестирование кода

Большинство разработчиков проводят неофициальное тестирование, когда пишут свои программы. После написания части кода (функции, класса или какого-либо другого «куска» кода) разработчик пишет некий код для проверки только что добавленной части, и если тест пройден успешно, то разработчик удаляет код этого теста. Например, для следующей функции isLowerVowel() мы можем написать следующий код:

Если вы получите 1 или 0, тогда всё хорошо. Вы знаете, что ваша функция работает, поэтому можно удалить временный тестовый код и продолжить программировать.

Совет при тестировании №1: Пишите свою программу по частям: в небольших, четко определенных единицах (функциях) и часто компилируйте их

Возьмем, к примеру, автопроизводителя, который создает автомобиль. Как вы думаете, что из следующего он делает?

a) Создает (или покупает) и проверяет каждый компонент автомобиля отдельно перед его установкой. Как только компонент успешно проходит проверку — интегрирует его в автомобиль и повторяет проверку, чтобы убедиться, что интеграция прошла успешно. В конце, перед презентацией, проводит генеральный тест работоспособности всего автомобиля.

b) Создает автомобиль из всех компонентов без какой-либо предварительной проверки — за один заход, затем в конце проводит первое и последнее тестирование уже готового автомобиля.

Не кажется вам, что более правильным является вариант a). И все же, большинство начинающих программистов пишут свой код в соответствии с вариантом b)!

В случае b), если какая-либо из частей автомобиля будет работать не правильно, то механику придется провести диагностику всего автомобиля, чтобы определить, что не так — проблема может находится где угодно. Например, автомобиль может не заводиться из-за неисправной свечи зажигания, аккумулятора, топливного насоса или чего-то еще. Это приведет к потере большого количества потраченного впустую времени в попытках точного определения корня проблемы. И если проблема будет найдена, то последствия могут быть катастрофическими — изменение в одной части автомобиля могут привести к «эффекту бабочки» — серьезным изменениям в других частях автомобиля. Например, слишком маленький топливный насос может привести к изменению двигателя, что приведет к реорганизации каркаса автомобиля. В конечном итоге вам придется переделывать огромную часть авто, просто чтобы выправить то, что изначально было небольшой проблемой!

В случае a), автопроизводитель проверяет все детали по мере поступления. Если какой-либо компонент оказался бракованным, то механики сразу понимают, в чем проблема и как её решить. Ничто не интегрируется в автомобиль, пока не будет успешно протестировано. К тому времени, когда они уже соберут весь автомобиль, у них будет разумная уверенность в его работоспособности — в конце концов, все его части были успешно протестированы. Все же возможно, что что-то произошло не так при соединении частей, но по сравнению с вариантом б) — это очень малая вероятность, о которой и не следует серьезно беспокоиться.

Вышеупомянутая аналогия справедлива и для программистов, хотя по некоторым причинам новички часто этого не осознают. Гораздо лучше писать небольшие функции, а затем сразу их компилировать и тестировать. Таким образом, если вы допустили ошибку, вы будете знать, что она находится в небольшом количестве кода, который вы только что написали/изменили. А это в свою очередь означает, что площадь поиска ошибки невелика, и время на отладку будет потрачено намного меньше.

Правило: Часто компилируйте свой код и всегда тестируйте все нетривиальные функции, которые вы пишете.

Совет при тестировании №2: Нацеливайтесь на 100% покрытие кода

Понятие покрытие кода (охват кода) относится к количеству исходного кода программы, которое было задействовано во время тестирования. Есть много разных показателей покрытия кода, но лишь несколько из них стоит упомянуть.

Покрытие стейтментов – это процент стейтментов в вашем коде, которые были задействованы во время выполнения тестирования.

Рассмотрим следующую функцию:

Вызов этой функции как boo(1, 0) даст вам полный охват стейтментов для этой функции, так как выполниться каждая строчка кода.

В случае с функцией isLowerVowel():

Здесь потребуется два вызова для проверки всех стейтментов, так как определить работу стейтментов 2 и 3 в одном вызове функции мы не сможем.

Правило: Убедитесь, что во время тестирования задействованы все стейтменты в вашей функции.

Совет при тестировании № 3: Нацеливайтесь на 100% покрытие ветвлений кода

Покрытие ветвлений кода относится к проценту ветвлений, которые были выполнены в каждом случае (положительном и отрицательном) отдельно. Оператор if имеет два ветвления – случаи true и false (даже если нет оператора else). Оператор switch может иметь много ветвлений.

Предыдущий вызов boo(1, 0) дал нам 100%-ый охват стейтментов и ветвление true. Но это всего лишь 50%-ный охват ветвлений. Нам нужен еще один вызов boo(0, 1), чтобы протестировать вариант false.

В функции isLowerVowel() нужны два вызова (например, isLowerVowel(‘a’) и isLowerVowel(‘q’)), чтобы убедиться в 100%-ном охвате ветвлений (все буквы, которые находятся в switch-е тестировать не обязательно, если сработала одна – сработают и остальные).

Пересмотрим функцию сравнения в примере выше:

Здесь необходимы 3 вызова, чтобы получить 100%-ный охват ветвлений: compare(1,0) проверяет вариант true для первого оператора if. compare(0, 1) проверяет вариант false для первого оператора if и вариант true для второго оператора if (else if). compare(0, 0) проверяет вариант false для второго оператора if и выполняет инструкцию else. Таким образом, мы можем сказать, что эту функцию можно протестировать с помощью всего лишь 3 вызовов (а не 18 квинтиллионов).

Правило: Тестируйте каждый случай ветвления в вашей программе.

Совет при тестировании №4: Нацеливайтесь на 100%-ное покрытие циклов

Покрытие циклов (неофициально называемый «тест 0, 1, 2») сообщает, что если у вас есть цикл в коде, то, чтобы убедиться в его работоспособности, нужно выполнить его итерацию 0, 1 и 2 раза. Если он работает правильно в случае 2 итерации, то должен работать правильно и для всех итераций > 2 (3, 4, 10, 100 и т.д.).

Например:

Чтобы протестировать цикл внутри функции, нам придется вызвать его три раза: spam(0), чтобы проверить случай нулевой итерации, spam(1) для проверки итерации №1 и spam(2) для проверки итерации №2. Если spam(2) работает, тогда и spam(n) будет работать (где n > 2).

Правило: Используйте тест 0, 1, 2 для проверки циклов на корректную работу с разным количеством итераций.

Совет при тестировании № 5: Убедитесь, что вы тестируете разные категории ввода

Когда вы пишете функции, которые принимают параметры или пользовательский ввод, заметьте, что происходит с разными категориями ввода. В этом контексте термин «категория» используется для обозначения набора вводов, имеющих аналогичные характеристики.

Например, если я написал функцию вычисления квадратного корня целого числа, то какие значения имело бы смысл протестировать? Вероятнее всего, вы бы начали с нормальных значений, например с 4. Но также было бы неплохо протестировать и с 0 и с отрицательным числом.

Вот несколько основных рекомендаций по тестированию категорий значений ввода:

для целых чисел убедитесь, что вы рассмотрели варианты, как ваша функция обрабатывает ноль, отрицательные и положительные значения. При наличии пользовательского ввода вы также должны проверить вариант возникновения переполнения;

для чисел типа с плавающей запятой убедитесь, что вы рассмотрели варианты, как ваша функция обрабатывает значения, которые имеют неточности (значения, которые немного больше или меньше ожидаемых). Хорошие тестовые значения – это 0.1 и -0.1 (для проверки чисел, которые немного больше ожидаемых) и 0.6 и -0.6 (для проверки чисел, которые немного меньше ожидаемых);

для строк убедитесь, что вы рассмотрели вариант, как ваша функция обрабатывает пустую строку, строку с допустимыми значениями, с пробелами и строку, содержимое которой одни пробелы.

Правило: Тестируйте различные категории значений ввода, чтобы убедиться, что ваш «кусок» кода правильно их обрабатывает.

Сохранение ваших тестов

Хотя написание тестов и последующее их удаление — достаточно хороший вариант для быстрого и временного тестирования, но для кода, который вы намереваетесь повторно использовать или модифицировать в будущем, может иметь смысл сохранить эти тесты. Например, вместо удаления вашего временного теста, вы можете переместить его в функцию test():

Автоматизация тестирования

Одна из проблем с вышеупомянутой тестовой функцией заключается в том, что вам придется вручную проверять результаты теста. А можно сделать лучше – добавить к тесту правильные ожидаемые результаты, которые должны получиться при успешном тестировании.

Теперь вы можете вызывать test() в любое время и функция сама всё сделает за вас.

Тест

1. Когда вы должны начинать тестировать свой код?

Ответ 1

Сразу, как только написали нетривиальную функцию.

2. Сколько тестов потребуется для следующей функции для минимального подтверждения её работоспособности?

Ответ 2

4 будет достаточно. Один для проверки случаев a/e/i/o/u. Один для проверки случая по умолчанию. Один для тестирования isLowerVowel(‘y’, true). И один для тестирования isLowerVowel(‘y’, false).

Оценить статью:

Звёзд: 1Звёзд: 2Звёзд: 3Звёзд: 4Звёзд: 5 (4 оценок, среднее: 4,25 из 5)
Загрузка...
Поделиться в:
Подписаться на обновления:

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *