Ваш опыт разработки через TDD на asp.net mvc

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті

Всем привет. Начал изучать тему юнит-тестирования и tdd, и мне стало интересно посмотреть как это выглядит в реальных проектах, особенно в asp.net mvc. Если у кого-то есть что сказать для помощи в постижении этой темы — пишите сюда или в личку.

👍ПодобаєтьсяСподобалось0
До обраногоВ обраному0
LinkedIn
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter
Дозволені теги: blockquote, a, pre, code, ul, ol, li, b, i, del.
Ctrl + Enter

Рассказывать как разрабатывать на asp.net mvc смысла не вижу — инфы навалом. Особого отличия в применении TDD именно для asp.net mvc то же не вижу. Поэтому расскажу свои мысли о TDD в общем.
1. TDD не должно быть догмой. Очень редко удается сразу написать TDD или BDD тест до написания кода. Это как «курица и яйцо». В итоге все обычно сводится в некому итеративному процессу: изменению интерфейса, теста, кода потом опять теста и т.д.
2. Юнит-тест полезен, только если он маленький и простой. Иначе его сложность и цена поддержки быстро превысят любую пользу.
3. Из п. 2 следует что в коде должна быть хорошая декомпозиция. Т.е. не должно быть больших сложных методов.
4. Из п. 2 следует что юнит-тестирование невозможно без моков и стабов. Иначе тест по-любому будет цеплять слишком много кода и станет сложным.
5. Из п. 4 следует что что обязательно должна быть изоляция интерфейсов и инверсия зависимостей. Что бы в любой класс можно было подсунуть моки.
6. Нужен правильный инструментарий. Например Visual Studio 2012 теперь позволяет замокать даже «системные» классы. Так же важно видеть покрытие кода, иметь возможность написать свою инициализацию тестов (например восстановление базы), настраивать в каком режиме STA или MTA будут запускаться тесты и т.д.
Если все это есть то проблем с asp.net mvc не вижу: контроллеры и представления можно тестировать независимо, подкладывая моки. Для проверки отрендеренного HTML можно использовать LINQ to Xml.

Visual Studio 2012 теперь позволяет замокать даже “системные” классы.
Ну якщо не помиляюсь то тільки алімейт, чи не так?

Все это справедливо, если речь идет о изолированной системе, разрабатываемой «правильно» с нуля. Обычно вопрос «как должно быть?» возникает гораздо реже, чем «ну и что мне с этим хламом делать?» Поясню:

Немного иначе обстоит ситуация когда вы начинаете работать над проектом, которому 5+ лет, в солюшене 100+ проектов, есть 10+ ненормализованных баз, пяток глючных внешних сервисов «от внешней команды разработки», чей интерфейс меняется раз в неделю, доступ произвольно исчезает, а интерфейс никак не описан, код состоит из «исторически сложилось», «мы так всегда раньше делали», «да это был у нас чувак, писал всякую муть, мы не лезем», «такая вот у нас архитектура» и «нет, юнит-тесты мы не пишем — у нас тестеры есть и вообще — некогда» :)

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

Все это справедливо, если речь идет о изолированной системе, разрабатываемой «правильно» с нуля. Обычно вопрос «как должно быть?» возникает гораздо реже, чем «ну и что мне с этим хламом делать?»
Если меня спрашивают «как слепить конфету из навоза» то я сначала поинтересуюсь а зачем вам именно из навоза, если можно из шоколада?
Немного иначе обстоит ситуация когда вы начинаете работать над проектом, которому 5+ лет, в солюшене 100+ проектов, есть 10+ ненормализованных баз, пяток глючных внешних сервисов «от внешней команды разработки», чей интерфейс меняется раз в неделю, доступ произвольно исчезает, а интерфейс никак не описан, код состоит из «исторически сложилось», «мы так всегда раньше делали», «да это был у нас чувак, писал всякую муть, мы не лезем», «такая вот у нас архитектура» и «нет, юнит-тесты мы не пишем — у нас тестеры есть и вообще — некогда» :)
Я видел несколько таких проектов (10+ лет). И немало попыток слепить из них конфетку. Мой вывод — конфету быстрее, проще и дешевле слепить из шоколада. И вкус будет намного лучше.
И в таких-то проектах юнит-тестирование как раз и имеет наибольшую пользу.
Скорее: имело бы, если оно было бы.
Вот теперь я спрошу — как? Как можно на таком проекте написать нормальные (простые и изолированные) юнит — тесты ничего не сломав?

