Каталог статей.


Разработка программного обеспечения через тестирование.

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

источник статьи 


Допустим, что мы придерживались трех простых правил:

1. Не писать код, пока не будет написан автономный тест, который не проходит.

2. Не писать автономный тест большего объема, чем необходимо для неудачного завершения или неудачной компиляции.

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


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


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


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


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


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


Пример проектирования начиная с тестов


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

Одним из первых, написанных для этой программы тестов, был testMove (листинг 1). Этот метод создавал новый объект WumpusGame, соединял залы 4 и 5 проходом в восточном направлении, помещал игрока в зал 4, выдавал команду перемещения на восток, а затем проверял, находится ли игрок в зале 5.


Изображение листинга №1.


Весь этот код был написан до того, как я приступил к кодированию класса WumpusGame. Я последовал совету знакомого и написал тест так, как следует читать. Я полагал, что смогу обеспечить успешное прохождение этого теста, написав код в соответствии со структурой, подразумеваемой тестом. Такой подход называется ментальным программированием (Intentional programming). Вы должны выразить свои намерения в тесте еще до его реализации, сделав их максимально простыми и удобочитаемыми. Идея в том, что простота и ясность будут способствовать написанию программы с хорошей структурой.


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


Это может показаться противоречащим здравому смыслу. Ведь вся программа построена на залах: мы переходим из зала в зал, выясняем, что находится в зале, и т. д. Быть может, вытекающий из моих намерений дизайн порочен, раз он не содержит класса Room?


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


Обратите внимание, что тест рассказывает о том, как работает программа. Исходя из этой простой спецификации, было бы несложно написать все четыре упомянутых в тесте метода класса WumpusGame. Без особого напряжения можно было бы поименовать и написать команды перемещения в других направлениях. Если впоследствии мы захотим узнать, как соединить два зала или переместиться в определенном направлении, то тест даст недвусмысленные ответы на эти вопросы. Этот тест выступает в роли компилируемого и исполняемого документа, описывающего программу.


Изоляция тестов


Написание тестов до кода нередко выявляет части программы, которые нуждаются в разъединении. Например, на рис. 1 приведена простая UML-диаграмма приложения для расчета заработной платы. Класс Payroll использует класс EmployeeDatabase для получения объекта Employee, затем просит Employee вычислить свою зарплату, передает величину зарплаты объекту CheckWriter, чтобы тот выписал чек, и напоследок передает сумму платежа объекту Employee и записывает объект обратно в базу данных.


Рисунок №1.


Предположим, что весь этот код еще не написан. Пока это всего лишь диаграмма, нарисованная на доске в ходе эскизного проектирования. Теперь необходимо написать тесты, описывающие поведение объекта Payroll. Но при попытке сделать это возникает ряд проблем. Во-первых, какую СУБД мы будем использовать? Ведь приложение должно читать данные из некоторой базы. Необходимо ли иметь полнофункциональную базу данных перед тем, как тестировать класс данные в нее загрузить? Во-вторых, как проверить, что напечатан правильный чек? Не можем же мы написать автоматизированный тест, который будет смотреть на напечатанный принтером чек и сверять проставленную сумму!


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


Такая структура показана на рис. 2. Теперь класс Payroll применяет интерфейсы для взаимодействия с EmployeeDatabase, CheckWriter и Employee. Для реализации интерфейсов было создано три объекта-имитации. Объект PayrollTest опрашивает их, чтобы удостовериться в том, что Payroll управляет ими правильно.


Рисунок №2.


В листинге 2 отражено намерение теста. Мы создаем подходящие объекты-имитации, передаем их объекту Payroll, требуем, чтобы объект Payroll начислил зарплату всем работникам, а затем просим имитации проверить, что все чеки правильно выписаны, а платежи занесены и зарегистрированы.


Конечно, этот тест просто проверяет, что Payroll вызвал все нужные методы, передав им правильные данные. Тест не контролирует правильность выписки чеков и обновления настоящей базы данных. Но он проверяет, что класс Payroll в изоляции от всего остального ведет себя, как положено.


Изображение листинга №2.


Может возникнуть вопрос, зачем нужен класс MockEmployee. Вроде бы можно воспользоваться настоящим классом Employee вместо имитации. Будь это так, я бы и воспользовался без зазрения совести. Но в данном случае я предположил, что класс Employee сложнее, чем необходимо для проверки работы Payroll.


Разделение в придачу


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


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


Приемочные тесты


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

системы. Приемочные тесты - это черные ящики, которые проверяют, удовлетворены ли требования заказчика.


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


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


Заодно акт предварительного написания приемочных тестов оказывает существенное влияние на архитектуру. Чтобы система поддавалась тестированию, связи следует разрывать на верхних уровнях архитектуры. Например, пользовательский интерфейс следует отделить от бизнес-правил, так чтобы приемочные тесты могли получить доступ к бизнес-правилам, минуя ГИП.


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


Снова рассмотрим приложение для расчета зарплаты. На первой итерации должна быть реализована возможность добавлять и удалять записи о работниках в базе данных. Мы также должны уметь создавать платежные чеки для имеющихся в базе данных работников. К счастью, нам придется иметь дело только с работниками на твердом окладе. Работников, тарифицируемых иначе, мы отложим до более поздней итерации.


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


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


На рис. 3 показан пример приемочного теста, написанного в FitNesse. На первом шаге теста в систему добавляются два работника. На втором шаге им начисляется зарплата. На третьем шаге проверяется, что чеки выписаны правильно. Мы предполагаем, что подоходный налог начисляется по плоской шкале и составляет 20%.


Рисунок №3.


Очевидно, что заказчику очень удобно читать и писать такие тесты. Но вдумайтесь, какие это влечет последствия для структуры системы. Первые две таблицы теста - это функции приложения для расчета зарплаты. Если бы мы писали такое приложение в виде повторно используемого каркаса, то они соответствовали бы функциям API. И действительно, чтобы FitNesse могла вызвать эти функции, такой API должен быть написан.


Архитектура в придачу


Отметим то давление, которое приемочные тесты оказывают на архитектуру всей системы для расчета зарплаты. С самого начала задумавшись о приемочных тестах, мы пришли к необходимости API для функций системы. Понятно, что ГИП сможет использовать этот API для достижения своих целей. Отметим также, что печать чеков следует отделить от функции Create Paychecks (Создать чеки). И то и другое - хорошие архитектурные решения.


Заключение


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

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


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