Programming
TDD
23 May 2012

Test Driven Design — первый опыт внедрения

Многочисленные статьи, посвященные TDD в общем и Unit-тестам в частности, как правило, оперируют довольно искусственными примерами. Например, давайте напишем функцию, которая складывает два числа и напишем для неё тест. Честно говоря, на таких примерах трудно оценить преимущества использования автоматизированных тестов.

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


Лирическое отступление. О роли тестов


Множество статей написано о полезности использования Unit тестов, но и немало копий сломано в спорах об их необходимости. И, несмотря на то, что я достаточно давно практикую написание и регулярный прогон Unit-тестов, мне долго не удавалось внятно сформулировать мотивы их использования.
Основным доводом против объемлющего автоматизированного тестирования являются дополнительные затраты времени на написание тестов. И действительно, автоматизированное тестирование не заменяет других видов тестирования. Распространен подход: «Пишем программу и отправляем тестерам (а то и сразу заказчику!). Если Когда ошибки будут найдены – исправим их и отправим исправленный вариант, и так далее». И такой подход может быть оправдан в ситуации «сделал и забыл» — когда созданную программу никогда не придется доделывать-переделывать.

Современные гибкие методики разработки предлагают совсем другой подход к созданию программного обеспечения. Внесение изменений в программу рассматривается не как некая гипотетическая и маловероятная возможность, а совершенно обыденная часть работы. Это стимулирует разработчиков к созданию кода, который легче поддается изменениям. Для упрощения внесения изменений в код рекомендуется делать архитектуру приложения слабосвязанной. А для уменьшения скрытых последствий внесения изменений предназначены автоматизированные тесты. Они позволяют обнаружить отклонение программы от ожидаемого поведения на самой ранней стадии внесения изменений.
Некоторое время назад я осознал одну очень простую истину, которая все расставила по своим местам. Не нужно рассматривать тесты как некое дополнение к коду. Тесты – такая же часть кода, как и его архитектура. Тесты – это та часть кода, которая делает его приспособленным к внесению изменений с минимальными последствиями. Чем лучше устроена архитектура – тем легче создавать тесты. Чем лучше организованы тесты, тем меньше скрытых последствий будет после внесения изменений. И тем надежнее и качественнее будет полученный код.

Каждый раз, когда возникает вопрос писать или не писать тест – повторите про себя мантру:
«Тест – это не дополнение к коду, но его важная часть».

Постановка задачи


Итак, поставлена задача: организовать автоматическую передачу файлов между двумя компьютерами по сети.
Уточнение: Файлы, которые появляются в папке на одном компьютере, должны неким магическим образом перенестись в папку на другом компьютере.
– Да это же DropBox! – скажете Вы.
Да, для этих целей можно было бы использовать и облачные хранилища, если бы не несколько но:
1. Файлы слегка конфиденциальны, выкладывать их в облако заказчик стесняется не хочет
2. Передачу файлов нужно подробно протоколировать, чтобы потом принимающая сторона не могла сказать «а был ли мальчик файл?»

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

Проектируем архитектуру


Решаем в лоб

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



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

Стоп-стоп-стоп. А как же TDD? Ну-ка, давайте подумаем, как мы будем тестировать такую программу. А, собственно, способ только один – написать, запустить и посмотреть что получится. И вычищать ошибки по мере обнаружения. Т.е. быстрый и простой способ написания программы приводит нас к «классическому» способу разработки.

А теперь давайте не будем спешить

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



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

Пишем тесты и программу


Классики TDD требуют писать сначала тесты – потом классы. Честно говоря, у меня так не всегда получается – видимо, нужно продолжать медитации. :-) Однако писать рабочий и тестирующий код практически одновременно оказалось, на удивление, несложно. Чаще всего я сначала писал набросок рабочего кода, потом тесты для соответствующего класса или метода, после чего доводил код до рабочего состояния.

В процессе написания тестов я открыл для себя прелесть использования mock-объектов (по совету dorofeevilya я использовал Rhino Mocks) для тестирования поведения классов, зависящих от абстрактных интерфейсов. Вот, для примера, тест, который проверяет, что в случае ошибки передатчик отпишет в протокол соответствующее сообщение.
[TestMethod]
 void test_that_failed_transfer_logs()
{
    //конструируем моки
    var mock = new MockRepository();
    var writerMock = mock.StrictMock<IFileReceiver>();
    var logMock = mock.StrictMock<INoDateLogger>();

    //задаем ожидаемое поведение
    var exception = new InvalidOperationException("transfer failed");
    Expect.Call(() => writerMock.Receive(fileName, fileData)).Throw(exception);
    Expect.Call(() => logMock.WriteEvent(FileTransferEvent.Send, 
                               fileName, FileTransferStatus.Failed, 
                               MessageFormatter.AttemptFailed(1, exception)));

    mock.ReplayAll();

    //инициируем передачу
    var sender = new FileSender (writerMock, logMock);
    sender.SendFile(fileName, fileData);
		
    //проверяем выполнение ожиданий
    mock.VerifyAll();
}


Запускаем программу


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

Усложняем задачу – подключаем веб-сервис. Уходит пара часов на то чтобы развернуть IIS, сконфигурировать и настроить сервис. Еще 25 мин на то чтобы написать и подключить класс, реализующий интерфейс приемника для веб-сервиса. И, как только были решены все инфраструктурные проблемы – программа заработала! Т.е. у меня не было проблем с неожиданным поведением написанных классов в реальных условиях.

И делаем выводы


Скажу честно, результат меня сильно впечатлил. Да, разработка заняла больше времени, чем обычно у меня занимает разработка приложений подобного уровня сложности. Но этап отладки, который обычно занимает заметное время, практически отсутствовал! Увеличение времени разработки я, в первую очередь, связываю с отсутствием у меня опыта написания тестов с использованием mock-ов. Сейчас я делаю следующий проект, используя те же принципы, и написание тестов уже не отнимает у меня столько времени.

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

+28
9.1k 137
Comments 24