Если не ошибаюсь, то с shims(перехватчик в рантайме от мс) юнит тестами можно на 100 процентов покрыть приложение с самой линейной и кривой архитектурой. Даже разделение ответсвенности и IoC не нужен.

Интересная мысль — как то не думал о таком применении. Теоретически может получится: если зашимить все конструкторы и возвращать моки то прокатит и без IoC. Код для этих шимов правда будет большой и сложный (ведь нужны еще и проверки).
Но это не решит другую проблему: если методы большие, сложные и возвращают сложные структуры данных. Тут код проверки в тесте будет слишком большой, как ни крути.

В этом и заключается опыт и мастерство :) Спланировать и планомерно «почистить» большое решение, осуществляя модификации в несколько этапов. Если в двух словах, то тактика такая: покрыл участок старой системы тестами, поправил его. Следующий — опять поправил и т.д. Главная хитрость в том, что поправить сразу на финальный «правильный код» не выйдет, поэтому эта процедура должна быть многопроходной. То есть надо спланировать поэтапную модификацию кода, причем так, чтобы он в любой момент времени продолжал работать. Этот нюанс часто выпадает из области внимания и многие бросаются переписать в чистую, чему заказчики и менеджеры обычно не очень-то рады :) Часто после таких попыток что-то идет не так и все дружно откатываются к тому, что было. И потом уже энтузиазм утихает, так как инициатор подвергся критике и больше лажать прилюдно не горит желанием.

Если в двух словах, то тактика такая: покрыл участок старой системы тестами, поправил его.
На практике проверял что если тест сложный (скажем больше 100 строк) то пользы от него нету! Поддерживать его так же сложно как и старый код. Поэтому когда через месяц после очередного изменения в коде десяток таких тестов перестанут проходить — никто не будет тратить несколько дней на их изменение — просто выкинут. Проверенно на 2 проектах.
Вот поэтому я и спрашиваю не «зачем покрывать тестами старую систему?», а есть ли какой-то волшебный способ это сделать нормально?
То есть надо спланировать поэтапную модификацию кода, причем так, чтобы он в любой момент времени продолжал работать.
Об этом я и говорю: дешевле, проще и более перспективно изолировать части системы и переписывать их по-правильному на новых технологиях. При этом старый код работает как есть и можно прогонять функциональные тесты для сравнения. Когда готово — старый код выкидываем. Так «по кусочку» можно перенести весь функционал на новую систему.
А вот юнит-тесты слишком привязаны к коду и архитектуре. Поэтому написать юнит-тесты для старого «гнилого» кода и применить их-же для нового «правильного» без изменений нереально.

Для TDD нужно чтобы система была архитектурно готова к этому, иначе поддерживать тесты будет себе дороже.

Спорить мы все горазды. :) Я веду речь про то, как можно в лохматом проекте перейти от неконтролируемого хаоса к тестируемому коду посредством многоэтапных изменений и покрытием старого кода тестами задним числом. А уж потом возможно и перейти к TDD практике.

Скажу по своему опыту, для этого нужно разбить лохматый монолитный проект на модули, взаимодействовать с ними через фасады/интерфейсы, убрать static методы, потом уже можно будет писать тесты mock’ая зависимости, ввести DI и прочее. Code by interface not by implementation.

