Страницы

пятница, 28 сентября 2012 г.

Эффективное тестирование. Design for testability.

 

Перед тем, как вернутся к CardFlow хотелось бы сделать большое отступление на тему тестов.   Тесты – важнейший элемент развития кода. Быть может, даже более важный, чем важный рабочий код системы. Подушка безопасности, которая предохраняет программы от ошибок программистов. Как сохранить тесты полезными и рабочими? Я полагаю, здесь есть три важные ортогональные концепции:

  • Поддерживаемость. (Maintainability).
  • Детерминизм. (Determinism).
  • Скорость. (Speed)

  Изначально, я хотел сделать этот список нумерованным, и отсортировать по убыванию важности. Но не смог расставить приоритеты (поэтому расставил в порядке убывания количества букв). Каждая из этих концепций чрезвычайно важна, и пренебрежение любой из них делает тесты обузой. Будь я Мартином Фаулером, ввёл бы специальный акроним – MDS, а тесты написанные с соблюдением этих концепций – MDS Complaint .

  Как должен выглядеть тест, чтобы называться MDS-Complaint:

  1. 100% детерминирован (любите отлаживать обрыв ethernet-кабеля через в дебаггере? я тоже не люблю).
  2. Не существует другого теста на тестируемую функциональность. (DRY, DRY, DRY).
  3. Выполняется менее чем за 20 миллисекунд (1000 тестов менее чем за 20 секунд – мой идеал.).
  4. Зеленеет после рефакторинга кода, если функционал не был повреждён (скажи прощай mock’ам). 
  5. Содержит не более 10 строк кода в самом тесте, и не более 10 в setup-методе. (не люблю долго вникать в суть теста).

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

Поддерживаемость.

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

  1. Организация тестов – в первую очередь, расположение. Если вы за 10 секунд не смогли найти нужный вам тест в проекте – что-то не так. Местоположение тестов должно быть интуитивно понятно, и подчинено неким соглашениям.
  2. Логика в тестах – очень страшное зло. Логика в тестах зачастую дублирует логику тестируемого кода, поэтому ошибки будут повторятся, и тест будет зелёным. Это страшно на самом деле. 
  3. Mocks – многие мне не поверят, но я абсолютно убеждён: в 95% случаев тесты с mock-объектами не тестируют ничего, кроме правильности установки ожиданий. Малейший рефакторинг приводит к необходимости вносить изменения в ожидания, причём зачастую в нескольких местах. Хотя система по-прежнему остаётся рабочей. Это чрезвычайно досадная печалька.
  4. Количество тестов. Как и любой другой код, тесты требуют поддержки, рефакторинга, создание дополнительных классов и т.д. Мысль проста – меньше тестов – меньше затрат на поддержку (и выше скорость выполнения).
  5. Имена тестов – Как и для методов, классов, интерфейсов и т.д. лучший комментарий для теста – его имя. Если вы не не можете написать хорошее имя для теста – скорее всего он тестирует слишком много всего сразу. Хорошенько подумайте, прежде чем его написать.

Детерминизм.

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

  1. Изоляция - Состояние системы, доставшееся после предыдущих тестов должно быть очищено, и новый тест работает с чистого листа. Всегда. Что это означает на практике? Возьмите перед тестом новый экземпляр объекта, полностью очистите базу данных, удалите файл и т.д. Исключение можно сделать лишь для объектов, состояние которых не влияет на тесты ( скажем, прочтённая из конфигурации строка подключения) ..
  2. Многопоточность. Асинхронные/многопоточные тесты – это всегда пляски с бубном в целях детерминизма. В таких тестах всегда приходится заниматься либо периодическим опросом, либо играться с примитивами синхронизации. И тот и другой способ сильно бьют по удобству поддержки и скорости выполнения тестов. Правильный вариант – тесты должны быть однопоточные, хотя иногда их сложно написать в таком ключе.  
  3. Удалённые сервисы. - Как минимум раз в неделю такой тест будет падать из-за проблем с сетью или самим удалённым сервисом. Исходя из своей практики, тесты с удалённым сервисом никогда не бывают более чем 90% детерминированы. Поэтому никогда не используйте удалённый сервис. Только заглушку, реализующую   интерфейса адаптера к удалённому сервису.
  4. Текущее время. – Настоящее реальное время в тестах ни к чему, поэтому под рукой всегда надо иметь хорошую реализацию фиктивных часов. Один из таких примеров я демонстрировал ранее.

