Страницы

четверг, 22 ноября 2012 г.

CardFlow. Приёмочный тест.

Любую разработку стоит начинать с приёмочного теста. Чем приёмочный тест отличается от модульных тестов? В первую очередь приёмочный тест – это тестирование всей функциональности системы с точки зрения конечного пользователя. Скажем, если вы нажали кнопку добавить в корзину – то необходимо проверить содержимое корзины на соответствие добавленному товару. Пользователь – это не обязательно человек. Если речь идёт про RESTful-сервис – то пользователь это другой сервис. Важная особенность – тестируют по принципу чёрного ящика, все действия с сервисом только через внешние интерфейсы или внешнее API. Хотя при этом надо использовать заглушки на внешние системы – лишняя недетерминированность ни к чему. Кроме того, заглушки могут эмулировать особые случаи поведения внешних систем (ошибки, разрывы связи и т.д).

Обычно приёмочные тесты пишутся на языке Gerkin. С этим же языком и работает SpecFlow, с помощью которого я и собираюсь писать тест. Для тех кто не знаком со SpecFlow, рекомендую посмотреть примеры. Кода немного, и можно быстро понять что к чему, даже не читая документацию.

Первый тест лучше писать для  минимальной реализации, постепенно докручивая в нём фишки. В моём случае это простейшее действие – создание новой Kanban-доски с указанными параметрами. У меня получилось так:

Как начать реализовывать систему – расскажу в следующий раз.

PS. Намучившись с форматированием кода решил окончательно переехать на gist’ы от github. Получается гораздо быстрее, и чище исходник блога.

воскресенье, 18 ноября 2012 г.

CardFlow. EventStore.

По жизненным обстоятельствам пришлось временно оставить профессию блоггера, но к счастью у меня оно снова есть. За это время утекло много воды, пришло много интересных идей (о которых вечно не хватает времени написать), я стал счастливым обладателем Windows 8 (честно счастливым, но об этом в другом раз). А еще пришло понимание, что свободного времени много не будет. Всё это приводит меня к неизбежности мысли уменьшить масштаб учебного проекта – оставить только создание и редактирование Kanban-досок.

В прошлый раз я остановился на поиске шины для приложения. Пришло время сделать еще один выбора – Event Store.  Но выбор был простым:

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

Что это значит? Для тестов жизненно необходимы три вещи Arrange, Act, Assert. Все шага должны выполняться быстро. Очень быстро. В вопросе хранения данных это означает возможность запустить хранилище в том же процессе. Совсем идеально хранить данные в памяти. Такой режим есть например в RavenDB. Да даже Microsoft озаботилась тестированием, и сотворила IIS Express для тестирования. В случае Грега Янга мы имеем только отличный сервер, что для быстрых тестов совершенно не годиться.

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

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

В следующий раз расскажу про самое главное, с чего следует начинать разработку – приёмочный тест.

пятница, 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

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