> Зачем же вы тогда тестируете текст сообщения, там что, есть циклы и условные переходы?
Вспомним, что все началось с фразы 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() мейлеру. Заодно еще будет выдавать юнит-тесту.
> гарантировать, что при таком, таком и таком состоянии заказа отданы команды на отправку такого, такого и такого письма. И это реально самый простой, самый быстрый и самый надеждый способ это протестировать.
Простите, что вклиниваюсь. Но в данном случае делать интерфейс и его реализацию для тестов — это, по-моему, не самый простой способ протестировать, что отданы нужные команды на отправку.
> Скорость разработки и стоимость поддержки коррелируются слабо.
Ну, в моей практике случалось, что благодаря высокой скорости разработки было проще и дешевле выкинуть и переписать с нуля, чем поддерживать. Кстати, особенно тяжело как выкидывать, так и поддерживать код с избыточным и неадекватным применением паттернов. Но это так, музыка навеяла.
> Разумеется, это просто мои мысли и причины перехода твиттера на Scala мне неизвестны.
Так почитайте (например, здесь: www.redfin.com/devblog/2010/05/how_and_why_twitter_uses_scala.html) прежде чем выдумывать сказки про «монструозные языки». Скалу любят в том числе за то, что она может дать скорость разработки и гибкость динамических языков, не жертвуя типобезопасностью и скоростью выполнения.
в 2012 году Китай вложил в образование 294 млрд долларов
США — 454 млрд долларов.
Российское правительство пообещало поддержку в размере 184 млн долларов на улучшение своих позиций в мировых рейтингах.
Разница на 3 порядка выглядит чудовищной. Но насколько сопоставимы категории «вклад в образование» и «поддержка на улучшение позиций в рейтингах»? Очевидно, только этой поддержкой траты на высшее образование не ограничиваются. Хотелось бы увидеть какие-то сопоставимые цифры, для объективности картины.
> Если вы намекаете на Javadoc и Pythondoc, то а) их нужно ставить отдельно
Не нужно. javadoc идет в комплекте с jdk, в питоне, как я понимаю — аналогично.
> для них нужно учить/запоминать специальный синтаксис
Что за синтаксис? Давайте-ка сравним доки Go и Java. Вот пример на Go (взятый здесь: blog.golang.org/godoc-documenting-go-code)
// Fprint formats using the default formats for its operands and writes to w.
// Spaces are added between operands when neither is a string.
// It returns the number of bytes written and any write error encountered.
func Fprint(w io.Writer, a ...interface{}) (n int, err error)
Вот похожий код из Java:
/**
* Writes a string.
*
* param str
* String to be written
*
* @throws IOException
* If an I/O error occurs
*/
public void write(String str) throws IOException
Ничего не скажешь, невероятно сложный синтаксис, несколько лет надо учить.
Что интересно, по той же ссылке на блог Go можно найти такой текст:
> Godoc is conceptually related to Python's Docstring and Java's Javadoc, but its design is simpler. The comments read by godoc are not language constructs (as with Docstring) nor must they have their own machine-readable syntax (as with Javadoc). Godoc comments are just good comments, the sort you would want to read even if godoc didn't exist.
Автор в общем признает, что по сути подход такой же, как в Java и Python. Правда потом зачем-то начинает решать за меня, какие комментарии мне читать приятнее. Но видимо, это такая черта Go-евангелистов.
> И реальность такова, что на практике проектов, которые следуют этим правилам, гораздо меньше, чем тех, которые не следуют. В Go это полностью наоборот.
Вспомним, что все началось с фразы 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() мейлеру. Заодно еще будет выдавать юнит-тесту.
Простите, что вклиниваюсь. Но в данном случае делать интерфейс и его реализацию для тестов — это, по-моему, не самый простой способ протестировать, что отданы нужные команды на отправку.
Ну, в моей практике случалось, что благодаря высокой скорости разработки было проще и дешевле выкинуть и переписать с нуля, чем поддерживать. Кстати, особенно тяжело как выкидывать, так и поддерживать код с избыточным и неадекватным применением паттернов. Но это так, музыка навеяла.
Так почитайте (например, здесь: www.redfin.com/devblog/2010/05/how_and_why_twitter_uses_scala.html) прежде чем выдумывать сказки про «монструозные языки». Скалу любят в том числе за то, что она может дать скорость разработки и гибкость динамических языков, не жертвуя типобезопасностью и скоростью выполнения.
На каких конкретно научных знаниях построена данная статья? Есть ли исследования, подтверждающие эффективность предложенной модели?
Разница на 3 порядка выглядит чудовищной. Но насколько сопоставимы категории «вклад в образование» и «поддержка на улучшение позиций в рейтингах»? Очевидно, только этой поддержкой траты на высшее образование не ограничиваются. Хотелось бы увидеть какие-то сопоставимые цифры, для объективности картины.
Не нужно. javadoc идет в комплекте с jdk, в питоне, как я понимаю — аналогично.
> для них нужно учить/запоминать специальный синтаксис
Что за синтаксис? Давайте-ка сравним доки Go и Java. Вот пример на Go (взятый здесь: blog.golang.org/godoc-documenting-go-code)
// Fprint formats using the default formats for its operands and writes to w.
// Spaces are added between operands when neither is a string.
// It returns the number of bytes written and any write error encountered.
func Fprint(w io.Writer, a ...interface{}) (n int, err error)
Вот похожий код из Java:
/**
* Writes a string.
*
* param str
* String to be written
*
* @throws IOException
* If an I/O error occurs
*/
public void write(String str) throws IOException
Ничего не скажешь, невероятно сложный синтаксис, несколько лет надо учить.
Что интересно, по той же ссылке на блог Go можно найти такой текст:
> Godoc is conceptually related to Python's Docstring and Java's Javadoc, but its design is simpler. The comments read by godoc are not language constructs (as with Docstring) nor must they have their own machine-readable syntax (as with Javadoc). Godoc comments are just good comments, the sort you would want to read even if godoc didn't exist.
Автор в общем признает, что по сути подход такой же, как в Java и Python. Правда потом зачем-то начинает решать за меня, какие комментарии мне читать приятнее. Но видимо, это такая черта Go-евангелистов.
> И реальность такова, что на практике проектов, которые следуют этим правилам, гораздо меньше, чем тех, которые не следуют. В Go это полностью наоборот.
Можете ли как-нибудь доказать этот тезис?