Скорость.

  1. Многопоточность. Из-за склонности многопоточных тестов к периодическому опросу (polling), тесты часто содержат нечто вроде Thread.Sleep(1000). Целая секунда потерянного на сон потока! Неуж-то в это время нельзя заняться чем-то более полезным? Можно! Не пишите таких тестов. Чуть менее вредительский вариант – использование примитивов синхронизации, вроде ManualResetEvent, но в целом проблемы одинаковы – мы тупим в ожидании.
  2. Удалённые сервисы. Как и говорил ранее – межпроцессные вызовы выполнятся на 4-6 порядков медленнее внутрипроцессных. А хорошее тестирование содержит десятки-сотни вызовов удалённых сервисов. Если один вызов длиться 100мс, и тесты содержат 90 вызовов – то нам предстоит полторы минуты тупить в монитор.  Непозволительно долго и бессмысленно. Хорошая заглушка спасёт нас.
  3. Файловая система. Операции с файловой системой очень просты и понятны, писать для них заглушку не хочется. Но жесткие диски (даже SSD) способны изрядно притормозить процесс тестирования, особенно если в фоне на жёсткий диск записывается еще и фильм из интернета Улыбка Что делать если фильм мешает тестам? Правильно – нафиг такие тесты жесткие диски. Виртуальный диск спасёт и скорость, и фильм!
  4. Базы данных. База данных обычно тоже является удалённым сервисом, но отличается  весьма высокой детерминированностью. Чего нельзя сказать о скорости. Поэтому при выборе БД и технологии доступа к ней стоит присмотреться в возможности перевода БД во внутрипроцессный режим (SQLite, SQL Server Compact, RavenDB). А еще лучше и хранить данные в памяти, как это умеет SQLite. Кроме чистой скорости мы выиграем еще и возможность гонять тесты параллельно, без риска испортить тестам контекст.

Справделивости ради, я не единственный, кто проводил такой анализ. Фаулер тоже писал о проблемах тестирования. К сожалению, написал поздно – я набил все шишки самостоятельно. Зато еще раз убедился в правильности собственных выводов.

Design For Testablity.

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

четверг, 27 сентября 2012 г.

CardFlow. Как выбрать шину сервисов.

Сделать второй шаг к коду оказалось гораздо сложнее. Как и написано во всех agile-книжках, я собирался использовать подход test first. А первый тест подразумевает отправку сообщения в шину, и получение какого-то результата. Здесь поджидала самая большая трудность – выбрать шину. Для начала надо было определиться с  требованиями к идеальной шине:

  • Xcopy-развёртывание.
  • Возможность работы во внутрипроцессном режиме.
  • Поддержка контейнеров (забегая вперёд - их поддерживают все шины).
  • Простая конфигурация. Идеально – без конфигурации.
  • Не требующий администрирования транспорт.

А теперь посмотрим, как им соответствуют кандидаты. Выбор (к счастью) не так уж велик, и рассмотреть стоит всего трёх кандидатов:

  • NServiceBus – идеальный вариант для рабочих .net систем. Поддерживает все модные концепции сообщения, команды, события, саги, контейнеры и всё-всё-всё.   Документация в идеальном состоянии – хорошо структурирована, описывает все аспекты, всё по делу. Но есть пара ложек дёгтя. Во-первых, транспорт MSMQ – для рабочих система пойдёт, но настраивать MSDTC в учебных целях совсем не хочется.  Во-вторых – требует лицензии. Конечно можно добыть express-лицензию, но… это ведь дополнительные усилия. А усилия я экономлю. Улыбка
  • MassTransit – тоже близка к идеалу, но… в качестве транспорта может использовать только MSMQ или RabbitMQ. Про MSQM я уже писал, а RabbitMQ требует установки отдельной среды выполнения. Есть и специальный тестовый транспорт – Loopback, но на реале не попробуешь. Документация неплоха, хотя до NServiceBus не дотягивает. Если бы не транспорт – это мой выбор. Может быть свободное время, попробую написать транспорт на Rhino.Queues.
  • Rhino Service Bus – документация откровенно куцая, информации вообще немного, и ту надо собирать по крупицам. Для быстро понимания советую прочитать пару статей Oren Eine, а потом почитать wiki на github’e про изменения в версии 2.0. В качестве транспорта MSMQ и Rhino.Queues. Последняя не требует никаких административных действий, и сразу готова к работе. И это решающий плюс. Ах да, есть один минус в карму – использует противный log4net, но это я могу пережить.

MassTransit отвергнут. В учебных целях не хочется возиться с администрированием. Если бы у меня была настоящая рабочая система – я бы разорился на NServiceBus. А пока снова возьмусь за Rhino.ServiceBus.

четверг, 6 сентября 2012 г.

CardFlow. Первый шаг к коду.

  Каждый .NET разработчик знает правило формирования имен – “Компания.Проект.Сборка”. Чаще всего это название идёт напрямую в имя сборки. Получается нечто подобное:

image

Два проекта – это еще ничего. Но когда их число подходит к 10 искать в этом нагромождении букв нужный проект уже сложно и долго. Инструменты вроде Resharper’а конечно спасают ситуацию быстрыми клавишами навигации. Но всё равно, периодически надо ковырять свойство конкретного проекта приходиться искать его глазами. Это раздражает. К счастью, бесполезное сочетание  “Компания.Проект” можно смело выкинуть на помойку:

image

Однако имена сборок всё равно надо сделать соответствующими шаблону “Компания.Проект.Сборка”. Проще пареной репы, просто пропишите их в свойствах проекта:

image

Есть и еще одна полезная мелочь. По-умолчанию, студия задаёт выходные папки (bin) проектов в подпапке каждого проекта. В результате, в процессе сборки проекта мегабайты сборок могут копироваться туда-сюда, сильно замедляя процесс. Кроме того, напрасно тратиться дисковое пространство. Это звучит глупо по современным меркам HDD, но я использую виртуальный диск, поэтому проблема свободного места для меня по прежнему актуальна. Замените Output path по-умолчанию на “..\bin” в Release и Debug конфигурации проектов, и жизнь станет немного легче:

image

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