> Сериализация лямбда-функций весьма представляет собой весьма интересный, но практически бесполезный механизм.
Как ни странно, но сериализация лямбд вполне успешно и с большой пользой применяется на практике. Например, на этом основана распределенная обработка данных в Apache Spark (см. spark.apache.org/docs/latest/programming-guide.html#passing-functions-to-spark).
> вынос генерации в отдельную функцию — такой же искусственный интерфейс
Нет. Исходя из наличия сложной логики генерации, выделение этой логики как отдельной функции — это вполне естественный шаг рефакторинга. А вот специальный интерфейс, который нужен только для тестирования — это искусственная конструкция.
> Не объявить, что оно тривиально, а проверить его.
Зачем проверять тривиальные вещи, если можно этого не делать и потратить высвободившееся время с реальной, измеряемой пользой?
> посмотрите на пример моего кода, то вы поймете, почему ничего гарантированного в нем нет
Я уже прокомментировал этот пример. Если прорефакторить его в соответствии с предложенным мною (в комментарии выше) подходом, то ничего не меняется. Усложнившаяся генерация сообщений по-прежнему тестируется юнит-тестом без моков, отправка сообщений по-прежнему тривиальна и не требует тестирования. (Понимаю, что вместо слов хорошо бы привести код, но без форматирования будет полный отстой.)
> Вот только во фразе lair был пункт «гарантировать… что отданы команды», который вы решили не тестировать.
Ну мы вроде это уже обсудили… Идея не в том, что я по своему призволу решаю, что тестировать, а что нет. А в том, чтобы используя ФП сделать отправку команд тривиальной (гарантированной) и не требующей тестирования. Чтобы достаточно было протестировать только генерацию, которая не требует искусственных интерфейсов и моков.
> Зачем же вы тогда тестируете текст сообщения, там что, есть циклы и условные переходы?
Вспомним, что все началось с фразы lair «гарантировать, что при таком, таком и таком состоянии заказа отданы команды на отправку такого, такого и такого письма. И это реально самый простой, самый быстрый и самый надеждый способ это протестировать». Т.е. нетривиальная логика генерации предполагалась по условию. Я показал более простой, быстрый и надежный способ ее протестировать.
> В моей личной системе ценностей любое бизнес-требование, реализованное на уровне доменного уровня, должно быть протестировано.
Ничего не имею против (пока вы не в одном проекте со мной). И, обратите внимание, не делаю глубокомысленных заключений о том, понимаете ли вы что-нибудь в чем-нибудь.
> Но как вы гарантируете вызов этого метода в заданных условиях?
Не совсем уверен, что понял вопрос. Но на всякий случай: GeneratePaymentReceivedMessage — «чистая» функция, зависит только от своих неизменяемых аргументов, никаких побочных эффектов внутри.
> Вы действительно не понимаете смысла юнит-тестов
Да я и не претендую даже. Но если поделитесь авторитетными ссылками по поводу проведения черты между очевидными с одной стороны и требующими тестирования с другой вещами — буду благодарен.
> Просто представьте, что код внутри PaymentReceived выглядит вот так
В моем примере код, который отвечает за генерацию сообщений был бы помещен в GeneratePaymentReceivedMessage(). Возвращаемый тип изменился бы на Option[Message]. Тестировался бы по-прежнему GeneratePaymentReceivedMessage(), без IMailer, без моков, без фреймворка для них.
> кто-нибудь сможет изменить код так, что письмо отправится несколько раз, а тесты все равно пройдут.
> можно изменить код так, что письмо не отправится вообще, а тесты все равно пройдут.
Думаю, я смогу изменить код так, чтобы он создавал background thread, который будет потихоньку удалять папки с диска, забивать память мусором и ддосить сайт виндовс10. И, о ужас, все тесты пройдут.
Повторю другими словами. Тест пытается сломать существующий код, а не гипотетическое его изменение в будущем. Ответственность за тестирование изменения ложится на автора этого изменения.
Да уж куда мне понять смысл юнит-тестирования… Я-то всегда думал, что нет смысла тестировать очевидную последовательность действий. Вот если она станет менее очевидной, например, кто-то добавит циклы или условные переходы — это будет его ответственность исправить тесты соответствующим образом.
Буду благодарен, если опровергните мое скромное и несовершенное понимание ссылкой на какого-нибудь признанного авторитета вроде Бека или Фаулера.
Итак, в представленом юнит-тесте, насколько я понял, проверяется 2 вещи:
1. Метод Send() был вызван 1 раз
2. Сообщение было сгененировано в соответствии с ордером.
Теперь берем изначальный код автора (слегка модифицированный по моему вкусу):
// заполняем объект заказа, cкидки, акции и т.д.
var order = GenerateOrder(....);
SaveOrder(order);
var orderNotification = GeneratePaymentReceivedMessage(order);
SendEmail(orderNotification);
Нужно ли проверять, что SendEmail() вызывается ровно один раз (если ранее не было исключения)? Нет, это очевидно.
Как проверить корректность сообщения:
var order = _CreateValidOrder();
var expectedNotification = _Msg(order.NotificationEmail, «Payment..» + order.Number, «Your order #» + order.Number + "....")
Assert.AreEqual(expectedNotification, GeneratePaymentReceivedMessage(validOrder));
Да, в этом варианте появляется новая сущность — сообщение, но не факт, что это минус.
При этом в этом коде нет IMailer, нет необходимости его мокать, и не используется фреймворк для мокинга.
PS. Прошу прощение за отсутствие возможности отформатировать код.
PPS. Не судите строго, если накосячил с синтаксисом. Последний раз писал на C# в 2008 году.
Стараешься писать с учетом будущей поддержки, накручиваешь все паттерны которые только знаешь для решения существующих и потенциальных проблем… А потом бац, требования поменялись, и пилить-то совсем другое надо, как оказалось. И код отправляется в помойку, независимо от всех стараний.
Я не утверждаю, что так бывает всегда, на всех проектах. Но бывают проекты, где решить текущую задачу быстро и просто важнее, чем думать о поддержке, которой может никогда и не случиться, если нет новых фич, нет привлеченных ими пользователей, и фирма обанкротилась.
Тогда давайте начнем с начала — покажите код юнит-теста. Как и что именно он будет проверять? Если я правильно понял изначально процитированные слова о том, что нужно проверить — я покажу как его упростить.
В самую точку! Конечно я ненавижу чужой код да и свой собственный за компанию. И если нахожу даже единственную несчастную опечатку, то разумеется удаляю сразу все, вместе с репозиторием.
Теперь серьезно. Если я вижу подсистему, которая в силу проблем архитектуры/дизайна не может справиться с возложенными задачами, бывает, что проще переписать с нуля, чем расшибаться в лепешку и лепить костыли вокруг очевидных косяков. Считаете, это всегда будет дороже? Почему?
Той самой void-функции с побочным эффектом, который будет заключаться в том, чтобы сунуть эти команды в Send() мейлеру. Заодно еще будет выдавать юнит-тесту.
> гарантировать, что при таком, таком и таком состоянии заказа отданы команды на отправку такого, такого и такого письма. И это реально самый простой, самый быстрый и самый надеждый способ это протестировать.
Простите, что вклиниваюсь. Но в данном случае делать интерфейс и его реализацию для тестов — это, по-моему, не самый простой способ протестировать, что отданы нужные команды на отправку.
> Скорость разработки и стоимость поддержки коррелируются слабо.
Ну, в моей практике случалось, что благодаря высокой скорости разработки было проще и дешевле выкинуть и переписать с нуля, чем поддерживать. Кстати, особенно тяжело как выкидывать, так и поддерживать код с избыточным и неадекватным применением паттернов. Но это так, музыка навеяла.
Как ни странно, но сериализация лямбд вполне успешно и с большой пользой применяется на практике. Например, на этом основана распределенная обработка данных в Apache Spark (см. spark.apache.org/docs/latest/programming-guide.html#passing-functions-to-spark).
Нет. Исходя из наличия сложной логики генерации, выделение этой логики как отдельной функции — это вполне естественный шаг рефакторинга. А вот специальный интерфейс, который нужен только для тестирования — это искусственная конструкция.
> Не объявить, что оно тривиально, а проверить его.
Зачем проверять тривиальные вещи, если можно этого не делать и потратить высвободившееся время с реальной, измеряемой пользой?
> посмотрите на пример моего кода, то вы поймете, почему ничего гарантированного в нем нет
Я уже прокомментировал этот пример. Если прорефакторить его в соответствии с предложенным мною (в комментарии выше) подходом, то ничего не меняется. Усложнившаяся генерация сообщений по-прежнему тестируется юнит-тестом без моков, отправка сообщений по-прежнему тривиальна и не требует тестирования. (Понимаю, что вместо слов хорошо бы привести код, но без форматирования будет полный отстой.)
Ну мы вроде это уже обсудили… Идея не в том, что я по своему призволу решаю, что тестировать, а что нет. А в том, чтобы используя ФП сделать отправку команд тривиальной (гарантированной) и не требующей тестирования. Чтобы достаточно было протестировать только генерацию, которая не требует искусственных интерфейсов и моков.
Вспомним, что все началось с фразы lair «гарантировать, что при таком, таком и таком состоянии заказа отданы команды на отправку такого, такого и такого письма. И это реально самый простой, самый быстрый и самый надеждый способ это протестировать». Т.е. нетривиальная логика генерации предполагалась по условию. Я показал более простой, быстрый и надежный способ ее протестировать.
Совершенно верно. Я же из упомянутой автором «монструозной» Scala сюда свалился.
> вы используете функциональную композицию, а не объектную
Тут подразумевается некое противопоставление. На деле их можно разумно сочитать.
Ничего не имею против (пока вы не в одном проекте со мной). И, обратите внимание, не делаю глубокомысленных заключений о том, понимаете ли вы что-нибудь в чем-нибудь.
> Но как вы гарантируете вызов этого метода в заданных условиях?
Не совсем уверен, что понял вопрос. Но на всякий случай: GeneratePaymentReceivedMessage — «чистая» функция, зависит только от своих неизменяемых аргументов, никаких побочных эффектов внутри.
Да я и не претендую даже. Но если поделитесь авторитетными ссылками по поводу проведения черты между очевидными с одной стороны и требующими тестирования с другой вещами — буду благодарен.
> Просто представьте, что код внутри PaymentReceived выглядит вот так
В моем примере код, который отвечает за генерацию сообщений был бы помещен в GeneratePaymentReceivedMessage(). Возвращаемый тип изменился бы на Option[Message]. Тестировался бы по-прежнему GeneratePaymentReceivedMessage(), без IMailer, без моков, без фреймворка для них.
В моей версии отправка происходит после сохранения платежа. Это очевидно из кода и не требует тестирования.
> можно изменить код так, что письмо не отправится вообще, а тесты все равно пройдут.
Думаю, я смогу изменить код так, чтобы он создавал background thread, который будет потихоньку удалять папки с диска, забивать память мусором и ддосить сайт виндовс10. И, о ужас, все тесты пройдут.
Повторю другими словами. Тест пытается сломать существующий код, а не гипотетическое его изменение в будущем. Ответственность за тестирование изменения ложится на автора этого изменения.
Буду благодарен, если опровергните мое скромное и несовершенное понимание ссылкой на какого-нибудь признанного авторитета вроде Бека или Фаулера.
1. Метод Send() был вызван 1 раз
2. Сообщение было сгененировано в соответствии с ордером.
Теперь берем изначальный код автора (слегка модифицированный по моему вкусу):
// заполняем объект заказа, cкидки, акции и т.д.
var order = GenerateOrder(....);
SaveOrder(order);
var orderNotification = GeneratePaymentReceivedMessage(order);
SendEmail(orderNotification);
Нужно ли проверять, что SendEmail() вызывается ровно один раз (если ранее не было исключения)? Нет, это очевидно.
Как проверить корректность сообщения:
var order = _CreateValidOrder();
var expectedNotification = _Msg(order.NotificationEmail, «Payment..» + order.Number, «Your order #» + order.Number + "....")
Assert.AreEqual(expectedNotification, GeneratePaymentReceivedMessage(validOrder));
Да, в этом варианте появляется новая сущность — сообщение, но не факт, что это минус.
При этом в этом коде нет IMailer, нет необходимости его мокать, и не используется фреймворк для мокинга.
PS. Прошу прощение за отсутствие возможности отформатировать код.
PPS. Не судите строго, если накосячил с синтаксисом. Последний раз писал на C# в 2008 году.
Я не утверждаю, что так бывает всегда, на всех проектах. Но бывают проекты, где решить текущую задачу быстро и просто важнее, чем думать о поддержке, которой может никогда и не случиться, если нет новых фич, нет привлеченных ими пользователей, и фирма обанкротилась.
Теперь серьезно. Если я вижу подсистему, которая в силу проблем архитектуры/дизайна не может справиться с возложенными задачами, бывает, что проще переписать с нуля, чем расшибаться в лепешку и лепить костыли вокруг очевидных косяков. Считаете, это всегда будет дороже? Почему?
Той самой void-функции с побочным эффектом, который будет заключаться в том, чтобы сунуть эти команды в Send() мейлеру. Заодно еще будет выдавать юнит-тесту.
Простите, что вклиниваюсь. Но в данном случае делать интерфейс и его реализацию для тестов — это, по-моему, не самый простой способ протестировать, что отданы нужные команды на отправку.
Ну, в моей практике случалось, что благодаря высокой скорости разработки было проще и дешевле выкинуть и переписать с нуля, чем поддерживать. Кстати, особенно тяжело как выкидывать, так и поддерживать код с избыточным и неадекватным применением паттернов. Но это так, музыка навеяла.