Истории из практики TDD
March 12, 2007 | Posted by Denis under Практики, Статьи |
У меня часто создается впечатление, что Test Driven Development это что то магическое. Достаточно произнести эти слова – и мир становится лучше без всяких усилий.
Конечно, юнит тестирование работает. Я сам энергично доказываю разработчикам и управленцам эффективность TDD. Но в какой то момент у меня сложилось ощущение, что я сам себя гипнотизирую.
Я решил разобраться: чего больше в моем “TDD оптимизме” – успешного опыта применения или самовнушения. Для этого я проделал “постмортем” больших TDD инициатив в нашем проекте.
Описанные истории значительно отличаются от примеров из известных мне книг по agile техникам. Они сложней, затрагиваемые проблемы выходят за рамки строго TDD. Но я думаю, тем они и интересны. В каждой из историй я расскажу про мотивацию внедрения TDD, реализацию юнит-тестирования и наступившие последствия.
Введение поддержки новой платформы баз данных
Долгое время мы находились русле технологий Microsoft. Но сэйлзы захотели выйти на новых клиентов, и было принято решение об интеграции с платформой Oracle.
В команде не было специалистов по Oracle. Было принято решение не нанимать Oracle-разработчиков, так как после проведения основных работ по портированию Oracle задач будет недостаточно для полной загрузки узкого специалиста. Вместо этого мы решили аутсорсить задачу. Предполагалось, что спецы правильно и эффективно проведут портирование – а потом и наши разработчики навыки Oracle подтянут.
Ежедневное интегрирование результатов работы аутсорсера – довольно неприятная задача. Аутсорсер плохо знает продукт и, скорее всего, билд был рушился каждый день. Постоянные ревью нашими разработчиком съедали бы время нашей команды, которого и так не хватало.
Тут нас озарила идея. Пусть аутсорсеры контролируют себя сами! Мы создали утилиту, которая автоматически сравнивала идентичность работы хранимых процедур и на MSSQL и на Oracle. Утилита состояла из C++ приложения и описаний тестов на VBScript’e. Скрипт готовил тестовые данные в БД, вызывал хранимые процедуры с разными параметрами и задавал условия сравнения resultset’ов.
Мы выдали аутсорсеру схему нашей MSSQL БД, утилиту тестирования и сказали, что необходимо портировать все хранимые процедуры на Oracle так, чтобы они работали строго идентично MSSQL версии. «Нет проблем» – сказал аутсорсер.
Очень скоро мы столкнулись с проблемами:
- Неконтролируемый рост затрат на саму утилиту тестирования, множество мелочей, в которых различались MSSQL и Oracle, требовали постоянных доработок алгоритмов сравнения.
- Тесты, которые делались аутсорсером, были низкого качества. Бизнес-логика сложных хранимых процедур не проверялась.
- Вместо двух мест для с синхронного изменения схем данных (MSSQL и Oracle) стало три. Обновление схем в тестах постоянно запаздывало.
- Но самой большой проблемой оказалось то, что мы не заметили, как внесли в архитектуру системы очень неудачное решение – сведение MSSQL и Oracle к общему интерфейсу на уровне самих БД. Простой пример для иллюстрации: MSSQL и Oracle возвращают даты в разных форматах. Мы стали принудительно приводить эти даты к общему виду с помощью функций внутри хранимых процедур. А надо было проводить это преобразование в коде сервера приложений.
В результате мы превратил SQL код в спагетти. Аутсорсер не виноват – у него не было общей картины. Однако формально TDD выполнило свою задачу, ведь портирование было произведено и ошибок было допущено немного. И все-таки цена оказалась слишком высока.
В итоге мы отказались от Unit Tests уровня баз данных. Вместо этого мы прогоняем тесты на уровне сервера приложений дважды: с подключением к MSSQL и Oracle. И не используем специальных утилит.
Оптимизация производительности сервера
У продукта появились первые крупные потребители. В их сценариях количество одновременно открытых сессий достигает тысячи.
Наш продукт, конечно, не справился с такой нагрузкой. Ошибки в архитектуре лишили систему масштабируемости, а потому увеличение производительности оборудования ничего не давало.
Мы приступили к оптимизации кода практически на всех уровнях системы. Чтобы видеть свой прогресс, понадобилось решение, способное создавать стресс-нагрузку. У нас уже использовались программы для автоматизированного функционального тестирования клиентских приложений. Но применить их на большом количество клиентов не удалось – не хватало ресурсов.
Выходом стало написание собственного инструмента для тестирования. Наша программа состояла С++ приложения, которое запускало множество рабочих потоков, в которых выполнялся VBScript. Скрипту предоставлялся COM объект с реализацией клиента нашего транспортного протокола.
Предполагалось следующее использование утилиты:
- Серверные вызовы описываются скриптом. Тесты покрывают весь интерфейс сервера. Этим достигается regression testing.
- Оптимизации связанные с производительностью сервера производятся с использованием утилиты, а не реального клиента. Это позволяет значительно ускорить поиск оптимальных решений.
- Все ошибки клиент-серверных взаимодействий описываются тестерами в скриптах
В целом, все это оказалось наивными фантазиями:
- Написание инструмента значительно затянулось:код клиентского транспортного уровня оказался не приспособленным для многопоточного использования.
- Итоговая производительность, число рабочих тредов оказались на порядок ниже ожидаемых
- Программирование тестов скриптами затянулось, т.к. была предпринята попытка повторить клиентскую бизнес-логику. Бизнес-логика оказалась достаточно сложной и все неудобства разработок на скриптах вылезли наружу
- Идея описывать баги скриптами была оценена самими тестерами как неумная шутка
В итоге стало ясно, что создание собственной утилиты было неэффективным решением. Но на тот момент у нас не было выбора, так как мы использовали собственный транспортный протокол. Чуть позже утилита была заброшена.
В новых проектах мы перешли на стандартный HTTP и используем для стресс тестирования MS ACT. Да и пропала охота смешивать юнит тесты и стресс тестирование.
Интеграция модуля авторизации с LDAP сервером
Заказчики захотели интеграцию с LDAP. Первое знакомство с администрированием Active Directory и LDAP API повергло нас в уныние – у нас не было соответствующего опыта. Утешая себя словами про «ограниченность интеграции» мы не заметили, как внесли в требования к системе «сканирование всех изменений с пользователями в Active Directory и отображение этих изменений в нашу БД». Мы поставили себе мат еще до начала партии!
Значительные сложности были вызваны большим и развесистым API, смеси C и C++ интерфейсов. Очень много возможностей совершить утечку памяти. Ограниченный контроль целостности данных в Active Directory.
Для юнит тестирования были использован VBScript. Скрипт создавал данные и генерировал события в AD, а код на C++ вызывал эти скрипты и проводил тестирование продукционного кода.
Благодаря UT код пережил три глобальных рефакторинга без регрессии. Использование VBScript’a для создания тестовых данных оправдало себя. Но постоянное нахождение все новых сценариев, в которых наша система ошибалась при определении изменений в AD – вот что стало главной проблемой. Увы, TDD никак не защитило от ошибок при планировании, оценки технических рисков, неверных архитектурных решений.
У истории TDD в интеграции с AD хороший конец. Был совершен переход на новую платформу (.net). Первыми были портированы тесты а затем и продукционный код. Конечно, от смеси C++VBScript мы отказались. Результат удовлетворителен: ни один дефект в компоненте пока не найден.
Заключение
Конечно, эти постмортемы не позволяют вывести абсолютные алгоритмы принятия решения о том как и где использовать TDD.
Описанные истории серьезно повлияли на архитектуру наших новых проектов. Нестандартные протоколы были преданы анафеме. Применение множества IDE и для разных языков стало считаться злом. Не пытаемся привлечь тестировщиков к юнит тестированию.
Есть ли у Вас свои истории сложностей в применении TDD? Давайте обсудим. Накопление подобной базы знаний серьезно повысит эффективность применения TDD.
-
Vitaliy Zhuravskiy