Thursday, October 30, 2008

Архитектура StackOverflow.com

Сегодня по дороге на работу/с работы прослушал подкаст Hanseminutes #134 о том, как Jeff Atwood, Joel Spolsky и его команда создали сайт StackOverflow.com. Пара слов о сайте: это с одной стороны типичный Q&A сайт-сообщество, а с другой – немного другая песня. Джефф и Джоэль провели достаточно серьезный анализ похожих сайтов и вроде как постарались сделать максимально удобный и продвинутый ресурс, ориентированный на IT-специалистов. Не могу судить о том, успешен он или нет, но вроде бы за месяц у них уже много пользователей.

В этом подкасте не было бы ничего особенного, если бы не техническое обсуждение особенностей реализации сайта. Во-первых, сайт написан с использованием ASP.NET MVC, что уже примечательно для меня, т.к. я последнюю неделю занимался его изучением. И это несмотря на то, что ASP.NET MVC еще только недавно вышел в бете. То есть это один из первых достаточно популярных сайтов, которые используют эту технологию. Во-вторых, несмотря на то, что это сайт, предполагающий высокую нагрузку, они использовали Linq to SQL в качестве ORM. Также они сами признаются, что им пришлось напрячься, чтобы реализовать output-кеширование разных разделов страницы (некоторые куски страницы обновляются раз в минуту, некоторые – чаще). Еще у них были большие проблемы с локами и дедлоками в MS SQL сервере и они были вынуждены перейти на “read committed snapshot”-транзакции (как это сделать в Linq to SQL и Entity Framework, можно почитать здесь). И это еще не все. Они использовали GZIP-сжатие страниц, причем не только для уменьшения размера страницы, но и для уменьшения размера страницы в кеше. И им действительно удалось добиться быстрой работы сайта на серьезных нагрузках. Можете проверить сами.

Однако это была лишь присказка :) Сказка начинается теперь. Угадайте, какое у них железо, сколько у них серверов? 4? 8? Больше? ОДИН! Один сервер, на котором крутятся и web-приложение, и база данных. 8-ядерный процессор, 4 Гб ОЗУ, 64-битная архитектура (процессор, ОС, .NET Framework, MS SQL сервер). Ничего особенного. В ответ на это Скотт говорит что-то вроде «это же не best practice, как вообще можно так делать?». И ему отвечают: «ну, мы перейдем со временем на ДВА сервера... когда будет нужно :)». И в конце добавляют контрольный выстрел в голову: их build-сервер, построенный на CC.NET, работает на ТОМ же production-сервере. Это какой-то ужас с точки зрения развертывания приложений, но это работает! :)

Со всем этим они умудряются делать ежедневные релизы новой версии, прогоняя ее на unit-тестах при каждом билде (обычный CI). Это значит, что у них получается нормальная стабильная версия, готовая к выходу в люди, каждый день. Думаю, нам всем есть чему поучиться в плане написания качественного софта, не говоря уже про performance на таком железе :) Да, нагрузка у них пока еще не слишком большая, но все же уже не детская. Я знаю продукты, на которых сайт лежал уже на десятке пользователей :)

А вы говорите, различные facebook’и работают на десятках, а то и сотнях серверов? Ну-ну :)

PS. Да, там уже доступна вторая серия этого общения. Еще не успел послушать, но если будет что-то интересное, сделаю update.

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

Upd2. Если кому-то интересно подробнее почитать про то, как рождался сайт StackOverflow.com, кому пришла в голову идея, сколько людей и как долго его писали, какие есть идеи его монетизации и сколько стоит обслуживание сайта в месяц - Джоэль рассказывает об этом в своей статье "How Hard Could It Be?: The Unproven Path"

Saturday, October 25, 2008

IT-отрасль и финансовый кризис

Сейчас о финансовом кризисе не говорит и не пишет разве что ленивый. Мы тут с другом тоже на досуге поразмыслили над тем, что несет финансовый кризис индустрии разработки ПО в целом и аутсорсингу ПО в частности.

Disclaimer. Эти размышления не претендуют на истину в последней инстанции, более того, они касаются в основном рынка разработки ПО города Харькова, хотя, думаю, их можно аппроксимировать как минимум на всю Украину, а, возможно, даже и на все постсоветское пространство.

Итак, вернемся к теме. С большой долей вероятности можно считать, что «золотой век русского программиста» если не закончился, то остановился. Я не скажу за всю Одессу, но в Харькове уже сейчас заметны некоторые отрицательные тенденции. Часть клиентов останавливает рост своих команд, некоторые компании, которые раньше брали людей на работу толпами, уже в открытую говорят о возможных сокращениях штата.

Причины и следствия

