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

  Юрий  | 

    | 

  Обновл. 1 мая 2019  | 

 10655

 ǀ   8 

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

Зачем выполнять тестирование?

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

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

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

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

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

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

Учитывая 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: Пишите свою программу по частям: в небольших, чётко определенных единицах (функциях)

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

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

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

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

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

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

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

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

Совет №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 (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, отрицательные и положительные значения. При наличии пользовательского ввода вы также должны проверить вариант возникновения переполнения.

   Для чисел типа с плавающей запятой убедитесь, что вы рассмотрели варианты, как ваша функция обрабатывает значения, которые имеют неточности (значения, которые немного больше/меньше ожидаемых). Хорошие тестовые значения — это 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 (85 оценок, среднее: 4,92 из 5)
Загрузка...

Комментариев: 8

  1. Аватар Алексей:

    За что и люблю RegEX, хоть он и сложный бывает.
    Выдал число — другое попадет, определенные символы — другое не найдет/пройдет (если пользоваться тем же "egrep").

    Мембрана электронная.

  2. Аватар Алексей:

    Так и делаю обычно.
    Все простое — гениальное.

    Проще с малого на большее, не наоборот.
    Частями обычно и программирую, может это другая сфера, то тоже шаги, не бег.

  3. Аватар Giveyn:

    В ответе на тест 2 объясните пожайлуста что такое случай по умолчанию?

  4. Аватар Oleksiy:

    "Совет при тестировании №4…"

    Функция spam(int timesToPrint) должна быть типа void.

  5. Аватар korvell:

    Вопрос: почему в 4 байтах максимальное число 18 квинтилионов?
    Если в 1 байте максимальное число FF, а в 4 — FF'FF'FF'FF, что равно 4 294 967 296

    1. Юрий Юрий:

      Потому что каждое число из диапазона сравнивается со всеми значениями диапазона (1 с 1, 1 с 2, 1 с 3, 1 с 123 и т.д., затем 2 с 1, 2 с 2 и т.д.). И получается что диапазон нужно поделить на диапазон (4 294 967 295 * 4 294 967 295), дальше уже математика.

      1. Аватар korvell:

        понял, спасибо

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

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