что такое тестирование белого ящика это тестирование
Тестирование белого ящика и черного ящика
В зависимости от доступа разработчика тестов к исходному коду тестируемой программы различают «тестирование (по стратегии) белого ящика» и «тестирование (по стратегии) чёрного ящика».
При тестировании белого ящика (также говорят — прозрачного ящика), разработчик теста имеет доступ к исходному коду программ и может писать код, который связан с библиотеками тестируемого программного обеспечения. Это типично для модульного тестирования, при котором тестируются только отдельные части системы. Оно обеспечивает то, что компоненты конструкции — работоспособны и устойчивы, до определённой степени. При тестировании белого ящика используются метрики покрытия
кода или мутационное тестирование.При тестировании чёрного ящика, тестировщик имеет доступ к программе только через те же интерфейсы, что и заказчик или пользователь, либо через внешние интерфейсы, позволяющие другому компьютеру либо другому процессу подключиться к системе для тестирования. Например, тестирующий модуль может виртуально нажимать клавиши или кнопки мыши в тестируемой программе с помощью механизма взаимодействия процессов, с уверенностью в том, все ли идёт правильно, что эти события вызывают тот же отклик, что и реальные нажатия клавиш и кнопок мыши.
Как правило, тестирование чёрного ящика ведётся с использованием спецификаций или иных документов, описывающих требования к системе. Обычно в данном виде тестирования критерий покрытия складывается из покрытия структуры входных данных, покрытия требований и покрытия модели (в тестировании на основе моделей).
При тестировании серого ящика разработчик теста имеет доступ к исходному коду, но при непосредственном выполнении тестов доступ к коду, как правило, не требуется.
Если «альфа-» и «бета-тестирование» относятся к стадиям до выпуска продукта (а также, неявно, к объёму тестирующего сообщества и ограничениям на методы тестирования), тестирование «белого ящика» и «чёрного ящика» имеет отношение к способам, которыми тестировщик достигает цели.
Бета-тестирование в целом ограничено техникой чёрного ящика (хотя постоянная часть тестировщиков обычно продолжает тестирование белого ящика параллельно бета-тестированию). Таким образом, термин «бета-тестирование» может указывать на состояние программы (ближе к выпуску чем «альфа»), или может указывать на некоторую группу тестировщиков и процесс, выполняемый этой группой. То есть, тестировщик может продолжать работу по тестированию белого ящика, хотя программа уже «бета-стадии», но в этом случае он не является частью «бета-тестирования».
Тестирование чёрного ящика или поведенческое тестирование
Тестирование чёрного ящика или поведенческое тестирование — стратегия (метод) тестирования функционального поведения объекта (программы, системы) с точки зрения внешнего мира, при котором не используется знание о внутреннем устройстве тестируемого объекта. Под стратегией понимаются систематические методы отбора и создания тестов для тестового набора. Стратегия поведенческого теста исходит из технических требований и их спецификаций.
Тестирование по стратегии белого ящика
Тестирование по стратегии белого ящика — тестирование кода на предмет логики работы программы и корректности её работы с точки зрения компилятора того языка, на котором она писалась.Тестирование по стратегии белого ящика, также называемое техникой тестирования, управляемой логикой программы, позволяет проверить
внутреннюю структуру программы. Исходя из этой стратегии тестировщик получает тестовые данные путем анализа логики работы программы.
Техника Белого ящика включает в себя следующие методы тестирования:
1. покрытие решений
2. покрытие условий
3. покрытие решений и условий
4. комбинаторное покрытие условий
Белый, серый и черный ящик
Существует три подхода к тестированию программного обеспечения: тестирование белого, серого и черного ящиков. Каждый из них рассматривает процесс с иной точки зрения, и не может быть использован как единственный подход. Завершение всех трех этапов гарантирует качество продукта.
Тестирование белого ящика
Главная цель этого процесса – проверка кода, тестирование как внутренней структуры, так и дизайна. Тестировщики могут видеть код на этой стадии, поэтому, этот метод имеет ряд других названий, таких как открытое тестирование или проверка кода. Каждый термин указывает на прозрачный процесс, как и возможность проверки процессов ввода-вывода, основываясь на внутренних алгоритмах работы.
Тестирование белого ящика предполагает поиск и улучшение следующих моментов:
Подтверждение работоспособности системы приложения – цель тестирования белого ящика. Любое расхождение с ожидаемыми результатами может быть классифицировано как баг, нуждающийся в корректировке.
Как осуществляется тестирование белого ящика?
Есть два шага для реализации данного этапа.
Шаг 1. Изучение исходного кода.
Шаг 2. Создание и внедрение алгоритмов проверки
На этом уровне проверяются различные сценарии. Разрабатываются алгоритмы проверки, реализуются с целью выявления слабых мест. Здесь может иметь место и ручное тестирование.
Основной подход – анализ кода. Выборка недоработок в процессе внедрения алгоритмов тестирования. Все алгоритмы запускаются по несколько раз на разных участках кода, помогая изъять возможные проблемные участки.
От тестировщика не требуется выполнения всего процесса вручную. Существует большое количество инструментов, которые помогут автоматизировать рабочий процесс и сохранить время.
Тестирование черного ящика
Во время поведенческого тестирования или тестирования черного ящика, специалист не знает наверняка, что за продукт он тестирует. Внутренняя структура, приложение и дизайн остаются неизвестными для тестировщика. Тесты, как правило, функциональны. Веб-страницы изучаются с использованием браузера, вводом некоторых данных и при анализе полученных данных.
Метод черного ящика применим на следующих уровнях тестирования
Количество методов тестирования зависит от сложности продукта, т.е. ящика.
Подходы к разработке алгоритмов тестирования черного ящика бывают следующие:
Тестирование серого ящика
Подходы к тестированию
Для запуска тестовых случаев во время тестирования серого ящика не обязателен доступ к коду. Каждый тест базируется на знании поведения программы. Это хороший подход к реализации функционального тестирования. Однако, это не будет успешным без реализации более глубоких методов, таких как тестирование белого и черного ящиков.
говориМ о тестировании
простым языком
Виды тестирования по доступу к коду
Почему оба подхода до сих пор существуют? Разве тестирование с доступом к коду не эффективнее? Может существует золотая середина? Давайте разбираться.
В терминологии тестирования фразы «тестирование белого ящика» и «тестирование чёрного ящика» относятся к тому, имеет ли тестировщик доступ к исходному коду тестируемого ПО или нет.
Черный ящик
Разработка тестов методом черного ящика (black box test design technique) — процедура создания и/или выбора тестовых сценариев, основанная на анализе функциональной или нефункциональной спецификации компонента или системы без знания внутренней структуры. (ISTQB)
Тестируемая программа для тестировщика – как черный непрозрачный ящик, содержания которого он не видит.
Основной посыл такого тестирования в том, что мы не знаем, как устроена тестируемая система. Имеется ввиду изнутри. При таком тестировании тестировщик очень похож на обычного пользователя: тест анализ и исследование продукта он проводит опираясь на ТЗ (техническое задание), спецификации и прочую документацию, которая описывает этот продукт.
Получается, что идеи для тестирования идут от предполагаемых паттернов (pattern — образец) поведения пользователей. Поэтому такой подход еще называют поведенческим.
Пример. Заходим в приложение вызова такси и видим возможность привязать карту для автоматической оплаты. Начинаем думать как пользователь:
— Что, если привязать заблокированную карту?
— А если забыть/неверно указать срок действия карты?
— Долларовая карта привяжется?
— А может можно после вызова такси и подачи машины быстро отвязать ее до списания оплаты… Что тогда будет? Спишутся средства? Или водителю придет уведомление, что оплата изменилась с безналичной на наличную?
По сути, мы тестируем и строим предположения на основе того, что видим и рисуем себе в голове. То есть мы только предполагаем, что элементы должны работать таким образом и на основании этого подбираем тесты, но точно не уверены, что это именно так.
Либо открываем спецификацию и смотрим, как система должна работать. Потом запускаем продукт и сверяем его с тем, что указано в спецификации.
Таким образом, мы не имеем представления о структуре и внутреннем устройстве системы. Нужно концентрироваться на том, что программа делает, а не на том, как она это делает.
Белый ящик
Разработка тестов методом белого ящика (white-box test design technique): Процедура разработки или выбора тестовых сценариев на основании анализа внутренней структуры компонента или системы. (ISTQB)
Белый ящик является полной противоположностью черному ящику. При тестировании черного ящика, нам необходимо запускать программу и смотреть, что она делает. А в белом этого не требуется. Достаточно смотреть на код программы.
Основной посыл этого типа тестирования — нам известны все детали реализации тестируемой программы.
Тестирование методом белого ящика (прозрачного, открытого, стеклянного ящика, основанное на коде или структурное тестирование) – метод тестирования программного обеспечения, который предполагает, что внутренняя структура/устройство/реализация системы известны тестировщику.
Мы выбираем входные значения, основываясь на знании кода, который будет их обрабатывать. Так же мы знаем, каким должен быть результат этой обработки. Знание всех особенностей тестируемой программы и ее реализации – обязательны для этой техники. Тестирование белого ящика – углубление во внутреннее устройство системы, за пределы ее внешних интерфейсов.
Профессиональные тестировщики, которые тестируют методами белого ящика, имеют большую экспертизу в программировании, так как должны уметь читать код и находить в нем проблемы.
Серый ящик. Отдельный вид или миф?
Его основной посыл в том, что нам известны только некоторые особенности реализации тестируемой системы.
То есть, внутреннее устройство программы нам известно лишь частично. Предполагается, например, доступ к внутренней структуре и алгоритмам работы ПО для написания максимально эффективных тест-кейсов, но само тестирование проводится с помощью техники черного ящика, то есть, с позиции пользователя.
Эту технику тестирования также называют методом полупрозрачного ящика: что-то мы видим, а что-то – нет.
Кто-то говорит, что этот вид тестирования — это симбиоз белого и черного ящика. Кто-то противопоставляет его белому и черному, опираясь на то, что внутренняя структура тестируемого объекта изначально известна частично и выясняется по мере исследования.
ISTQB относит тестирование методами белого и черного ящика к методам проектирования тестов. Поэтому, ни о каком «среднем» или «промежуточном» методе в этом случае конечно и речи быть не может. Мы либо разрабатываем тесты, зная код, либо не зная его. То есть в классификации ISTQB такого вида тестирования не существует.
Думаю, что на собеседовании это явно стоит упомянуть.
Почему все ящики эффективны?
Методы белого и черного ящика не являются конкурирующими или взаимоисключающими. Наоборот, они гармонично дополняют друг друга, компенсируя имеющиеся недостатки.
Параллельное использование черного и белого ящиков увеличивает покрытие возможных сценариев:
То есть у каждого из методов тестирования свои неоспоримые плюсы, которые помогают выпустить качественный продукт.
White/Black/Grey Box-тестирование
Для того, чтобы лучше понимать подходы к тестированию программного обеспечения, нужно, конечно же, знать, какие виды и типы тестирования в принципе бывают. Давайте начнем с рассмотрения основных типов тестирования, которые определяют высокоуровневую классификацию тестов.
Самым высоким уровнем в иерархии подходов к тестированию будет понятие типа, которое может охватывать сразу несколько смежных техник тестирования. То есть, одному типу тестирования может соответствовать несколько его видов. Рассмотрим, для начала, несколько типов тестирования, которые отличаются знанием внутреннего устройства объекта тестирования.
Black Box
Summary: Мы не знаем, как устроена тестируемая система.
Тестирование методом «черного ящика», также известное как тестирование, основанное на спецификации или тестирование поведения – техника тестирования, основанная на работе исключительно с внешними интерфейсами тестируемой системы.
Согласно ISTQB, тестирование черного ящика – это:
Почему именно «черный ящик»? Тестируемая программа для тестировщика – как черный непрозрачный ящик, содержания которого он не видит. Целью этой техники является поиск ошибок в таких категориях:
Таким образом, мы не имеем представления о структуре и внутреннем устройстве системы. Нужно концентрироваться на том,что программа делает, а не на том, как она это делает.
Пример:
Тестировщик проводит тестирование веб-сайта, не зная особенностей его реализации, используя только предусмотренные разработчиком поля ввода и кнопки. Источник ожидаемого результата – спецификация.
Поскольку это тип тестирования, то он может включать и другие его виды. Тестирование черного ящика может быть как функциональным, так и нефункциональным. Функциональное тестирование предполагает проверку работы функций системы, а нефункциональное – общие характеристики нашей программы.
Техника черного ящика применима на всех уровнях тестирования (от модульного до приемочного), для которых существует спецификация. Например, при осуществлении системного или интеграционного тестирования, требования или функциональная спецификация будут основой для написания тест-кейсов.
Техники тест-дизайна, основанные на использовании черного ящика, включают:
Преимущества:
Недостатки:
Противоположностью техники черного ящика является тестирование методом белого ящика, речь о котором пойдет ниже.
White Box
Summary: Нам известны все детали реализации тестируемой программы.
Тестирование методом белого ящика (также прозрачного, открытого, стеклянного ящика или же основанное на коде или структурное тестирование) – метод тестирования программного обеспечения, который предполагает, что внутренняя структура/устройство/реализация системы известны тестировщику. Мы выбираем входные значения, основываясь на знании кода, который будет их обрабатывать. Точно так же мы знаем, каким должен быть результат этой обработки. Знание всех особенностей тестируемой программы и ее реализации обязательны для этой техники. Тестирование белого ящика – углубление во внутреннее устройство системы за пределы ее внешних интерфейсов.
Согласно ISTQB: тестирование белого ящика – это:
Почему «белый ящик»? Тестируемая программа для тестировщика – прозрачный ящик, содержимое которого он прекрасно видит.
Пример:
Тестировщик, который, как правило, является программистом, изучает реализацию кода поля ввода на веб-странице, определяет все предусмотренные (как правильные, так и неправильные) и не предусмотренные пользовательские вводы и сравнивает фактический результат выполнения программы с ожидаемым. При этом ожидаемый результат определяется именно тем, как должен работать код программы.
Тестирование методом белого ящика похоже на работу механика, который изучает двигатель машины, чтобы понять, почему она не заводится.
Техника белого ящика применима на разных уровнях тестирования: от модульного до системного, но, главным образом, применяется именно для реализации модульного тестирования компонента его автором.
Преимущества:
Недостатки:
Сравнение Black Box и White Box
Grey Box
Summary: Нам известны только некоторые особенности реализации тестируемой системы.
Тестирование методом серого ящика – метод тестирования программного обеспечения, который предполагает комбинацию White Box и Black Box подходов. То есть внутреннее устройство программы нам известно лишь частично. Предполагается, например, доступ ко внутренней структуре и алгоритмам работы ПО для написания максимально эффективных тест-кейсов, но само тестирование проводится с помощью техники черного ящика, то есть с позиции пользователя.
Эту технику тестирования также называют методом полупрозрачного ящика: что-то мы видим, а что-то – нет.
Пример:
Тестировщик изучает код программы с тем, чтобы лучше понимать принципы ее работы и изучить возможные пути ее выполнения. Такое знание поможет написать тест-кейс, который наверняка будет проверять определенную функциональность.
Техника серого ящика применима на разных уровнях тестирования: от модульного до системного, но, главным образом, применяется на интеграционном уровне для проверки взаимодействия разных модулей программы.
Тестирование белого ящика
Разработка программ высокого качества подразумевает, что программа и её части подвергаются тестированию. Классическое модульное (unit) тестирование подразумевает разбиение большой программы на маленькие блоки, удобные для тестов. Либо, если разработка тестов происходит параллельно с разработкой кода или тесты разрабатываются до программы (TDD — test driven development), то программа изначально разрабатыватся небольшими блоками, подходящими под требования тестов.
Одной из разновидностей модульного тестирования можно считать propery-based testing (такой подход реализован, например, в библиотеках QuickCheck, ScalaCheck). Этот подход основан на нахождении универсальных свойств, которые должны быть справедливы для любых входных данных. Например, сериализация с последующей десериализацией должна давать такой же объект. Или, повторная сортировка не должна менять порядок элементов в списке. Для проверки таких универсальных свойств в вышеупомянутых библиотеках поддерживается механизм генерации случайных входных данных. Особенно хорошо такой подход работает для программ, основанных на математических законах, которые служат универсальными свойствами, справедливыми для широкого класса программ. Есть даже библиотека готовых математических свойств — discipline — позволяющая проверить выполнение этих свойств в новых программах (хороший пример повторного использования тестов).
Иногда оказывается, что необходимо протестировать сложную программу, не имея возможности разобрать её на независимо проверяемые части. В таком случае тестируемая программа представляет собой черный белый ящик (белый — потому что мы имеем возможность изучать внутреннее устройство программы).
Под катом описаны несколько подходов к тестированию сложных программ с одним входом с разной степенью сложности (вовлеченности) и разной степенью покрытия.
*В этой статье мы предполагаем, что тестируемую программу можно представить в виде чистой функции без внутреннего состояния. (Некоторые соображения, приведённые далее, можно применять и в том случае, если внутреннее состояние присутствует, но есть возможность сброса этого состояния к фиксированному значению.)
Тестовый стенд (test bench)
Прежде всего, так как тестируется всего одна функция, код вызова которой всегда одинаков, то у нас нет необходимости создавать отдельные unit test’ы. Все такие тесты были бы одинаковыми с точностью до входных данных и проверок. Вполне достаточно в цикле передавать исходные данные ( input ) и проверять результаты ( expectedOutput ). Чтобы в случае обнаружения ошибки можно было идентифицировать проблемный набор тестовых данных, все тестовые данные надо снабдить меткой ( label ). Таким образом, один набор тестовых данных можно представить в виде тройки:
Результат одного прогона можно представить в виде TestCaseResult :
Чтобы упростить прогон всех тестовых данных через тестируемую программу, можно использовать вспомогательную функцию, которая будет вызывать программу для каждого входного значения:
Эта вспомогательная функция вернёт проблемные данные и результаты, которые отличаются от ожидаемых.
Для удобства можно отформатировать результаты тестирования
и выводить отчёт только в случае ошибок:
Подготовка входных данных
В простейшем случае можно вручную создать тестовые данные для проверки программы, записать их напрямую в тестовом коде, и использовать, как продемонстрировано выше. Часто оказывается, что интересные случаи тестовых данных имеют много общего и могут быть представлены как некоторый базовый экземпляр, с небольшими изменениями.
При работе со вложенными неизменяемыми структурами данных большим подспорьем являются линзы, например, из библиотеки Monocle:
Линзы позволяют элегантно «модифицировать» глубоко вложенные части структур данных: Каждая линза представляет собой getter и setter для одного свойства. Линзы можно соединять и получать линзы, «фокусирующиеся» на следующем уровне.
Использование DSL для представления изменений
Далее будем рассматривать формирование тестовых данных путём внесения изменений в некоторый исходный входной объект. Обычно для получения нужного нам тестового объекта требуется внести несколько изменений. При этом весьма полезно в текстовое описание TestCase’а включить перечень изменений:
Тогда мы всегда будем знать, для каких тестовых данных выполняется тестирование.
Чтобы текстовый перечень изменений не расходился с фактическими изменениями, необходимо следовать принципу «единой версии правды». (Если одна и та же информация требуется/используется в нескольких точках, то следует иметь единственный первичный источник уникальной информации, а во все остальные точки использования информация должны распространяться автоматически, с необходимыми преобразованиями. Если этот принцип нарушать, и копировать информацию вручную, то неизбежно расхождение версий информации в разных точках. То есть в описании тестовых данных мы увидем одно, а в тестовые данных — другое. Например, копируя изменение field2 = «456» и корректируя его в field3 = «789» мы можем случайно забыть исправить описание. В итоге описание будет отражать только два изменения из трёх.)
В нашем случае первичным источником информации являются сами изменения, вернее, исходный код программы, которая вносит изменения. Нам хотелось бы вывести из них текст, описывающий изменения. Навскидку, в качестве первого варианта, можно предложить использовать макрос, который будет захватывать исходный код изменений, и использовать исходный код в качестве документации. Это, по-видимому, хороший и относительно несложный способ задокументировать фактические изменения и он вполне может применяться в некоторых случаях. К сожалению, если мы представляем изменения в виде простого текста, мы теряем возможность выполнять осмысленные трансформации перечня изменений. Например, обнаруживать и устранять дублирующиеся или перекрывающиеся изменения, оформлять перечень изменений удобным для конечного пользователя способом.
Чтобы иметь возможность оперировать изменениями, необходимо иметь их структурированную модель. Модель должна быть достаточно выразительной, чтобы описывать все интересующие нас изменения. Частью этой модели, например, будет адресация полей объектов, константы, операции присваивания.
Модель изменений должна позволять решать следующие задачи:
Другим способом формирования экземпляров модели изменений может служить специализированный язык (DSL), создающий объекты моделей изменения с помощью набора extension-методов и вспомогательных операторов. Ну а в простейших случаях экземпляры модели изменений можно создавать непосредственно, через конструкторы.
Язык изменений представляет собой довольно сложную конструкцию, включающую несколько компонентов, которые также, в свою очередь, нетривиальны.
Приведём пример программы, записанной с использованием DSL:
Для представления изменений необходим набор классов того же плана, что и вышеприведённый класс SetProperty :
Интерпретатор языка изменений представляет собой обычный рекурсивный вычислитель выражений, основанный на PatternMatching’е. Что-то наподобие:
Для непосредственного оперирования свойствами объектов необходимо для каждого свойства, используемого в модели изменений, задать getter и setter. Этого можно достичь, заполнив отображение ( Map ) между онтологическими свойствами и соответствующими им линзами.
Такой подход в целом работает, и действительно позволяет описывать изменения один раз, однако постепенно появляется потребность представлять всё более сложные изменения и модель изменений несколько разрастается. Например, если необходимо изменить какое-то свойство с использованием значения другого свойства того же объекта (например, field1 = field2 + 1 ), то возникает необходимость в поддержки переменных на уровне DSL. А если изменение свойства нетривиально, то на уровне DSL потребуется поддержка арифметических выражений и функций.
Тестирование ветвей
Тестируемый код может быть линейным, и тогда нам по большому счёту достаточно одного набора тестовых данных, чтобы понять, работает ли он. В случае наличия ветвления ( if-then-else ), необходимо запускать белый ящик как минимум дважды с разными входными данными, чтобы были исполнены обе ветки. Количество наборов входных данных, достаточных для покрытия всех ветвей, по-видимому, численно равно цикломатической сложности кода с ветвлениями.
Как сформировать все наборы входных данных? Так как мы имеем дело с белым ящиком, то мы можем вычленить условия ветвления и дважды модифицировать входной объект так, чтобы в одном случае выполнялась одна ветвь, в другом случае — другая. Рассмотрим пример:
Имея такое условие, мы можем сформировать два тестовых случая:
(В случае, если один из тестовых сценариев невозможно создать, то можно считать, что обнаружен мертвый код, и условие вместе с соответствующей веткой можно спокойно удалить.)
Если в нескольких ветвлениях проверяются независимые свойства объекта, то можно довольно просто сформировать исчерпывающий набор измененных тестовых объектов, который полностью покрывает все возможные комбинации.
Рассмотрим подробнее механизм, позволяющий сформировать все возможные перечни изменений, обеспечивающие полное покрытие всех ветвлений. Для того, чтобы использовать перечень изменений при тестировании, нам надо все изменения объединить в один объект, который мы подадим на вход тестируемого кода, то есть требуется поддержка композиции. Для этого можно либо воспользоваться вышеприведённым DSL для моделирования изменений, и тогда достаточно простого списка изменений, либо представить одно изменение в виде функции модификации T => T :
тогда цепочка изменений будет представлять собой просто композицию функций:
или, для списка изменений:
Чтобы компактно записать все изменения, соответствующие всем возможным ветвлениям, можно использовать DSL следующего уровня абстракции, моделирующий структуру тестируемого белого ящика:
Здесь коллекция tests содержит агрегированные изменения, соответствующие всем возможным комбинациям ветвей. Параметр типа String будет содержать все названия условий и все описания изменений, из которых сформирована агрегированная функция изменений. А второй элемент пары типа T => T — как раз агрегированная функция изменений, полученная в результате композиции отдельных изменений.
Чтобы получить изменённые объекты, надо все агрегированные функции изменений применить к baseline-объекту:
В результате мы получим коллекцию пар, причем строка будет описывать применённые изменения, а второй элемент пары будет объектом, в котором все эти изменения объединены.
Исходя из структуры модели тестируемого кода в форме дерева, перечни изменений будут представлять собой пути от корня к листам этого дерева. Тем самым значительная часть изменений будет дублироваться. Можно избавиться от этого дублирования, используя вариант DSL, при котором изменения непосредственно применяются к baseline-объекту по мере продвижения по ветвям. В этом случае будет производиться несколько меньше лишних вычислений.
Автоматическое формирование тестовых данных
Так как мы имеем дело с белым ящиком, то можем видеть все ветвления. Это даёт возможность построения модели логики, содержащейся в белом ящике, и использования модели для генерации тестовых данных. В случае, если тестируемый код написан на Scala, можно, например, использовать scalameta для чтения кода, с последующем преобразованием в модель логики. Опять же, как и в рассмотренном ранее вопросе моделирования логики изменений, для нас затруднительно моделирование всех возможностей универсального языка. Далее будем предполагать, что тестируемый код реализован с использованием ограниченного подмножества языка, либо на другом языке или DSL, который изначально ограничен. Это позволяет сосредоточиться на тех аспектах языка, которые представляют для нас интерес.
Рассмотрим пример кода, содержащего единственное ветвление:
Подобным образом можно генерировать данные, подходящие под ограничения, порождаемые простыми условными операторами с константами (больше/меньше константы, входит во множество, начинается с константы). Такие условия нетрудно обратить. Даже если в тестируемом коде вызываются несложные функции, то мы можем заменить их вызов на их определение (inline) и всё-таки осуществить обращение условных выражений.
Трудно обратимые функции
Иначе обстоит дело в том случае, когда в условии используется функция, которую затруднительно обратить. Например, если используется хэш-функция, то автоматически генерировать пример, дающий требуемое значение хэш-кода, по-видимому, не получится.
В таком случае можно добавить во входной объект дополнительный параметр, представляющий результат вычисления функции, заменить вызов функции на обращение к этому параметру, и обновлять этот параметр, невзирая на нарушение функциональной связи:
Дополнительный параметр позволяет обеспечить выполнение кода внутри ветки, но, очевидно, может привести к фактически некорректным результатам. То есть тестируемая программа будет выдавать результаты, которые никогда не могут наблюдаться в реальности. Тем не менее, проверка части кода, которая иначе нам недоступна, всё равно полезна и может рассматриваться как разновидность модульного тестирования. Ведь и при модульном тестировании подфункция вызывается с такими аргументами, которые, возможно, никогда не будут использоваться в программе.
При таких манипуляциях мы заменяем (подменяем) объект тестирования. Тем не менее, в каком-то смысле новая построенная программа включает логику прежней программы. Действительно, если в качестве значений новых искуственных параметров взять результаты вычисления функций, которые мы заменили на параметры, то программа выдаст те же самые результаты. По-видимому, тестирование изменённой программы по-прежнему может представлять интерес. Надо лишь помнить, при каких условиях изменённая программа будет вести себя также, как исходная.
Зависимые условия
Если при формировании уточняющих множеств мы обнаружим, что одно из подмножеств пусто, то это означает, что условие всегда будет принимать фиксированное значение true или false вне зависимости от входных значений. Поэтому соответствующая ветка, которая никогда не вызывается, является «мертвым кодом» и может быть удалена из кода вместе с условием.
Связанные параметры
Рассмотрим случай, когда условие ветвления основано на двух полях объекта, также связанных условиями:
Символьное выполнение
Для того, чтобы собрать все условия, порождающие результат по какой-либо из ветвей, можно воспользоваться «cимвольным выполнением» (Symbolic Execution, Символьное выполнение программ), суть которого заключается в следующем. Входные данные принимаются равными некоторым символьным значениям ( field1 = field1_initial_value ). Затем над символьными значениями производятся все манипуляции, описанные в тестируемом коде. Все манипуляции выполняются в символьном же виде:
Накопленные в символьном виде ограничения можно использовать либо для формирования генератора, порождающего значения, удовлетворяющие этим ограничениям, либо для проверки случайных значений, формируемых менее точным генератором. В любом случае появляется возможность генерировать случайные данные, приводящие к исполнению заранее известного пути (и, возможно, к известному результату).
Тестирование циклов и рекурсивных функций
До сих пор мы обходили вопрос циклов стороной. Связано это с тем, что в цикле меняется состояние, то есть цикл обязательно использует изменяемую переменную. Мы же обозначили границы нашего рассмотрения чистыми функциями, что подразумевает использование только неизменяемых структур данных. Также при наличии циклов существует риск формирования таких условий, при которых результат не будет получен за разумное время.
Известно, что любой цикл можно заменить рекурсией. Это может быть непросто для сложных циклов. Но, допустим, что в нашем случае такая операция была произведена. Тем самым, в тестируемом коде будут встречаться рекурсивные вызовы, а мы можем продолжать наши рассуждения, сохраняя исходное предположение о рассмотрении только чистых функций. Как мы могли бы протестировать такой белый ящик, учитывая тот факт, что рекурсивные вызовы, так же, как и циклы, могут не завершиться за разумное время?
Воспользуемся такой конструкцией как Y-комбинатор («комбинатор неподвижной точки», stackoverflow:What is a Y-combinator? (2-ой ответ), habr: Получение Y-комбинатора в 7 простых шагов). Комбинатор позволяет реализовать рекурсию в языках, которые рекурсию в чистом виде не поддерживают. (Сам комбинатор является рекурсивным, поэтому должен быть реализован на языке, поддерживающем рекурсию.) Работает он следующим образом. Из рекурсивной функции удаляются все рекурсивные вызовы и заменяются на вызовы функции, которая передаётся в качестве дополнительного аргумента. Такая переработанная функция уже не будет являться рекурсивной, а служит только «заготовкой» для получения целевой функции. Y-комбинатор превращает такую «заготовку рекурсивной функции» в полноценную рекурсивную функцию (передавая в качестве аргумента собственное продолжение).
В случае общей рекурсии рекурсивный вызов возвращает результат, который затем используется. В этом случае вышеприведённый подход напрямую не работает. Можно попробовать применить подход, аналогичный тому, что мы использовали для вызовов трудно обратимых функций. А именно, заменим каждый рекурсивный вызов на новый параметр. Значение этих параметров можно будет генерировать как обычно, исходя из условий ветвлений, в которых эти параметры используются. Как и в случае с заменой вызовов функций на параметры, результаты, которые мы будем получать, могут отличаться от результатов, которые мы можем получить в действительности. Совпадение будет достигаться в том случае, если значение параметра совпадает со значением рекурсивной функции. Такой подход позволяет нам протестировать шаги, выполняемые после рекурсивного вызова.
Смысл тестирования белого ящика
При определённом усердии можно добиться того, что тесты, написанные вручную или сгенерированные автоматически, будут покрывать все ветви тестируемого кода, то есть обеспечат 100% покрытие. Тем самым мы сможем с уверенностью сказать, что белый ящик делает то, что он делает. Хм. Секундочку. А в чём, собственно, смысл такого тестирования, спросит внимательный читатель? Ведь для любого содержимого белого ящика будут построены тесты, которые только лишь подтверждают, что белый ящик работает каким-то определённым образом.
В некоторых случаях такой набор тестов всё же может иметь смысл:
Следует иметь в виду некоторые особенности тестирования, основанного на реализации, в отличие от тестирования на основе спецификации. Во-первых, если изначальная реализация не поддерживала некоторую функциональность, которую можно было бы ожидать, основываясь на спецификации, то наши тесты не заметят её отсутствия. Во-вторых, если такая функциональность присутствовала, но работала иначе, чем указано в спецификации (то есть, с ошибками), то наши тесты не просто этих ошибок не обнаружат, а напротив, ошибки будут «кодифицированы» в тестах. И если последующие/альтернативные реализации попробуют исправить ошибки, то такие тесты не позволят этого просто так сделать.
Заключение
Тестирование белого ящика смещает акцент с вопроса «что должен делать код» на «что фактически делает код». Иными словами, вместо использования более высокого уровня абстракции, формирования тестов на основе спецификации, используется точно тот же уровень абстракции, что и при реализации кода. Мы можем получить хорошие результаты в плане покрытия кода, но при этом такое тестирование имеет смысл в ограниченном наборе случаев.
Если вы столкнулись с таким случаем, в котором тестирование белого ящика оправдано, то соображения, приведённые выше, могут пригодиться. Во-первых, основные усилия имеет смысл сосредоточить на формировании тестовых наборов данных, так как вход у белого ящика один (вызов функции), а протестировать хотелось бы все ветви. Во-вторых, по-видимому, имеет смысл построить модель тестируемого кода. Для этого может использоваться специализированный DSL, достаточно выразительный, чтобы представлять тестируемую логику. В-третьих, пользуясь моделью тестируемой логики можно попробовать автоматически сформировать тестовые данные, покрывающие все ветви. В-четвертых, тестируемый код может быть подвергнут автоматическим преобразованиям, которые делают его более удобным для тестирования (исключение вызовов труднообратимых функций, переход от циклов к рекурсии, исключение рекурсивных вызовов). При использовании этих подходов можно получить хорошие результаты в плане покрытия кода.
Таким образом, в благоприятных условиях и при реализации некоторых из вышеприведённых подходов, появляется возможность автоматической генерации содержательных тестов. Возможно, заинтересованные читатели предложат и другие области, где могло бы применяться тестирование белого ящика или какие-либо из рассмотренных подходов.
Благодарности
Хотелось бы поблагодарить @mneychev за терпение и неоценимую помощь при подготовке статьи.