Попробуем проанализировать ситуацию. Финансовый кризис, прежде всего, вызван кризисом инвестиций. На западе очень частой является ситуация, когда у одних людей (разработчиков) есть идея стартапа, а у других – деньги (венчурные капиталисты и другие инвесторы). Раньше инвесторы позволяли себе роскошь выделять средства на рискованные проекты (даже после краха доткомов), теперь же с этим будет туго. Если вы заняты разработкой подобного стартапа, будьте готовы к тому, что на проекте может быть урезан бюджет или он может быть вообще закрыт (это, в частности, про меня). В тех же ситуациях, когда компания заказывает проект для себя, для автоматизации своего производства или дальнейшей продажи, можно ожидать всякого. При отсутствии финансов компании в первую очередь урезают всякие дополнительные расходы, к которым зачастую относят и разработку ПО, если, конечно, компания не зарабатывает продажей этого ПО. Немаловажно здесь и состояние компании-клиента и область ее бизнеса. Некоторые отрасли будут более подвержены кризису, чем другие. То есть вашему проекту может повезти, а может и не очень. В то же время, финансовый кризис может подстегнуть рост аутсорсинга, потому что западные и дальневосточные компании будут вынуждены уменьшать расходы, как и 8 лет назад, когда крах доткомов и 11 сентября дали толчок быстрому развитию аутсорсинга. Подробнее об этом можно почитать здесь и здесь.

Чем это грозит простым труженикам клавиатуры и мышки?

Теперь посмотрим на то, чем это грозит всем нам. На мой взгляд, потенциально возможны как минимум 2 этапа перед тем, как ситуация снова вернется в старую колею: 1) остановка развития и 2) деградация. Заденут ли нас оба этапа или ситуация успеет нормализироваться до наступления второго – это очень интересный вопрос.

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

Второй этап будет намного хуже, чем первый. И начнется он, если, не дай Бог, клиенты начнут банкротиться или сворачивать проекты. Судя по тому, что аналитики прогнозируют пик кризиса на начало 2009 года, это может начаться именно тогда. А может и не начаться – я не специалист, чтобы ответить на этот вопрос. Однако, чисто гипотетически, ЕСЛИ такое произойдет, то всем нам это грозит двумя вещами: 1) начнутся массовые увольнения вследствие закрытия целых проектов, которые увеличат предложение на рынке, при этом спрос будет почти равен нулю, 2) будут понижения зарплат, если уж не для текущих сотрудников, то, по крайней мере, для тех, кого принимают на работу. Средние зарплаты начнут падать.

Так что, о постоянных существенных повышениях зарплат (за 4 года, которые я слежу за рынком, средние зарплаты в аутсорсовых компаниях повысились в 4-8 раза) и легких переходах с места на место можно забыть. Кроме того, возможно обострение противоречий между сотрудниками компаний и их владельцами. Понять можно и тех, и других: обеим сторонам будут нужны деньги, чтобы выжить. Вот только для первых это означает увеличение зарплаты, а для вторых - уменьшение расходов, в том числе и на зарплату. Если вам повезло, и вы нашли хорошее место работы с достойной зарплатой – good for you. Возможно, нас всех пронесет на этот раз, а возможно, и нет, и мы получим еще один 2000-2002 год, когда зарплаты держались на уровне 200-400 долларов, а новых сотрудников предпочитали не нанимать.

Как пережить смутные времена?

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

Быть эффективным и полезным – это значит не просто делать свою работу, но делать ее быстро, качественно, ответственно и, что самое главное, добиваться положительного результата. Про эффективность хорошо написал Сергей Розовик. Если вы привыкли часть рабочего времени посвящать социальным сетям или башоргу – пора отучиться от этой вредной привычки. Если вы привыкли работать расслабленно, спустя рукава – почитайте про «7 навыков высокоэффективных людей», статьи о лайфхакинге и других способах повышения личной производительности и эффективности. Если у вас есть проблемы на проекте – надо задуматься об их причинах и устранить их.

Быть перспективным – это значит не просто обладать потенциалом, но использовать и повышать его. Если вы уже забыли, когда последний раз читали книги или статьи по программированию (менеджменту, тестированию, администрированию, нужное подчеркнуть) – впору заняться самообразованием. IT – это не та отрасль, где можно выйти из университета с полученным багажом знаний и годами его использовать в работе, здесь нужно развиваться. Почитайте про новые технологии, языки программирования, подходы к разработке и методологии ведения проектов. Даже если вам это не понадобиться сегодня, вам это может срочно понадобиться завтра при приеме на новую работу.

Ну, и последний совет. Финансовый кризис ударит по нам не только со стороны работы, но и стороны нашей повседневной жизни. Проанализируйте ситуацию и советы аналитиков о том, что нужно делать с лежащими на депозитах деньгами, счетами, кредитами. Запасайтесь картошкой :) Отучите себя наконец-то брать потребительские кредиты! А если нужен кредит на машину/квартиру, то лучше повременить. Еще о стратегиях выживания во время кризиса можно почитать у Макса Крайнова, а можно найти и другие – благо их сейчас полно.

Какие будут результаты

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

На локальном же уровне в нашей отрасли наверняка произойдут следующие вещи:

1) Произойдет естественный отбор, количество компаний на рынке сократиться, исчезнут самые слабые и неприспособленные

2) Выжившие компании сбросят лишний вес, который им мешал развиваться дальше, станут более гибкими и устойчивыми (почитайте, например, книгу Луиса Герстнера «Кто сказал что слоны не умеют танцевать?» о том, как IBM вышла из крутого пике в начале 90-х годов)