Я веду речь про то, как можно в лохматом проекте перейти от неконтролируемого хаоса к тестируемому коду посредством многоэтапных изменений и покрытием старого кода тестами задним числом.
Писать юнит-тесты для старого кода «задним числом» не имеет смысла. Юит-тест, это white-box тест и он «завязан» на конкретную реализацию и архитектуру. Любое изменение старого кода, а особенно улучшение архитектуры потребует переписывать и юнит-тесты. Но ведь цель юнит-тестов в том, что бы проверить что ничего не сломалось. А если мы их то же меняем то гарантии уже нет.
Если говорить о функциональных тестах для старого кода — тут как раз наоборот. Это black-box тесты, которые проверяют именно функциональность и не зависят от реализации. Вот ими то и надо проверять что новая реализация (написанная «правильно») работает так же, как старая и ничего не потерялось.
Хотя, как показала практика, насчет «ничего не потерялось» то же особо заморачиваться не стоит. Когда дошло у нас дошло до переписывания, оказалось что про некоторые фичеры уже никто и не помнит зачем они и как работать должны. В итоге процентов 30 функционала просто «оставили за бортом» — никто не расстроился.
А с другим проектом была еще более поучительная история: за 10+ лет наплодили мега-монстра с кучей функционала и настроек, которая его переключала. В итоге клиент платил за сапорт кучу денег. А потом поменялся владелец, пришел другой менеджер — и клиент купил готовую комерс-систему и за полгода настроил под себя. В итоге 80% старого функционала выкинули вместе со старой системой (и большой командой которая ее поддерживала).
Мой вывод — конфету быстрее, проще и дешевле слепить из шоколада.
В плане «быстрее, проще, дешевле» тут наше мнение (мнение разработчика), к сожалению или к счастью, не единственной и не самое важное. Эти вопросы должны решаться бизнесом. Не всегда обычная логика справляется с принятыми решениями в области денег. Наша задача — выдать наиболее эффективное техническое решение. Если бизнес готов заморозить использование системы и дать время на переписывание ее с нуля — для нас это счастье. Но бизнес не любит так делать, поскольку если сорвутся сроки, и переписать продукт не успеют, то у него не будет никакого работающего решения. Поэтому обычно предпочтение отдается пусть и плохо, но кое как работающей старой системе.

Riding a Dead Horse
www.tysknews.com/..._dead_horse.htm
Деньги тут не главный фактор: на изменения в старой системе денег уйдет даже больше, чем на внесение этой же функциональности в новую версию. А переход на новую систему через 1-2 года потом позволит сэкономить еще больше.
Тут больше политика и бюрократия: наемному менеджеру важнее показать хоть какой-то результат сейчас, выслужиться, а что будет через 1-2 года это не его проблема. Тем более что бюджеты планирую на год. И сказать «дайте мне в этом году в 2 раза больше а потом 2 года будете платить в половину меньше» — никто не поверит. Да и какой менеджер сам себе потом урежет бюджет?
Но и у меня, как у девелопера, свои интересы — мне их деньги пофиг. А вот деградировать на «гнилом» проекте — прямой ущерб и перспективам, и здоровью. Поэтому я готов предложить лучшее техническое решение. Не нравится? Ну набирайте индусов — пусть они вам копипастят заплаты, а я лучше помогу своими знаниями тем, кому надо.

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

еще пара слов о покрытии. оно считается разными тулами по-разному. алгоритм не всегда понятен и/или выглядит правильным. но это неважно. при сборе метрики «покрытие юнит-тестами» важно снимать ее один и тем-же способом. определить, какой минимальный процент приемлем и жить счастливо. обычно 70-80% при снятии студией достаточно, чтобы тесты адекватно реагировали на изменения кода.

Как бы не банально это не звучало, я посоветовал бы бизнес логику в контроллерах не держать, а вынести в отдельные проекты и тестировать ее отдельно.

Тестирование самих контроллеров делается так. Можно еще глянуть этот тренинг для общего развития. Вот тут описан довольно любопытный способ тестировать контент razor вьюх. Здесь — как можно тестировать маршрутизацию на сайте или API проекте. Работа UI (навигация, результат пользовательских операций) — это можно тестировать тулами для автоматического тестирования веб приложений типа Selenium. А вообще, тестировать можнои нужно по-всякому. Главное, чтобы это было оправдано.

