В первой части мы рассмотрели примеры тестов, из которых не все одинаково полезны. Здесь попробуем разобраться, что такое качество ПО и какую роль в обеспечении качества играют тесты.





Что делать?


Во-первых, раз тесты предполагается использовать для улучшения качества, надо определиться с критериями качества программного обеспечения. Ведь, если ориентироваться на % покрытия, то ряд вышеприведённых примеров будет разумным решением.


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


В-третьих, понять, как преобразовать требования к ПО в работающий код с использованием уровней абстракции.


В-четвёртых, проанализировать типы ошибок и понять роль тестов/типов в сфере защиты от ошибок. Что разумно требовать от тестов, и чего ожидать не стоит.


В-пятых, понять, какое отношение имеет системный подход к юнит-тестированию.


Качество ПО


Качественная программа — (1) достигает (2) требуемого результата с соблюдением (3) заданных ограничений и (4) минимизирует (5) целевую функцию. Причём ограничения и целевая функция могут относиться не к самой программе, а к процессу её разработки.

Типичные ограничения (3):


  • язык программирования/платформа;
  • набор библиотек;
  • стиль кодирования;
  • ограничения предметной области (например, секретность);
  • производительность (не ниже уровня);
  • способности (знания, навыки) команды;
  • ...

В качестве целевой функции (5) можно рассматривать:


  • финансовые показатели:
    • инвестиции (время/стоимость разработки);
    • стоимость владения;
    • стоимость (потенциальная) последующего развития/расширения функционала;
  • срок готовности ПО;
  • количество строк кода;
  • количество багов в течение опытного периода эксплуатации;
  • производительность (время — деньги);
  • задержки (latency);
  • "степень удовлетворённости";
  • ...

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


Иногда (на мой взгляд, ошибочно) тесты включают в ограничения: "покрытие тестами должно быть не ниже 70%" или "все возвращаемые ошибки должны быть проверены". А иногда (ещё более, на мой взгляд, ошибочно) — в целевую функцию: "код максимально покрыт тестами".


Можно ли выразить требуемый результат (2) в форме приёмочных тестов? Несмотря на общую веру в приёмочные тесты, требуемый результат, вообще говоря, невозможно описать одними лишь тестами. Программа по определению имеет бо́льшую общность, иначе её можно было бы заменить таблицей с параметрами приёмочных тестов.


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


Уровни абстракции


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


В программных системах связь между уровнями абстракции осуществляется непосредственно в исходном коде программы. Функция может служить наглядным примером связи между абстрактным и конкретным. Имя функции используется как короткая запись некоторого вычисления на более высоком уровне абстракции. А тело функции — подробная реализация этого же вычисления на более низком уровне абстракции.


Аналогичное явление возникает при конструировании составных структур данных из структур меньшего размера. Или при объединении данных и методов в объект. Или при создании библиотек, содержащих наборы инструментов для решения классов задач.


Вне программных систем также можно обнаружить уровни абстракции. К примеру, передача сигнала. На некотором уровне абстракции сигнал может иметь семантическую нагрузку. На следующем уровне семантическая нагрузка преобразуется в слова. Далее, при произнесении слова превращаются в звук (который также имеет временну́ю структуру — высота тона, паузы, интонация, n-граммы...). Звук, в свою очередь может быть оцифрован (следующий уровень абстракции), закодирован и передан посредством радиосвязи в виде радиосигнала (низший уровень абстракции).


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


Каким образом качество ПО связано с абстракциями? Абстракции напрямую влияют на следующие характеристики ПО и процесса разработки ПО:


  • приемлемость для команды (либо слишком низкоуровневые абстракции, либо наоборот, слишком сложные);
  • скорость onboarding'а (нестандартные абстракции ещё надо освоить);
  • рекрутинг; (Широко распространённые абстракции освоены многими разработчиками, присутствующими на рынке. Новые абстракции могут прокладывать себе дорогу через фреймворки и библиотеки.)
  • стоимость (скорость/сложность) внесения изменений;
  • защита от ошибок или наоборот, поощрение ошибок;
  • размер кодовой базы;
  • производительность программы;
  • реализуемость (может оказаться, что некоторые сложные вещи невозможно реализовать на низком уровне абстракций за разумное время);
  • и т.п.

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


Системный подход


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


На мой взгляд, подходящий понятийный аппарат даёт системный подход. Разрабатываемая программа, работающая в каком-то окружении, является системой (runtime-системой), состоящей из взаимосвязанных компонентов. Каждый компонент в свою очередь представляется подсистемой, состоящей из компонентов следующего уровня.


Деление программы на системы/компоненты неоднозначно и зависит от точки зрения. Например, ту же программу, при взгляде на исходный код, можно разделить на библиотеки, пакеты, отдельные файлы. Связи между такими компонентами будут представлены через явный или неявный import. В некоторых случаях один и тот же программный код может использоваться и при запуске большой системы в качестве подсистемы, и в качестве системы верхнего уровня. Например, подсистема импорта данных вполне может работать и автономно.


Несмотря на то, что юнит-тесты зачастую организуются вокруг файлов с программным кодом, их задачей является тестирование компонента будущей runtime-системы. Runtime-система обычно представляет собой программный код + конфигурация (т.е. программа, запущенная в определённой конфигурации). Поэтому одной из задач, стоящих перед юнит-тестами, является инициализация используемых компонентов таким образом, чтобы заставить программный код работать похожим образом, как в действующей системе, в тех же режимах.


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


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


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


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


Заключение


В этой части мы возвращаемся к цели тестирования, — обеспечение качества ПО, — и пытаемся понять, какие наши действия в бо́льшей степени влияют на качество. Если во главу угла поставлено качество программного обеспечения, то тесты являются лишь одним из компонентов процесса разработки. Значительно большее влияние на качество могут оказать типы данных.


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


Главными инструментами при разработке сложных программных систем являются абстрагирование и системный подход. Именно они дают возможность из относительно простых средств, предоставляемых языком программирования, конструировать всё то многообразие программ, которое нас окружает. Они же являются ключом к пониманию места тестов с точки зрения качества ПО.


Вся серия заметок:


  1. Примеры тестов.
  2. Что делать?
    1. Качество ПО.
    2. "Прямолинейность" кода.
    3. Классификация ошибок.
    4. Эквивалентность функций.
    5. Применимость тестов.
  3. Исправление примеров.

Комментарии (0)