3) Рынок IT-специалистов стабилизируется, зарплаты перестанут расти неимоверными скачками, а сами специалисты станут умнее и профессиональнее.

4) Вполне возможно, что государство наконец-то обратит свое внимание на IT-сферу и у нас наконец-то появиться нормальные законы, регулирующие рынок, упрощающие развитие и улучшающие конкурентоспособность наших компаний (как в Индии, например)

Еще немного о кризисе понятными словами можно прочитать здесь:

Понятно о кризисе
Экономический кризис на пальцах
Прививка от идиотизма

Еще немного о пользе unit-тестирования

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

1. Unit-тесты позволяют найти баги в существующем коде

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

2. Зачастую unit-тесты позволяют быстрее и проще тестировать код, чем это можно сделать через UI

Столкнувшись с ситуацией, описанной в предыдущем примере, я начал задавать себе вопрос: почему баги не были найдены раньше, и получил достаточно простой ответ: сделать это через UI очень сложно. Наши QA, как и программисты, которые писали этот код, не виноваты – просто есть определенная степень сложности в системе, после которой и первые, и вторые просто физически не могут проверить все варианты в установленные сжатые сроки. Например, система прав учитывает с одной стороны иерархию пользователей и ролей, а с другой – иерархию объектов. Обе иерархии имеют свои правила наследования прав, которые влияют на получение окончательного результата. Вычисления очень похожи: они учитывают как иерархические связи внутри иерархии, так и связи с другими иерархиями. При этом через UI это все просто невозможно протестировать полностью в резонное время! Вот здесь-то на помощь и приходят unit-тесты. Быстро воссоздав иерархии объектов и связи между ними в тестах, мы легко нашли и обезвредили толпу багов.

3. Unit-тесты позволяют почувствовать душок, исходящий от вашего кода

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

Я не буду уже писать о том, как написанные unit-тесты помогали нам после изменения существующего кода (допустим, добавления кеширования) – здесь их польза и так ясна как день :)

Sunday, October 19, 2008

Happy Birthday!

В ежедневной рутине прошедшей недели как-то совсем пропустил момент, что у данного блога 15 октября был день рождения. Да, нам уже годик, так что принимаются поздравления :)

Что хочется сказать по этому поводу? Прежде всего то, что я доволен прошедшим годом. В общей сложности было написано 32 сообщения, что сравнительно немного, но в то же время надеюсь, что содержимое этих сообщений было небессмысленно и полезно вам. То, что это было полезно мне самому – без сомнения. Во-первых, это возможность делится своим опытом с единомышленниками, обсуждать что-то, помогать другим. Во-вторых, это способ сингулировать свои знания и опыт в одном месте и не забыть что-то важное. В-третьих, блог – это еще и средство самовыражения, причем не самое плохое, не говоря уже о том, что это в какой-то степени визитная карточка специалиста. Ну, и в-четвертых, иногда так хочется чем-то поделиться, что нет возможности не написать – это единственный выход дать волю своим чувствам и эмоциям, поделиться с окружающими :)

За этот год сформировалась определенная тематика блога: это не чистый технический блог, но в то же время и не дневник. Я стараюсь писать около 80% постов об IT и программировании, так как это основное назначение блога, но в то же время разбавляю эти сообщения рассказами о путешествиях и походах, в которых я побывал, а также фотографиями разной направленности. Думаю, такой подход имеет право на жизнь. Кроме того, я стараюсь писать о том, с чем сталкивался сам, делиться своим опытом. Принципиально стараюсь не писать сообщений типа «Сегодня вышла технология такая-то, в ней есть такой вот переведенный мной с MSDN список фич».

Также где-то около недели назад я настроил Google Analytics для блога (сделать это можно для любого сайта, а как это сделать для blogspot’а, можно прочитать здесь). Жаль, конечно, что я этого не сделал раньше (урок №1), но все же уже даже по данным за эту неделю можно сделать определенные выводы о том, кто посещает блог, откуда, зачем и в каких количествах.

Абсолютное большинство посетителей – пользователи из России и Украины, что понятно. За недельку несколько человек заглянули из Испании и Норвегии, но, скорее всего они испугались, закрыли окошко и пошли дальше :) Хотя один человек оставил комментарий на английском в теме об Entity Framework rollback, но я его увидел слишком поздно. Урок №2 – не забывайте настраивать оповещение о комментариях на почту, если не хотите пропустить что-то важное.

По данным Google Reader, на блог сейчас подписано около 20 человек, что не очень много, за неделю же пришло 60 уникальных пользователей. Еще один вывод: люди приходят на блог, но при этом редко добавляют его в ридеры. Так что если вы иногда читаете блог, но еще не добавили его в ридер – сделайте это, мне будет приятно :) Конечно, если он того стоит.