Мой принцип такой — словил ошибку — напиши тест ее покрывающий. Дебажишься второй раз, чтобы проверить данные — напиши тест. Есть зависимость от внешних систем — напиши тест проверяющий их доступность. В идеале прогон тестов должен наглядно показать насколько система работоспособна, что отвалилось и почему. Тогда после выходных или отпуска можно быстро вспомнить на чем остановился :) Успехов!

Как бы не банально это не звучало, я посоветовал бы бизнес логику в контроллерах не держать, а вынести в отдельные проекты и тестировать ее отдельно.

а что плохого в том, что бы ее реализовывать расширениями от абстрактного generics репозитория классах модели? все прозрачно, тестируемо в рамках ддд и никакой избыточности. или были практические сложности в каких-то решениях?
такое разделение ответственности так или иначе все осложняет и выглядит целесообразным не всегда.

согласен —

такое разделение ответственности так или иначе все осложняет и выглядит целесообразным не всегда.
, ничего плохого нет. если система пишется строго как mvc приложение — то почему нет?

однако, если бизнес логика сама по себе имеет ценность и в последствии (а то и по ходу проекта — так даже чаще случается :) ) будет решено что ее нужно еще скажем обернуть в API, то удобнее сразу проектировать решение так, чтобы поверх бизнес логики можно было: написать любой UI от консольного приложения, до веб приложения (неважно что это — обвязка RESTful сервисами, mvc приложение или WCF сервисы или что-либо еще. Ну и выделение бизнес логики позволяет максимально просто и понятно использовать TDD подход не зашиваясь на всевозможные ухищрения, связанные с попыткой применять юнит-тестирование поверх специфики выбранного потребителя этой логики (то бишь UI приложения).

Могу спорить долго на тему универсальных модульных систем. Тестов придется написать в раза 2-3 больше, если скурпулезность проекта обязывает. А чего стоит отладка СОА приложений могу сказать не понаслышке, а если их там в цепи штуки 3-4 то и вовсе отговорить в пользу альтернативных решений:)

Понимаю :) Это да, в SOA удобней и проще полагаться на детальное логирование для проверки, что вся цепь работает как надо. Ну и поскольку каждый модуль может быть написан на чем угодно, то выбирать, какое там применять тестирование — следует только строго в рамках этого модуля.

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

Думаю основная путаница, которой подверженны очень многие девелоперы — это путать юнит-тесты и функциональные тесты.
Юнит-тест не должен тестировать функциональность или логику. Это white-box тест. Он, например, может протестировать что «метод Update контроллера принимает модель и передает ее в метод Save интерфейса IRepository». Т.е. он четко и буквально проверяет только тот код, который написан в одном методе одного класса. Будет ли в итоге модель сохранена в базу — это не его забота.
Надо ли объяснять почему метод контроллера должен быть маленький, простой и не делать всю работу по сохранению в базу сам?

Надо ли объяснять почему метод контроллера должен быть маленький, простой и не делать всю работу по сохранению в базу сам?
Если это не риторический вопрос, то нет:)
Юнит-тест не должен тестировать функциональность или логику. Это white-box тест. Он, например, может протестировать что «метод Update контроллера принимает модель и передает ее в метод Save интерфейса IRepository

Есть два стиля написания юнит-тестов. Тот, о котором говоришь ты, называется «mockist testing», он лучше подходит для тестирования контроллеров и т.п. классов, осуществляющих «дирижирование» процесса.

А вот для тестирования именно бизнес-логики лучше подходит классическое юнит-тестирование, которое проверяет результат и состояние объекта после выполнения операции, а не ее внутреннюю реализацию.

Почитай: Mocks aren’t Stubs

а как это может выглядеть?
пишете тесты для контроллера и сервиса (в идеале просто тесты для функции), реализуете функционал так, чтобы он проходил тесты и покрытие было близким к 100, рефакторите.
и в больших и в маленьких проектах это выглядит одинаково

если вы пишете сразу тесты, а потом реализуете функционал, который проходит эти тесты и у вас покрытие тестами даже и близко не 100 — то либо вы делаете это не добросовестно, либо занимаетесь глупостью чтобы удовлетворить заказчика

