Послойный (многоуровневый) подход к тестированию и работе с требованиями

(многословный недоработанный черновик)

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

В условиях проектирования по контракту и наличия развитых инструментов развертывания и управления пакетами (что имеем в законченном виде в моем случае), разные части проекта могут запускаться на машинах разработчиков самостоятельно, в отрыве от проекта. Мало того, в условиях активной разработки проекта распределенной командой такая политика развертывания может быть оптимальной. При этом, за редкими исключениями, никаких других способов запуска пакета, кроме как запуска его тестов - не существует. Разработчик запускает тесты и только тесты.

Итак, имеем следующие условия:

  1. Отдельные части проекта развертываются на машинах разработчиков в отрыве от проекта. И это - НЕ универсальные библиотеки компонент, но именно модули конкретного проекта.
  2. В большинстве случаев, единственный способ независимого запуска этих модулей - это запуск их собственных тестов.
  3. Существует развитый, гибкий и расширяемый инструментарий работы с пакетами.
  4. Проектирование и программирование по контракту является общей политикой.
  5. Предполагается наличие выделенной роли архитектора.

В таком случае мы имеем 3 слоя (уровня) тестов.

Уровни тестирования

Юнит-тесты

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

Не суть важно, в действительности. Такая методология не нуждается в формализации.

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

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

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

Т.е. здесь идет речь об истинном инкрементальном юнит-тестировании.

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

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

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

Модульные функциональнальные тесты

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

Опять же, говоря "соответствующее", мы имеем в виду все то, что мы берем извне, и то, что отдаем наружу. Ничего другого под понятием "соответствующее" мы иметь в виду не можем. Таким и должен быть виртуальный, урезанный mock-проект.

Здесь ряд важных моментов:

  • как уже сказано, совершенство и мощь тестового фреймворка играют решающую роль. Иначе - затраты на построение виртуального mock-проекта и собственно написание тестов могут быть слишком велики и не соответствовать тем выгодам, которые мы получаем от написания данного уровня тестов.
  • говоря о "соответствующем" окружении, можно свести его к вполне конкретному и описуемому на бумаге набору характеристик mock-проекта. Требуется: выработка практики написания так их характеристик, незатратная по времени. Требуется решить, КТО пишет эти характеристики. По роли - это скорее ближе к архитектору проекта, чем к разработчику данного пакета.
  • требуется решить, КТО, собственно, пишет данный уровнь тестов. Ведь поскольку это не юнит-тесты, а функциональные, то совсем не обязательно их писать разработчику данного пакета . Он может получить их со стороны.
  • если их пишет не разработчик пакета, то встает вопрос о корректности функциональных тестов. Даже от опечаток никто не застрахован.
  • встает вопрос о степени инкрементальности запуска функциональных тестов. Для тестового фреймворка требуется наличие возможности удобно управлять списком запускаемых тестов, инач е куча проваленных тестов по функционалу, до которого еще далеко в работе, будет мешать анализировать те тесты, которые актуальны для данного этапа разработки.

В целом, с учетом указанных выше требований, как раз этот уровень тестов можно делать тем, что мы называем test-first в самом чистом виде.

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

Интеграционные тесты

По пунктам:
Где:в интегрирующем пакете (policy package). Т.е. только на уровне проекта.
Кто:специалист по тестированию
Как:по техническим спецификациям к проекту. Без оглядки на архитектуру и реализацию.
Когда:да хоть когда. Когда и что успеет. Иногда заранее, иногда с отставанием. Процессы разработки должны быть достаточно гибки, чтобы учитывать возможность отставания этого эт апа от других процессов работы над проектом. Иная ситуация приводит к финансовым потерям.

Итоги по вопросу test-first

Разбивая на слои то, как мы думаем о тестах и как мы их делаем, мы превращаем вопрос "тесты сначала или тесты потом" в несущественный. Степень инкрементальности также не стоит ограничивать правилами. В целом, для как можно большего количества процессов преднамеренно понижается важность вопроса "когда". Первый слой тестирования имеет иденственную цель - помочь в процессе написания кода. Это единственный способ убедиться хотя бы в отсутствии опечаток. Если разработчику пакета достаточно для работы использовать второй слой тестов (функциональные тесты для пакета) - то он вообще волен пропустить первый слой. Третий же слой он даже не контролирует. Направление разработки ("снизу вверх" или "сверху вниз") решается послойно. Граница слоев определяется архитектором, и формально сводится к решению об: 1) разделении функционала на модули-пакеты; 2) очередности написания 2-го слоя тестов для конкретного пакета (т.е. перед кодом или не-важно-когда".)

proof-first

Здесь я буду описывать мой конкретный опыт, связанный с ZTK и BlueBream?. Генерация кода по спецификации сделана у нас как генерация класов-имплементаторов по интерфейсу. Т.е. в качестве спецификации выступает интерфейс. Дополнительно генерируется и юнит тест, проверяющий данную реализацию.

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

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

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

Итоги по вопросу proof-first

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

Резюме

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

Примечание

Работу с требованиями мне не удалось сколько-нибудь затронуть в этой статье. Однако, здесь предполагается, что тестирование и работа с требованиями - плотно связанны. Собственно, test-first и proof-first именно так и предполагают, не так ли? С другой стороны, в работе с требованиями самый первый вопрос - "чьи это требования и для кого?". Соответственно, в решении вопроса о том, КТО делает 2-й слой тестирования, возможно, следует проанализировать граф технических требований: какие требования предьявляют друг к другу разработчики разных частей проекта, и откуда у этих требований "растут ноги" (т.е. зависимости требований). Очевидно лишь то, что этот вопрос "КТО пишет 2-й уровень тестов" не имеет универсального ответа (напомню, что именно 2-й уровень ближе всего к обсуждаемой теме, т.е. к test-first)

Другими словами

Есть предусловия А и Б, и это - печка, от которой я "пляшу". Попытаюсь резюмировать другими словами, о чем шла речь.

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

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

Я не приму у вас модуль, если вам нечем доказать, что он работает, не так ли. Не магическими заклинаниями, в конце концов? Говорится, что эта роль отводится также тому самому 2-му слою. Потому что 3-й слой - это "последний бастион" перед релизом проекта, и он неподконтролен Вам. Им занимаются, в общем случае, тестировщики. И, исходя из условия А, никак мы не можем смешать 2-й и 3-й слои тестов. Да и надо ли? У них разные роли.

Что же насчет 1-го слоя тестов - то еще и еще раз повторю - его задача - только одна. Это - помощь тому, кто пишет код. На практике это выглядит у меня так: в емаксе открыто 2 буфера. В одном - код, в другом - тесты. И консолька с запуском этих тестов. Я пишу эти юнит-тесты (2-й уровень), осознавая, что они - только для меня. Их вообще потом можно удалить.

Тут нужно напомнить условие Б. Есть ли альтернативы 1-му слою тестов? Например, всякие шибко интеллектуальные чекеры кода, встроенные в IDE, кои проверяют синтаксис и семантику всего я вся. Но с ними беда в том, что они никогда не могут угнаться за программистом. Добавь в свой код чуточку мета-программирования - и все эти чекеры сходят с ума.

И я согласен с Вами в том, что хотелось бы вынести весь жесткий код в независимые библиотеки компонент (о чем также указано в посте). Но это - в теории. Это - желаемое. А действительность получается иной. Для того, чтобы обойти эту действительность, предлагается также снизить нагрузку на разработчика на 1-ом слое тестов. Делается это с помощью автоматической генерации тестов и верификации интерфейсов в условиях, кгда применяется design-by-contract. Об этом и говорится во последней части основного текста.

Обсуждение - здесь.