Дальше о браузерах. Думаю, поклонникам Firefox будет приятно узнать, что по данным моего блога, его используют больше 50% пользователей (ура, товарищи!), IE вместе с Оперой делят почетное второе место с 17%, а Хромом пользуются уже почти 9% (ага, попались, гуглолюбы :)). Есть даже 1% SeaMonkey (это еще что за зверь такой?), а вот Сафари пока не было (хотя что им делать на блоге, посвященном технологиям Microsoft).

Распределение по операционным системам очень простое: 96% за Windows, 4% пользуются Linux и Mac (причем, Firefox).

61% посетителей приходит на сайт из поисковиков (70% - Google, 30% - Yandex), 23% - с других сайтов (спасибо Паше Подлипенскому, Наде Плаховой и каким-то неизвестным блогам в Blogger), и 16% - напрямую вбивая адрес в поисковике.

Ну, и самое интересное – зачем же люди сюда идут? Судя по поисковым запросам, довольно много посетителей приходит, чтобы почитать про Entity Framework, второй по популярности темой оказались unit-тесты, что не может не радовать, далее с большим отставанием остальные темы. Понравилось, что люди ищут навыки программиста, mind maps, подкасты для программистов и даже слова для песни «Щедрик» :)

Еще несколько выводов за этот год: почти никто не комментирует сообщения о путешествиях, но раз друзья присылают сообщения в аську, что им нравятся отчеты, то буду продолжать в том же духе. Ну, и урок №3 – длинные технические посты обычно не комментируются вообще, а вот маленькие с размышлениями на разные темы – за милую душу :)

И напоследок небольшой хит-парад постов.

Самые обсуждаемые:
Entity Framework: Vote Of Confidence (8)
Оценка уровня программиста (8)
Entity Framework + NAnt - трудности с включением файлов модели в сборку (6)
Наш ответ Scott'у Hanselman'у (5)
Аутсорсинг ПО = утечка мозгов (5)
Планирование, учитывающее предыдущие результаты (5)

Полезные технические (с моей точки зрения):
Continuous Tech Development (полезно всем, кто хочет профессионально развиваться и в то же время тратить на это минимум свободного времени)
Оценка уровня программиста (о том, какие критерии оценки я вынес для себя)
Mind maps и анализ требований (mind maps – интересная техника, советую всем)
Entity Framework rollback: Часть 4. Пример приложения (советую все 4 части тем, кто хочет разобраться в том, как работает EF внутри)
OutOfMemoryException и как его побороть (многие друзья написали, что круто, но сложно :) – невзирая на это, советую всем, кто хочет разобраться с тем, как отлаживать сложные ситуации)
Введение в mock-объекты. Классификация (must-read для всех, кто хочет понять, что такое mock-объекты и как их правильно использовать)
Unit-тестирование для Entity Framework (название говорит само за себя)

Интересные нетехнические посты:
Хамар-Дабан, пик Черского (отчет о походе на пик Черского, что возле Байкала)
Байкал (описание Байкала)
Иркутск и панорамы из путешествия на Байкал (описание Иркутска и панорамы)
Путешествие «Щедрика» (о том, как малоизвестная украинская мелодия стала одной из самых популярных в мире, а мы об этом и не знаем)
Фильм "Бесценный доллар" (интересный, хотя и спорный фильм об экономике Штатов и мировой экономике)

Итак, итоги года подведены. Спасибо всем, кто читает этот блог! Если у вас есть советы, критика или комментарии по его улучшению – поделитесь со мной :) Я же, со своей стороны, постараюсь писать больше и интереснее.

Sunday, October 12, 2008

Unit-тестирование для Entity Framework

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

Если вы сейчас погуглите по этой теме, то найдете мало ответов на простой вопрос: как заставить unit-тесты работать с Entity Framework. Думаю, есть несколько вариантов решения, но я бы хотел поделиться нашим. Его преимущество в том, что он не использует базу. Можно долго спорить на тему того, как нужно тестировать Data Access Layer, чтобы код лез в базу или не лез туда. У варианта с использованием базы есть как свои достоинства, так и недостатки. Лично я считаю, что unit-тесты должны покрывать полностью лишь код DAL'а, если же вы хотите заодно проверять и базу – напишите небольшое количество integration-тестов, которые будут проверять соединение, а также какой-то минимальный набор CRUD-запросов, чтобы при случае найти ошибки и здесь. Мы же дальше будем рассматривать тестирование без базы данных.

Итак, в чем же, собственно, состоит проблема? Дело в том, что Entity Framework, к сожалению, проектировался не совсем для того, чтобы код, содержащий вызовы к нему, можно было легко протестировать – слишком уж завязаны сущности на контекст и слишком уж много функций контекст берет на себя (куда ж без этого):

  • сгенерированные ObjectQuery<T>-свойства для доступа к данным обращаются напрямую к базе данных
  • не поддерживает POCO (Plain Old CLR Objects), все объекты содержат navigation properties и Load-методы, которые также обращаются к базе
  • не поддерживает принцип Persistence Ignorance, то есть все объекты сами знают, как себя сохранять