Очень часто встречается код вида

if(param == null)
throw new ArgumentNullException("param«);
Вроде как и протестировать весь код нужно, но тестировать именно такой код глупо.
Отдельный вопрос — необходимость такого кода, но если посмотреть исходники дотнета, такого кода очень много.
Он помогает быстро ловить неправильное использование при работе большой группы разработчиков либо при использовании ваших библиотек этими самыми разработчиками.
При этом, если код написан без грубых ошибок, то этот проверки никогда не «выстрелят».
Вот вам и не 100% и даже не 99%, возможно на глаз 95%, если все остальное идеально.
пишете тесты для контроллера
Это нужно быть офигенно не ленивым и очень сильно любить стучать по кнопкам.
А в остальном, как в случае с сервисами, да, все так и все довольно типично.

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

ну а если вы считаете, что такой простой код (что-то дернуло что-то и отдало куда-то результат) не надо покрывать
Да, я действительно не считаю, что контроллеры необходимо тестировать. Зачем? Если у вас в контроллере есть код, который необходимо тестировать, то это вы что-то делаете не так. В ином случае тестируется сервис. Если вы сомневаетесь в данных, приходящих «со стороны», ткстируйте гуй. Ну, я действительно не понимаю на кой болт тестировать механизм MVC, как таковой, по-моему его давно уже ( где-то перед релизом) оттестировали.
ну а если вы считаете,... то тдд не для вас.
Я не просил совета, а высказывал мнение. Я в состоянии разобраться, что мне нужно, а что — нет.

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

еще раз повторю. мы сейчас говорим о ТДД. не о покрытии критичного кода юнит тестами чтобы избежать регрессии, а именно о тдд
При разработке очень важно бывает включать здравый смысл.
при таком подходе модулями будут экшны контроллера. ну и вполне резонным вопросом будет какого хрена вы формализовали требования в виде модульных тестов, если ваши модули делают какие-то вещи, которые вообще не предусмотрены в ваших формализованных требованиях
Слов много, смысла чуть. Тестируете контроллеры и это кому-то нужно, этот кто-то платит за тесты контроллеров (как за часть целого) деньги — значит вы правы и делаете все верно. Так или иначе, у нас несколько отличается взгляд на процесс разработки, ну и Бог с ним.
UPD. Тестировать, а любом случае, необходимо. Но нужно быть более гибким, а не тупо следовать книге.

я не выступаю в качестве адвоката ТДД. но если вы покрываете несуществующий код тестами — то было бы здорово, чтобы ваш код в полной мере отвечал тестам. если вы покрываете отдельно сервисы с существенной бизнес логикой, покрываете какеи-то утилитные методы (функции), решаете, что контроллеры можно не покрывать — то не обманывайте себя. напишите этот код и покройте критичные фрагменты и не называйте это тдд. тдд не об этом

не обманывайте себя. напишите этот код и покройте критичные фрагменты и не называйте это тдд. тдд не об этом
я не выступаю в качестве адвоката ТДД
Серьезно? Х))
не называйте это тдд. тдд не об этом
Я понимаю важность того, что вещи нужно называть своими именами. Смотрю, что вы привыкли работать строго по инструкции, и не важна степень ее абсурдности? Воля ваша.

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

ну если вы не заметили призыв к здравому смыслу в моем прошлом посте — то я и не знаю что делать.
Слабый призыв.
сейчас у людей тдд ассоциируется с написанием тестов, но это в корне не так
Естественно, если вы написали 2 теста на 100 методов, то это явно не тдд, но и до маразма доводить не нужно.
Эмм... По-моему мы по-второму кругу пошли ))

если вы покрываете критичный код тестами — то это тоже не тдд. это просто написание модульных тестов.

Конечно же нет, не только, а так хотелось бы )). На написание сценариев и толковых(и не очень) тестов уходит львиная доля времени...

Возможно, эта статья dou.ua/...opment-process будет для вас полезной (она правда довольно старая и не совсем о TDD на asp.net mvc).

Підписатись на коментарі