Однако, несмотря на все это, Entity Framework обладает дополнительными полезными свойствами, которые нам помогут:

  • EF кеширует сущности в памяти, более того, позволяет работать с контекстом, как датасет: у сущностей есть состояния (unchanged, inserted, updated, deleted и др.), а также метод AcceptAllChanges, который переводит добавленные или измененные объекты в состояние unchanged
  • EF не поддерживает lazy loading, который бы мог привести к проблемам
  • контекст EF, основываясь на схеме модели, имеет достаточное количество информации о типах связей между сущностями, constraint'ах, not null-полях и т.д., чтобы автоматически проверять, все ли вы верно проинициализировали перед тем, как засылать сущность в базу данных, причем делает это даже когда вы не работаете с базой, а вызываете AcceptAllChanges()
  • внутренний API контекста достаточно открыт, чтобы самому производить доступ к закешированным сущностям, обходя стандартные ObjectQuery-свойства

Итак, как же нам нужно модифицировать наш код, чтобы он стал тестабельным?

  1. Прежде всего, для нашего же удобства нам стоит выделить код, работающий с IQueryable, ObjectQuery и IRelatedEnd.Load в отдельный слой – Data Access Layer. Не важно, как вы это реализуете, лишь бы у вас был контроль над теми местами, где идут запросы к EF. Если вы это уже сделали, good for you.
  2. Далее нужно перевести контекст в так называемый metadata-only режим, который исключает доступ к базе данных вообще. Перевод этот производится урезанием строки соединения до путей к метаданным и названия провайдера. То есть строку соединения с базой мы не пишем.
  3. Для тех случаев, когда мы работаем с unit-тестами, нужно заменить вызовы SaveChanges на AcceptAllChanges
  4. Для тех случаев, когда мы работаем с unit-тестами, нужно реализовать получение данных исключительно из ObjectStateManager, т.е. из памяти, а не из автосгенерированных ObjectQuery-свойств.
  5. Для тех случаев, когда мы работаем с unit-тестами, исключаем из кода любые вызовы IRelatedEnd.Load, то есть все обращения к Load-методу из navigation-свойств сущностей.
  6. Если был реализован lazy loading (через все те же IRelatedEnd.Load), необходимо его отключить.

Возникает вопрос: как ЭТО ВСЕ реализовать физически в коде? Не писать же отдельные методы доступа к данным для unit-тестов. Ответ прост: это можно реализовать при помощи Dependency Injection. То есть основная идея такова: нужно реализовать специальный интерфейс, содержащий методы GetEntities<T>, GetEntity<T> и ApplyPendingChanges, а также как минимум двух его наследников: одного для реального доступа к базе, другого – для работы с контекстом в памяти. Все, наши методы DAL, естественно, будут использовать именно эти методы, а вот какой объект будет инициализирован в DAL – это уже зависит от того, находимся мы в реальном приложении, или в режиме тестирования.

Я решил приготовить пример кода вместо того, чтобы делать еще один длиннющий пост с подробностями. Реализация достаточно проста, поэтому, думаю, кому нужно, те разберутся и без моей помощи :) В примере используется контекст на базе данных Northwind, в качестве unit-test framework'а я использовал NUnit. Если будут вопросы по коду – пишите в комментариях. Будет много вопросов – напишу отдельный пост с разъяснениями.

Собственно, пример реализации можно найти здесь:

Пример реализации поддержки unit-тестирования в Entity Framework

Удачи всем!

Upd. В ответ на справедливое замечание Миши Чалого о том, что в коде нифига не DI, изменил код, чтобы DI там было :) Кроме того, вынес знание об инициализации контекста и mock-обертку в проект unit-теста, где они и должны быть. Код стал более правильным, так что прошу скачивать и использовать вторую версию по той же ссылке.

Monday, September 29, 2008

Entity Framework + NAnt - трудности с включением файлов модели в сборку

Вдогонку своему предыдущему посту про миграцию на .NET 3.5 SP1 пишу еще один, может, кому полезно будет. Еще одна беда подкралась внезапно, со стороны компиляции метаданных контекста EF в библиотеку. Дело в том, что у нас есть автобилды, построенные на Cruise Control + NAnt. Как вы сами, наверно, уже догадались, если NAnt'у не сказать, чтобы он билдил csdl, msl и ssdl в сборку, он сам не догадается. В принципе, ничего особенного, даже поста отдельного не заслуживает, однако здесь есть одна засада.

Дело в том, что раньше процессом разбивания файла edmx на три файла метаданных занимался специальный exe-файл, который назывался EdmxDeploy.exe, который по умолчанию не включал файлы метаданных в сборку, это нужно было делать отдельно. Так мы и жили, кто хотел – работал с файлами на диске, остальные – с ресурсами. Теперь же сборкой занимается специальный таск MSBuild, который лежит глубоко внутри и по умолчанию билдит метаданные в сборку. Мы переписали все строки соединения на ресурсы и думали, что дешево отделались. Не получилось: упал серверный билд. Причем, причина падения понятна – в сборке просто нет ресурсов с метаданными, но вот как их туда добавить, если у нас лишь edmx-файл? Как я ни пытался настроить билд edmx-файла, чтобы он и в сборку данные клал, и три файла метаданных мне на диск генерировал – ничего не получалось. Старый EdmxDeploy.exe разработчики с диска уже снесли – он вроде как не нужен. С горя уже полез изучать, как извне запустить MSBuild и получить нужные мне файлы метаданных отдельно, чтобы потом через <resources> добавить их в сборочный процесс NAnt'а. Однако снова спас гугл. В процессе поиска утилит случайно наткнулся на утилиту EdmGen2, которая, в отличие от EdmGen как раз и занимается тем, что умеет делить и собирать edmx.

У этой замечательной тулы есть несколько параметров, которые позволяют генерировать csdl, msl и ssdl по edmx, наоборот, валидировать edmx-модель, а также генерировать edmx-модель по базе и потом код по этой модели. Ну, и на закуску, эта тула поставляется с исходниками, так что вы можете все посмотреть и исправить в случае необходимости сами.

Все, что остается в нашем случае – это лишь добавить код генерации нужных нам файлов в postbuild-скрипт и добавить сгенерированные файлы в nant-скрипт.

Postbuild-скрипт:

rem Post-build Event Command Line:
rem "$(ProjectDir)postbuild.cmd" "$(ProjectDir)"

@echo off
cd "%1"
%WINDIR%\system32\xcopy "%1Model\Model.edmx" "%1EntitySchema\" /s /r /y

cd "%1EntitySchema"
EdmGen2.exe /FromEdmx CCF.edmx

Кусок nant-скрипта:

<target name="build">
    <csc ...>
        ...
        <resources prefix="Model">
            <include name="EntitySchema/Model.csdl" />
            <include name="EntitySchema/Model.ssdl" />
            <include name="EntitySchema/Model.msl" />
        </resources>
    </csc>
</target>

Thursday, September 25, 2008

Введение в mock-объекты. Классификация

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

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

Однако наши классы очень часто используют другие классы в своей работе. Например, слой бизнес логики (Business Logic layer) часто работает с другими объектами бизнес логики или обращается к слою доступа к данным (Data Access layer). В трехслойной архитектуре веб-приложений это вообще постоянный процесс: Presentation layer обращается к Business Logic layer, тот, в свою очередь, к Data Access layer, а Data Access layer – к базе данных. Как же тестировать подобный код, если вызов одного метода влечет за собой цепочку вплоть до базы данных?

В таких случаях на помощь приходят так называемые mock-объекты, предназначенные для симуляции поведения реальных объектов во время тестирования. Вообще, понятие mock-объект достаточно широко: оно может, с одной стороны, обозначать любые тест-дублеры (Test Doubles) или конкретный вид этих дублеров – mock-объекты. Я постараюсь использовать этот термин исключительно во втором случае, чтобы никого не путать, и не запутаться самому :)

Понятие тест-дублеров введено неким Gerard Meszaros в своей книге «XUnit Test Patterns» и теперь с подачи небезызвестного Мартина Фаулера эта терминология набирает популярность. Джерард и Мартин делят все тест-дублеры на 4 группы:

  • Dummy – пустые объекты, которые передаются в вызываемые внутренние методы, но не используются. Предназначены лишь для заполнения параметров методов.
  • Fake – объекты, имеющие работающие реализации, но в таком виде, который делает их неподходящими для production-кода (например, In Memory Database).
  • Stub – объекты, которые предоставляют заранее заготовленные ответы на вызовы во время выполнения теста и обычно не отвечающие ни на какие другие вызовы, которые не требуются в тесте. Также могут запоминать какую-то дополнительную информацию о количестве вызовов, параметрах и возвращать их потом тесту для проверки.
  • Mock – объекты, которые заменяют реальный объект в условиях теста и позволяют проверять вызовы своих членов как часть системы или unit-теста. Содержат заранее запрограммированные ожидания вызовов, которые они ожидают получить. Применяются в основном для т.н. interaction (behavioral) testing.

Поначалу эта классификация выглядит очень непонятной. Но если вдуматься, то можно разобраться, в чем заключается отличие между теми и иными типами объектов. Предположим, что вам нужно протестировать метод Foo() класса TestFoo, который делает вызов другого метода Bar() класса TestBar. Предположим, что метод Bar() принимает какой-нибудь объект класса Bla в качестве параметра и потом ничего особого с ним не делает. В таком случае имеет смысл создать пустой объект Bla, передать его в класс TestFoo (сделать это можно при помощи широко применяемого паттерна Dependency Injection или каким-либо другим приемлемым способом), а затем уже Foo() при тестировании сам вызовет метод TestBar.Bar() с переданным пустым объектом. Это и есть иллюстрация использования dummy-объекта в unit-тестировании.

К сожалению, редко можно обойтись простыми dummy-объектами. Иногда метод Bar() выполняет какие-то действия с ним (допустим, Bar() сохраняет данные в базу или вызывает веб-сервис, а мы этого не хотим). В таких случаях наш объект класса TestBar должен быть уже не таким глупым. Мы должны научить его в ответ на запрос сохранения данных просто выполнить какой-то простой код (допустим, сохранение во внутреннюю коллекцию). В таких случаях можно выделить интерфейс ITestBar, который будет реализовывать класс TestBar и наш дополнительный класс FakeBar. При unit-тестировании мы просто будем создавать объект класса FakeBar и передавать его в класс с методом Foo() через интерфейс. Естественно, при этом класс Bar будет по-прежнему создаваться в реальном приложении, а FakeBar будет использован лишь в тестировании. Это иллюстрация fake-объекта.

Со stub- и mock-объектами все немного сложнее, хотя и здесь есть от чего отталкиваться. Stub-объекты (стабы) – это типичные заглушки. Они ничего полезного не делают и умеют лишь возвращать определенные данные в ответ на вызовы своих методов. В нашем примере стаб бы подменял класс TestBar и в ответ на вызов Bar() просто бы возвращал какие-то левые данные. При этом внутренняя реализация реального метода Bar() бы просто не вызывалась. Реализуется этот подход через интерфейс и создание дополнительного класса StubBar, либо просто через создание StubBar, который является унаследованным от TestBar. В принципе, реализация очень похожа на fake-объект с тем лишь исключением, что стаб ничего полезного, кроме постоянного возвращения каких-то константных данных не требует. Типичная заглушка. Стабам позволяется лишь сохранять у себя внутри какие-нибудь данные, удостоверяющие, что вызовы были произведены или содержащие копии переданных параметров, которые затем может проверить тест.

Mock-объект (мок), в свою очередь, является, грубо говоря, более умной реализацией заглушки, которая уже не просто возвращает предустановленные данные, но еще и записывает все вызовы, которые проходят через нее, чтобы вы могли дальше в unit-тесте проверить, что именно эти методы вот этих вот классов были вызваны тестируемым методом и именно в такой последовательности (хотя учет последовательности и строгость проверки, в принципе, настраиваемая вещь). То есть мы можем сделать мок MockFoo, который будет каким-то образом вызывать реальный метод Foo() класса TestFoo и затем смотреть, какие вызовы тот сделал. Или сделать мок MockBar и затем проверить, что при вызове метода Foo() реально произошел вызов метода Bar() с нужными нам параметрами. Не совсем понятно? :) Вам нужно знать еще кое-что о unit-тестировании, чтобы понять разницу.

Unit-тестирование условно делится на два подхода:

  • state-based testing, в котором мы тестируем состояние объекта после прохождения unit-теста
  • interaction (behavioral) testing, в котором мы тестируем взаимодействие между объектами, поведение тестируемого метода, последовательность вызовов методов и их параметры и т.д.

То есть в state-based testing нас интересует в основном, в какое состояние перешел объект после вызова тестируемого метода, или, что более часто встречается, что в реальности вернул наш метод и правилен ли этот результат. Подобные проверки проводятся при помощи вызова методов класса Assert различных unit-тест фреймворков: Assert.AreEqual(), Assert.That(), Assert.IsNull() и т.д.

В interaction testing нас интересует прежде всего не статическое состояние объекта, а те динамические вызовы методов, которые происходят у него внутри. То есть для нашего примера с классами TestFoo и TestBar мы будем проверять, что тестируемый метод Foo() действительно вызвал метод Bar() класса TestBar, а не то, что он при этом вернул и в какое состояние перешел. Как правило, в случае подобного тестирования программисты используют специальные mock-фреймворки (TypeMock.Net, EasyMock.Net, MoQ, Rhino Mocks, NMock2), которые содержат определенные конструкции для записи ожиданий и их последующей проверки через методы Verify(), VerifyAll(), VerifyAllExpectations() или других (в зависимости от конкретного фреймворка).

То есть во многом это отличие можно назвать аналогичным отличию state machine diagram и activity diagram в UML: описывают они, в принципе, одно и то же, но разными способами. Иногда удобнее один, иногда второй.

Фаулер вот называет эти два подхода классическим (classical) и мокистским (mockist, ну и слово выдумал) unit-тестированием и делит программистов на тех, кто предпочитают первый и кто предпочитает второй подходы. Я бы этого не делал. Мне кажется, что иногда просто удобнее проверить состояние объекта, а иногда – его взаимодействие с другими объектами. Поэтому эти два подхода прекрасно уживаются вместе, когда вы понимаете, о чем идет речь, и что именно вы хотите сейчас проверить. Так же, как уживаются в одном тесте моки и стабы.

Вот такие пирожки. Теперь несколько примеров, которые иллюстрируют различия между этими двумя подходами и использования стабов и моков. Я также покажу, зачем в реальности может понадобиться interaction-тестирование, чтобы вам было проще выбирать тот или иной вид в будущем. Примеры используют NUnit и Rhino Mocks, хотя на их месте с небольшим изменением синтаксиса может оказаться почти любая другая пара фреймворков.

Допустим, у нас есть несколько простых классов:

public class Order
{
    public string ProductName { get; private set; }
    public int Quantity { get; private set; }
    public bool IsFilled { get; private set; }

    public Order(string productName, int quantity)
    {
        ProductName = productName;
        Quantity = quantity;
    }

    public void Fill(IWarehouse warehouse)
    {
        if (warehouse.HasInventory(ProductName, Quantity))
        {
            warehouse.Remove(ProductName, Quantity);
            IsFilled = true;
        }
    }
}

public class Warehouse
{
    private DataAccess db;

    public Warehouse()
    {
        db = new DataAccess();
    }

    public virtual bool HasInventory(string productName, int quantity)
    {
        return db.HasInventory(productName, quantity);
    }

    public virtual void Remove(string productName, int quantity)
    {
        db.Remove(productName, quantity);
    }
}

Вот такой набор классов. Класс Order обращается к классу Warehouse, а тот обращается к базе данных. Предположим, что мы тестируем метод Fill() класса Order. Вот пример тестирования с использованием стаба для state-based тестирования:

[Test]
public void TestFillingOrderWithRhinoStub()
{
    Order order = new Order(Talisker, 50);
    var stubUserRepository = MockRepository.GenerateStub<Warehouse>();

    stubUserRepository.Stub(x => x.HasInventory(Talisker, 50)).Return(true);
    stubUserRepository.Stub(x => x.Remove(Talisker, 50));

    order.Fill(stubUserRepository);
    Assert.IsTrue(order.IsFilled);
}

Пара пояснений по коду. Сначала мы создаем объект типа Order, затем – стаб для класса Warehouse. После этого мы при помощи mock-фреймворка говорим, что при вызове метода HasInventory с определенными параметрами этот метод должен нам вернуть true. Аналогичным образом переопределяем поведение метода Remove (а то еще вызовет реальный и будет бяка). Далее идет вызов метода Fill() с переданным стабом, после чего проверяется, что свойство IsFilled установлено в true. Как видите, ничего сложного. Однако данный тест обладает некоторыми недостатками. Во-первых, непонятно, что делать, если в тестируемом объекте нет свойства, аналогичного IsFilled. Как проверять правильность выполнения кода? Во-вторых, непонятно, что случится, если программист удалит или закомментирует вызов следующей строчки в коде метода Fill():

warehouse.Remove(ProductName, Quantity);

IsFilled устанавливается в true, тест проходит, но код-то уже не работает!

Обе эти проблемы легко разрешаются, если мы воспользуемся interaction тестированием с использованием мока. Для этого напишем другой тест:

[Test]
public void TestFillingOrderWithRhino()
{
    Order order = new Order(Talisker, 50);
    var mockUserRepository = MockRepository.GenerateMock<Warehouse>();

    mockUserRepository.Expect(x => x.HasInventory(Talisker, 50)).Return(true);
    mockUserRepository.Expect(x => x.Remove(Talisker, 50));
    mockUserRepository.Replay();

    order.Fill(mockUserRepository);
    Assert.IsTrue(order.IsFilled);
    mockUserRepository.VerifyAllExpectations();
}

Начало теста аналогичное, затем идет создание мока Warehouse, после чего идет несколько вызовов метода Expect с теми же параметрами, что и в предыдущем тесте. При помощи этого метода мы говорим моку, что мы ожидаем вызова этих методов с такими параметрами и нам в ответ на их вызовы нужно вернуть такие-то значения. Затем идет вызов метода Replay(), который переводит мок из режима записи ожиданий в режим их проверки, то есть запуска тестового метода. Все моки имеют несколько режимов работы (Record, Replay, Verify), это распространенный подход. Далее непосредственно запуск, проверка IsFilled и вызов нового для нас метода VerifyAllExpectations(). Последний как раз и делает всю работу по проверке вызовов методов, параметров и т.д. Теперь, если метод Remove оказался закомментированным, тест не пройдет. Кроме того, нам уже не так важна проверка состояния объекта Order. Если бы свойства IsFilled не было, ничего бы не изменилось, а так мы лишь проверяем, что оно было установлено в соответствии с алгоритмом. Теперь немного поэкспериментируем с кодом. Что, если мы уберем второй Expect или поменяем их местами? Есть несколько режимов строгости проверки, которые задаются через конструктор класса Mock, который также можно использовать для создания мока. В Rhino Mocks есть три уровня строгости: Loose, Strict и Default (Loose). В Loose-режиме мок проверяет лишь то, что все ожидаемые методы были вызваны из тестируемого метода, в то время как в Strict-режиме проверяется также, что не было любых других вызовов и что порядок вызову соответствует порядку ожиданий. В других фреймворках иногда есть и другие режимы. Таким образом, в нашем случае при изменении порядка тест бы прошел, но в Strict-режиме – уже нет. Еще один момент, который показывает отличие методов Expect от методов Stub (в моке они также доступны): методы, зарегистрированные в моке при помощи метода Stub невидимы для метода VerifyAllExpectations. То есть, если нужна проверка вызовов – используйте Expect. Также стоит отметить, что при помощи дополнительных методов типа Return вы можете не только указывать возвращаемые значения, но еще генерировать exception'ы (Throw), вызывать настоящий метод (CallOriginalMethod), задавать ограничения на параметры (Constraints), вызывать дополнительные методы (Callback, Do), работать со свойствами и событиями. В общем, список потрясающий.

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