Pull to refresh

Comments 401

>Статья получилась большой, и я надеюсь, что старался не зря.
Тема на самом деле очень интересная. И хотя выводы не новые, и достаточно очевидно, что платите вы за разработку тестов сегодня, а профит возможно получите потом, но я видел с десяток наверное статей прямо тут, где ультимативно утверждается: «Вы обязаны писать тесты». Ну то есть, очевидно это далеко не всем.

Есть еще несколько аспектов, на которых может быть стоило бы акцентировать внимание:

— вопрос не стоит упрощать до «писать или не писать юнит тесты». Правильная постановка ближе к чему-то такому: «Какой уровень надежности нам нужен, и как лучше его добиться?». То есть, следует рассмотреть разные способы повышения надежности. Тесты — не единственный.
— кстати, а измеряли ли ли мы надежность на сегодня? Мы уверены, что она недостаточная? Умеем ли мы вообще ее мерять?
— тесты не гарантируют отсутствие багов. Поэтому ответ на вопрос: «Стоит ли Василию обеспокоиться написанием юнит-тестов?» вполне может быть иным: нет, не стоит, все равно надежность от этого не вырастет, потому что Василий и тесты писать не умеет тоже. А нужно лучше поискать опытного консультанта, и провести допустим code review.

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

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

И у вас нет ресурсов через неделю после начала проекта согласовывать кто и что будет писать в проекте сегодня
Потому Василию стоит обеспокоится написанием тестов сразу, кроме случаев, когда он пишет сам и сразу в /dev/null
Тесты убирают страх написания кода в большом и сложном проекте, коим любой проект становится через неделю написания кода тремя человеками
А может быть не стоит писать код одновременно тремя человеками неделю? А стоит, эта, разбить его на подзадачи?

И у вас нет ресурсов через неделю после начала проекта согласовывать кто и что будет писать в проекте сегодня
Нафига вы вообще посадили на проект, в котором неясно что делать вообще трёх человек?

Не разделяя идею бессмысленности юнит-тестов в принципе я, всё же, склонен согласиться с тем, что часто их пытаются использовать как замену пониманию: нам не нужно знать что делает этот код — у нас же есть юниттесты!

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

Как разбивать на подзадачи, так чтоб один разраб всегда отвечал только за тот код, который он написал?
Как разбивать на подзадачи так, чтоб они никогда не пересекались?
А кто сказал, что они не должны пересекаться?

Как разбивать на подзадачи, так чтоб один разраб всегда отвечал только за тот код, который он написал?
Разраб не должен отвечать «только за тот код, который он написал». Он отвечает за тот код, который он принял.

А кто его написал — дело десятое. Но у любого компонента должен быть владелец (MAINTAINER или OWNER — в разных проектах по разному), который за него отвечает. И пока он ваш код не «примет» — вы его, в этом компоненте, не увидите.
А кто сказал, что они не должны пересекаться?
вы

Он отвечает за тот код, который он принял.
Как принять 100k LOC?
В таком случае обычно предполагается что на проекте есть условный «тимлид», который сам редко пишет код и занимается в основном интеграциями и планированием.
К сожалению, во многих проектах тимлид слишком занят пилением фич и бесполезными митингами, чтобы быть нормальным owner'ом

ПС ну и патч на 100k LOC это уж слишком, надо разбивать на подзадачи
И как тимлиду принять в одно лицо столько кода?

И еще: как делать рефакторинг без юнит тестов?
Еще раз повторяю — тимлид должен сопровождать изменения на 100к строк, пока эти строки пишутся. Не должно быть ситуации с «неожиданной» кучей кода, которую никто кроме писавшего, не понимает.
Ну и никто не говорит что юнит тестов быть не должно. Речь о том что не должно быть хаоса и надежды на юнит тесты, как на единственное спасение.
Что значит
Не должно быть ситуации с «неожиданной» кучей кода, которую никто кроме писавшего, не понимает.
?

Ето регулярная ситуация при подключении любой либы

Речь о том что не должно быть хаоса и надежды на юнит тесты, как на единственное спасение.
а на что тогда полагаться?
Ето регулярная ситуация при подключении любой либы
А для ядра операционки вы тоже юниттесты пишите? А для Minix'а в ME?

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

а на что тогда полагаться?
На людей, очевидно. Либо у вас есть люди, знающие что именно они делают, либо нет. В первом случае юниттесты — не так важны, во втором — они вам не помогут.
Сторонний код без тестов используется в крайнем случае

Люди допускают ошибки

На что полагаться?
Сторонний код без тестов используется в крайнем случае
И чего вы этим добиваетесь? Увеличения цены разработки? Есть много других способов.

На что полагаться?
На людей, однако.

Люди допускают ошибки.
Люди также исправляют ошибки — на что юниттесты в принципе неспособны.

Потому если вас устраивает некоторая вероятность ошибки — вам можно полагаться на людей. Если не устраивает — нужны формальные доказательства, за которые так топит 0xd34df00d.

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

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

А самые дорогостоящие разработчики еще и будут заниматься ручным тестирование чужого кода?

Я наконец-то понял вашу позицию
Те вместо написания кода разработчики будут делать ручное тестирование при каждом изменение кода?

А вам не приходило в голову, что в 99% проектов не нужно делать тестирование всего и вся при каждом изменении кода? Что нормально написанный код не приводит к тому, что изменение функции А ломает функции С, D и Z? И что с другой стороны, если вы меняете алгоритм функции А, то вы должны и тест под неё переписать, а значит, дорогостоящий разработчик будет делать куда больше рутинной работы, чем просто взять и раз эту функцию прогнать вручную.
И что ошибка в нижележащем коде обычно находится сразу по результатам UI-теста, без всяких юнит-тестов, вы тоже не слыхали?
А самые дорогостоящие разработчики еще и будут заниматься ручным тестирование чужого кода?

Вы издеваетесь? Зачем тестировать чужой код вообще? У вас есть библиотека, есть отзывы о её применении. Что вы там тестировать собрались? Опыт её эксплуатации — единственный более-менее достоверный тест. Чужие тесты вам никакой гарантии не дадут, а собственные — дурацкая трата времени и денег.
Как принять 100k LOC?
Попросить того, кто «это» сотворил разбить на кусочки в 100-200 строк и в каждом из них описать — что он делает и зачем.
Хм
Как попросить гугл (или любую корпорацию) сделать для их либ
разбить на кусочки в 100-200 строк и в каждом из них описать — что он делает и зачем
?
Я прошу прощения, а вы, подключая либы от сторонних вендоров, делаете и повторно работу за этих вендоров — рефакторите их код, обвешиваете тестами?
Не подключаю если там нет нормального покрытия тестами
Это, честно говоря, больше похоже на некий фанатизм, чем на осознанное решение.
Да. Выходит, что здравые основания по выбору библиотеки, т.е. её полезность для вашего проекта, её известность и распространённость могут быть перевешены второстепенным фактором, представляющим собой лишь иллюзию надежности. Ведь вы, с одной стороны, понятия не имеете, насколько тесты написаны качественно, как они покрывают код. Тесты нашлёпаны, формальности соблюдены — можно брать. С другой стороны, отсекаете массу качественных и хороших продуктов, разработчики которых не пишут тесты.
А чем может быть полезна дырявая библиотека?

И опять же.
Вы так топите за ручную проверку кода людьми и при етом не допускаете, что тесты также можно просмотреть и оценить?

Странно, странно
Вы так топите за ручную проверку кода людьми и при етом не допускаете, что тесты также можно просмотреть и оценить?

Я не допускаю, что адекватный, а не пустивший всё на самотёк, заказчик софта будет готов платить деньги команде, которая тратит их на подобную хрень. Давайте ещё тестеров для тестеров нанимать. Чего уж там, раз решили подробно выверять юнит-тесты сторонних библиотек, то вполне логичное продолжение.
UFO just landed and posted this here
Нет, они этого не гарантируют ни формально, ни эмпирически.
почему проверка функционирования кода не гарантирует его работоспособность?

при таких вводных можно рассмотреть и другие способы обеспечения надёжности.
какие?
UFO just landed and posted this here
Потому что проверяются только конкретные частные случаи. .
да, проверяется как код должен работать.
Так как тест, проверяющий корректную работу кода не гарантирует, что код будет работать так как он задуман?

То же использование систем формальной верификации.
Что сие есть?
Как оно делается?
Как оно гарантирует корректность?
UFO just landed and posted this here
Он не проверяет, что код будет работать так, как нужно, на всём возможном пространстве входов
что не является задачей юнит тестов, а задачей регресионных тестов

Даже если вы проверили, что
такие юнит-тесты никто не пишет

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

Хммм
UFO just landed and posted this here
Не понял. А зачем тогда юнит-тесты нужны?

да, проверяется как код должен работать.


Как вы напишете тесты на функцию сортировки? На какие-нибудь деревья?
а зачем тестировать, то что уже покрыто тестами?

В тесте вы принципиально не можете использовать квантор всеобщности
потому что в реальности у вас нет возможности пройтись по всем значениям в любом случае

Кстати, кто и где использует ваши инструменты для покрытия тестами?
UFO just landed and posted this here
Те опять вкусовщина — один инструмент у вас дает гарантии, другой нет

Кстати про математику — что там с автоматическим доказательством теорем?
UFO just landed and posted this here
Не в курсе про автопроверку доказательств
Подскажите материалы по данному поводу?
UFO just landed and posted this here
Мне нужна статья про
даёт автоматические проверки доказательств теорем
UFO just landed and posted this here
Вполне устроит
Спасибо

The CoC is strongly normalizing


although, by Gödel's incompleteness theorem, it is impossible to prove this property within the CoC since it implies inconsistency.


UFO just landed and posted this here
Меня больше смутил простой пример
The proof of commutativity of addition on natural numbers
UFO just landed and posted this here

Юнит-тесты, ставшие однажды зелёными, становятся регрессионными тестами. Это их основная задача обычно. Иначе нет смысла хранить их в кодовой базе.

— тесты гарантируют, что если я вот прямо сейчас захочу наговнокодить побыстрому рядышком пару k LOC, то завтра все не упадет у всех

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

Изначальный посыл статьи верный
Я не знаю изначальный замысел статьи
Как я вижу, статья пытается, исходя из вымышленных препосылок, что-то утверждать
почему нет?

Да потому что вы тесты писать будете. Они же не сами сверху падают, как манна небесная. Это ваше рабочее время. Вы можете реализовывать фичи, а можете писать тесты.
Как я вижу, статья пытается, исходя из вымышленных препосылок, что-то утверждать

Посыл статьи — сначала считайте, нужны вам тесты или нет. И есть достаточно высокая вероятность, что арифметика покажет, что не нужны. Ничего хитрого, всё очевидно. Так оно и есть, экономически для множества (и пожалуй, я не ошибусь, если скажу, что даже для большинства) проектов тесты не выгодны, и их создание не окупается никогда.
Вы можете реализовывать фичи, а можете писать тесты.
А кто подтвержит, что фича написана правильно?

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

В 90% проектов с этим успешно справляется команда тестировщиков. Схема «провели UI-тесты и подтвердили, что всё нижележащее отработало согласно ТЗ» вполне жизнеспособна, и за исключением ряда специфических продуктов обеспечивает достаточный уровень качества.
в статье нет основы для таких расчетов. Потому как посчитать — также не ясно

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

И нет
Емпирика также никого не интересует в расчетах, где есть деньги

Я уже не говорю про то, что трудозатраты посчитать крайне сложно

И вернемся к основному вопросу
Как делать частичный рефакторинг, когда у вас есть только функциональные тесты?
И нет
Емпирика также никого не интересует в расчетах, где есть деньги

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

Я не понимаю смысла этого вопроса. Берете и делаете. Вам тесты для выполнения рефакторинга не нужны. Они играют лишь вспомогательную роль, упрощая локализацию возможных ошибок при рефакторинге. У вас есть какие-то сомнения, что получится провести успешный рефакторинг без автоматизированной локализации возможных ошибок?
Как берете и делаете?
Конкретнее
Так некуда уже конкретнее. Возьмите любой известный вам пример рефакторинга, это именно то, что я имею в виду.
Чем дальше, тем меньше конкретики
Жаль
UFO just landed and posted this here
Да, разумеется. Но заметьте, даже в этом случае, будучи на стороне разработки, я бы все равно подумал, на что мне стоит потратить свое время и чужие деньги — на написание тестов, или на новую функциональность.

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

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

Наверное, если заказчик платит за тесты, то надежность и качество для него не на последнем месте.

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

Число строк/методов/классов/файлов? Отработанные часы? :)


Принесённый доход/прибыль?

Не обязательно, кстати. Могут быть и другие варианты:
— Заказчик не слишком компетентный в особенностях разработки, и ему просто продали разработку тестов.
— Заказчик не знает, что он платит за тесты. Стоимость их разработки просто тихой сапой включена в общий эстимейт.
И то, и другое встречается нередко.
Хорошая статья, показывающая что есть ложь, есть наглая ложь, а есть статистика.

Рассмотрим первый случай. Вероятность сбоя возьмем равной 33%, то есть инцидент, способный завалить их сайт надолго может произойти примерно раз в три года, что не так уж плохо.

Один инцидент раз в три года? Ну это примерно частота, с которой падают такие сервисы, как GMail или YouTube. Сервисы типа Steam или там Deutsche Bank — падают в разы чаще. И, разумеется, все эти компании вбухивают хорошие деньги в свои сервисы и в юниттестирование.

Сказать, что вы получаете вероятность сбоя за год 33% (без юниттестирование, напоминаю) — это примерно как рассмотреть вариант «мы наняли 500 водителей-такжиков по объявлению, ну а они, конечно, без тренировок и дополнительных затрат ездят чуть получше, чем Шумахер… ну… большинство из них».

То есть посыл статьи прост: если вы наняли супергениев, которые, по сравнению со средними программистами Гугла и Яндекса — как Эйнштейн по сравнению с трёхлетним ребёнком (работники Гугла — тут трёхлетним дети, это если вдруг, кто не понял)… то да, вам, видимо, юниттестирование не нужно.

Вот только… где вы берёте супергениев, которые без всяких тестов дают такую же надёжность, как у лучших компаний в мире со всякими многоуровневыми тестами, SRE и прочим… секретом не поделитесь?

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

Нормальные — не верят.Но таки хотят цифр, выраженных в денежных знаках…
Кстати, почему вы думаете, что youtube падает раз в три года? Скажем для меня, как для пользователя, его надежность выглядит совсем не так — примерно раз в неделю я наблюдаю, как отдельные видео не грузятся, и вместо них лишь сообщение об ошибке на черном фоне. Так что я бы не преувеличивал надежность гугловских сервисов.
Так что я бы не преувеличивал надежность гугловских сервисов.
Вот только по статистике — они надёжнее Netflix'а и многих других.

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

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

Когда раз в два дня какое-нибудь видео зависает — в газетах о таком рядовом событии не пишут.

Если бы вы читали статью, то заметили бы, что речь идёт не о «зависающих видео, а о, цитирую:
инцидент, способный завалить их сайт надолго
К YouTube применяем те же критерии.
UFO just landed and posted this here
Обратите внимание, в примере речь идет о крупной компании с выручкой 25 — 50 миллиардов. Разумеется, у нее есть и тестировщики и грамотная стратегия для деплоя и автоматизированные тесты помимо юнит тестов.

Кроме того, ничто не мешает вам попробовать подставить в модель свои данные и это здорово, потому что мы уже переходим с уровня верю — не верю к конкретным показателям.
Разумеется, у нее есть и тестировщики и грамотная стратегия для деплоя и автоматизированные тесты помимо юнит тестов.
В таком случае нужно сравнивать не с затратами времени на юниттесты, а с затратами времени на другие виды тестов. Если у вас 30% времени тратится на униттесты и ещё 20% на функциональные, но функциональные покрывают 90% проблем, а юниттесты 99% — то эффект от юниттестов, тем не менее, гораздо выше, чем от функциональных. И лучше убрать как раз функциональные, которые, вроде как, дешевле.

Писать нужно те тесты, которые больше подходят для компонента, который разрабатываете.

В сложных системах код отдельных фрагментов, которые могут быть покрыты юнит-тестами, местами достаточно примитивен. Поэтому юнит-тесты вырождаются до набора моков, проверяющих, что "А дёргает Б, после чего дергается В и Г" (я эту мысль пытался раскрыть в "Unit-тестирование в сложных приложениях"). Так что лучше как-раз таки оставить функциональные тесты, т.к. юнит-тесты не выявляют имеющихся проблем, а только свидетельствуют о том, что проверяемый фрагмент кода работает так, как думает, что так и должен работать этот фрагмент, программист, его написавший (это особенно характерно для TDD). Т.е., 99% покрытие кода юнит-тестами не говорит о том, что код работает правильно, а всего лишь о том, что тесты обнаружат изменения в покрытом коде, если он будет изменён без соответствующего изменения тестов.


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

А как делать рефакторинг без юнит-тестов?

Изменяете код и проверяете, что он отрабатывает так, как вы ожидаете. Если в вашем проекте вы не можете делать это без юнит-тестов, то рефакторьте с юнит-тестами. Если можете проверить без юнит-тестов — рефакторьте без.

Вы же понимает, что ето не ответ?

Вы же понимаете, что до появления юнит-тестов рефакторинг как-то делали?

Проверяя работоспособность программы вручную, разумеется.


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

Сочувствие требовалось бы, если бы так и продолжили поддерживать тот проект без рефакторинга...

В этой статье написано:


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

По мере разрастания проекта, времени на ручное тестирование будет оставаться все меньше и меньше…

Отсюда логически вытекает необходимость автоматизировать работу тестировщиков…

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

а затем змея начинает кусать свой хвост:


На практике чаще всего написанием юнит-тестов для собственного кода занимается сам разработчик.

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


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


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


До появления юнит-тестов рефакторинг делали невзирая на наличие тестов, довольствуясь ручной проверкой, затем проверку стали автоматизировать, затем появились функциональные и интеграционные тесты, а потом решили получить 100% code coverage и вышли на unit-тесты.


Простой вопрос, считаете ли вы, что полноценное юнит-тестирование должно на 100% покрывать код проекта?


Если вы не гонитесь за 100% code coverage unit-тестами, то мы с вами, скорее всего, имеем в виду одно и то же под разными названиями (вы называете это юнит-тестами, я — функциональными тестами).

Проблемы высокоуровневых тестов основных три:


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

Тем не менее, они что-то проверяют, в отличие от. А что проверяют юнит-тесты?

Юз-кейсы конкретного модуля.

Модуль в данном контексте это что — функция, класс, скрипт, набор скриптов? Как задаётся нужное состояние модуля, чтобы протестировать конкретное управляющее воздействие?

В зависимости от языка и архитектура понятия модуля могут сильно меняться. Насколько я знаю общая практика для ООП-языков: модуль — класс, тестируется вызов одного метода. Создаём инстанс класса, приводим его к нужному состоянию различными способами, вызываем метод, проверяем состояние.

Возможно ли функциональное тестирование отдельного класса? Если да, то чем функциональное тестирование отдельного класса отличается от юнит-тестирования отдельного класса?

По сути юнит-тестирование является подвидом функционального тестирования, если делить всё тестирование на функциональное и нефункциональное. Просто так исторически сложилось, что функциональным тестированием называется высокоуровневое тестирование функциональности всей системы "в сборе" или близко к тому.

Вот я и считаю, что достаточно функционального тестирования для проверки работоспособности модуля/приложения. Юнит-тестирование избыточно. Оно создаёт ложное ощущение контроля ситуации, не предоставляя таковой по сути. Нам не нужно проверять всё, нам нужно проверять только то, что действительно важно, на что есть спецификация (требования).

Юнит-тесты по сути формализованные требования к модулю. Нам не нужно проверять, что модуль соответствует требованиям к модулю?

Кто формализует требования к модулю в виде юнит-тестов? Разработчик, который пишет сам модуль? TDD подразумевает, что да. Если разработчик неправильно понимает, что должен делать его модуль, он создаст неправильный тест для которого напишет неправильный код. В результате, неправильный юнит-тест отработает правильно и покажет, что неправильный код работает, как и задумывалось. При рефакторинге придётся переделывать и код, и юнит-тест к нему.


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


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


Я сказал, что мог, и больше не могу ничего добавить к.

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

Это всё проблемы не тестов, а архитектуры. Например, высокоуровневый тест берущий ToDoMVC и проверяющий, что если добавить 2 задачи, одну завершить, а потом потыкать по фильтрам, то список задач будет правильно перефильтровываться:


// Создали инстанс компонента в тестовом контексте
const app = $mol_app_todomvc.make({ $ })

// Ввели текст в инпут добавления задачи
app.Add().value( 'test title' )
// Закоммитили добавление задачи
app.Add().event_done()

// Добавили ещё одну задачу
app.Add().value( 'test title 2' )
app.Add().event_done()

// Щёлкнули по чекбоксу завершения задачи
app.Task_row(1).Complete().event_click()

// Проверили, что изначально показываются обе задачи
$mol_assert_like( app.List().sub() , [ app.Task_row(1) , app.Task_row(2) ] )

// Перешли по ссылке "только завершённые"
app.$.$mol_state_arg.href( app.Filter_completed().uri() )
$mol_assert_like( app.List().sub() , [ app.Task_row(1) ] )

// Перешли по ссылке "только активные"
app.$.$mol_state_arg.href( app.Filter_active().uri() )
$mol_assert_like( app.List().sub() , [ app.Task_row(2) ] )

// Перешли по ссылке "все задачи"
app.$.$mol_state_arg.href( app.Filter_all().uri() )
$mol_assert_like( app.List().sub() , [ app.Task_row(1) , app.Task_row(2) ] )

Исполняется всего 3 миллисекунды.

Это вместе с ответом сервера? :)

Это на мокнутом сервере.

Создание моков вы не учитываете?

Пускай, тем не менее юнит-тесты на условной модели отработают быстрее чем на полном приложении.


Кстати, забыл ещё один минус высокоуровневых тестов — сложность локализации ошибки. Вот первый ваш ассерт вернёт ошибку, где будете искать её причину?

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


Пройдусь дебаггером по шагам. Можно подумать юнит-тесты каким-то чудесным образом могут сказать в каком именно месте функции ошибка.

DRY при написании юнит-тестов никто не отменял. Как и оптимизацию по скорости путём переиспользования (главное, чтобы изолированность тестов не нарушалась).


Юнит-тесты скажут в какой функции ошибка :) При хороших подходах к коду и тестам.


Ещё один минус высокоуровневых — не понятно кто и когда их должен писать, если несколько человек пилят одну фичу, ну там один UI, другой модель/стор, третий и/о сервисы и т. п.

Каждый свою часть тестирует.

Мокая остальные? Чем тогда это отличается от юнит-тестов? :)

Не мокая остальные.

Как UI разработчик будет тестировать, если стор ещё не готов, не мокая его?

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

Когда-то такой подход назывался "нисходящее программирование"...

"нисходящее" имеет негативные коннотации и звучит как какой-то бородатый легаси. А вот WhateverDriverDevelopment звучит как best pactice.

Ну примерно так и выглядит юнит тестирование. Замокали все зависимости и тестируем один модуль.

Вы доказать-то что пытаетесь?

Ну, собственно, для того тесты и нужны чтобы проверять, что код отрабатывает так, как ожидается. Конкретно юнит-тесты — обычно самый быстрый способ проверки. То есть наоборот, если не можете проверить с юнит-тестами, то тогда придётся как-то проверять без них. Вплоть до выкатки на продакшен и ожидания багов.

UFO just landed and posted this here
А тем кто не пишет на функциональных языках или языках со статической типизацией?
UFO just landed and posted this here
UFO just landed and posted this here
UFO just landed and posted this here
Юнит-тесты должны быть тривиальны
Тесты пишутся перед изменением функционала

По сути наш разговор свелся к тому, что, по вашим словам, стабильную разработку на ЯП кроме особенных вести нельзя.

И как бы мне не хотелось все писать на кложуре, пока не получается
UFO just landed and posted this here
Вы совсем не знакомы с тдд воркфлоу?

Тогда и хаскель никак вам не помогает или вы запутались?
UFO just landed and posted this here
Но давайте обсудим на примере. Вот пишете вы сами лично функцию сортировки, скажем. Timsort там какой-нибудь. Какие тесты вы на неё пишете?

  1. краевые случаи: пустой список, список из одного элемента, упорядоченный список, обратно упорядоченный список, список из одинаковых элементов;


  2. некоторое количество случайных перестановок;


  3. некоторое количество случайных списков с повторяющимися элементами;


  4. большой список (для проверки быстродействия);


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


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

А вы что, предлагаете вообще ни разу не запускать написанную функцию?


Ну хорошо, на том же Идрисе это возможно, но там на построение строгого доказательства работы как бы не больше времени потрачено будет. А что делать на "обычных" языках программирования?

Да не предлагаю, конечно. Но написание теста как минимум подразумевает, что есть потребность проверять этот код многократно. Я бы просто проверил работоспособность функции, а остальное оставил бы на откуп функциональным/интеграционным тестам. Если там где-то вылезет проблема с отображением данных, например, по производительности/потреблению памяти, тогда бы уже погружался в детали. Я не сторонник делать значительный объем работы просто так, «на всякий случай», вдруг оно там понадобится. Потому что опыт показывает, что практически никогда не понадобится, а в тех редких случаях, когда понадобится, можно доделать потом. Запустить проект раньше и усовершенствовать его, когда он уже работает и приносит деньги, это выгоднее, чем делать его сразу правильно, но запускать позже (естественно, при условии, что вы запускаете его в работоспособном виде, а не совсем кривую поделку). Это касается и избыточной заложенной в проект масштабируемости, и избыточных тестов, и т.п.
И естественно, вышенаписанное — это общий случай. Если эта функция сортировки будет являться краеугольным камнем в моём приложении, то и подход к ней будет иной.
Но написание теста как минимум подразумевает, что есть потребность проверять этот код многократно.

Так ведь в процессе отладки функции её именно что нужно проверять многократно.


Я бы просто проверил работоспособность функции

Так для того тесты и пишутся


Я не сторонник делать значительный объем работы просто так

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

В чем заключается значительный объём работы? В том, чтобы написать один класс и десяток заголовков методов?

Так не только заголовки ведь. Вам нужно подготовить тестовые наборы данных для каждого случая, входные и выходные для сравнения, продумать, какие assert'ы нужны, прописать их. Проверить всё это, т.к. тесты — такой же самый код, и там тоже могут быть ошибки. Это всё такая же работа, как и написание самой функции, причем достаточно объемная.
Я бы просто проверил работоспособность функции

Я имею в виду, в целом, а не обкладывать её тестами с краеугольными случаями со всех сторон.
Вам нужно подготовить тестовые наборы данных для каждого случая

А при проверке работоспособности функции это все, как бы, не нужно?


и выходные для сравнения

Не обязательно. При правильном построении исходных данных корректность сортировки можно проверить без "эталонных" выходных.


Тесты в пунктах 4 и 5 можно оставить и вовсе без проверки результата: не падает и ладно.


Проверить всё это, т.к. тесты — такой же самый код, и там тоже могут быть ошибки.

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


Я имею в виду, в целом, а не обкладывать её тестами с краеугольными случаями со всех сторон.

А кому нужна функция сортировки, работающая "в целом"?

А при проверке работоспособности функции это все, как бы, не нужно?

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

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

И тем не менее, вы не можете обеспечить то, что тесты у вас будут значительно проще проверяемого алгоритма. Это вообще не от вас зависит, а объективно от требований к алгоритму. И вы должны будете проверять и алгоритм и тесты.
А кому нужна функция сортировки, работающая «в целом»?

Например, тем, кто платит деньги за разработку и ждет от вас результат. Вы можете гарантировать работоспособность с вероятностью 99% за N денег и M времени, и с вероятностью 99.9% за N*2 денег и M*2 времени. Если ваш софт управляет не штуками вроде обсуждаемых по соседству Боингов-737, а относится к подавляющему большинству других приложений, частный случай отказа которых никого не убьет, и даже не нанесёт сколь-нибудь существенных материальных убытков, то первый вариант куда более логичен.
Ну вы же понимаете, что массив данных сам себя на сортировку не проверит. Что-то должно быть, или опорные данные для сравнения, или хотя бы проход по массиву для проверки его на отсортированность.

А в чем проблема прохода по массиву для проверки упорядоченности? Этому проходу всё ещё не нужен "эталонный" результат.


И тем не менее, вы не можете обеспечить то, что тесты у вас будут значительно проще проверяемого алгоритма.

Могу. Если тесты оказываются сложнее чем алгоритм — я обычно просто не пишу их. Но в случае с сортировкой это не так.


Вы можете гарантировать работоспособность с вероятностью 99%...

… а потом коллеги будут неделю искать баг в каком-нибудь безобидном месте, просто потому что не будут ожидать подвоха от функции сортировки, а когда найдут — заменят timsort на условный "пузырёк", потому что сроки давно вышли, а фиксить-то баг надо. В итоге все время, потраченное на реализацию сортировки, уйдет впустую.


И все из-за того что я пожалел получаса на написание теста.

И все из-за того что я пожалел получаса на написание теста.

А вы не напишете и не отладите все те тесты за полчаса. Вы реально потратите на них больше времени, чем потратили на ту сортировку. А ведь мы говорим только об одной функции, а сколько их ещё там будет. Полноценное покрытие тестами в среднем удваивает сроки разработки для большинства проектов. Выгода появляется лишь там, где очень сложное поведение и много внутренних взаимосвязей, либо что-то с высокой стоимостью отказа. Тесты однозначно нужны, если вы пишете трансляторы, движки СУБД и тому подобный софт, или какие-то платные сервисы, работающие в режиме 24х7. Но если вы относитесь к 99% остальных программистов, которые пишут бизнес-приложения, сайты/интернет-магазины, мелкие аппликухи для смартфонов и т.д., то расходы на юнит-тесты у вас не окупятся, даже со всеми теми возможными «коллеги неделю баг искали». Поэтому тут здравый подход, как по мне, следующий: заказчик платит — делаете. Вам-то только лучше, вам за это заплатили. Заказчик не платит, или пишете для себя и за свой счет, пишите без юнит-тестов и не переживайте, вы ничего ровным счетом не потеряете.
А вы не напишете и не отладите все те тесты за полчаса.

Если ошибок не будет — напишу и отлажу. А если будут — значит, тесты были написаны точно не зря.


А ведь мы говорим только об одной функции, а сколько их ещё там будет.

И правда, сколько подобных (т.е. алгоритмических) задач у меня будет в типичном проекте? Моя личная статистика — в среднем 0,1 на проект...


Выгода появляется лишь там, где очень сложное поведение

А у сортировки, что, простой алгоритм?


Напомню, свою сортировку нужно писать только в том случае, когда находящаяся в стандартной библиотеке почему-то не подходит. Это автоматически отметает все простые алгоритмы.

UFO just landed and posted this here
Вам потребуется убедиться, что алгоритм не выкидывает дубликаты и не добавляет их

Ещё один проход по массиву. Ну прямо очень сложно...


что он пользуется компаратором, в него переданным, а не случайно подвернувшимся под руку operator<

Ну, на том же C# это попросту невозможно, если алгоритм обобщенный. На плюсах можно структуру-обертку сделать без оператора <. Или можно использовать компаратор, который не совпадает с естественным порядком, например x * 3 % n < y * 3 % n


Где-то в процессе вам придётся в тестах научиться подсчитывать и группировать элементы согласно предикату, что уже начинает плохо пахнуть

В языке с мутабельными массивами нет необходимости делать так сложно.

UFO just landed and posted this here
Только вот как теперь доказать, что этого набора тестов достаточно?

Тесты — не про доказательства. Тесты проверяют, что не наделано глупых ошибок.


Не зная, что внутри функции, вы не сможете определить, что надо потестировать массивы разного размера, и где проходит эта самая граница разности

Тут согласен, надо достать эту константу из алгоритма и учесть ее в тестах.


Проще доказательство написать, ей-богу.

Не уверен. На языках с формальной верификацией придется долго объяснять компилятору тривиальные вещи (вы и сами на это жаловались), а на остальных ваше "доказательство" тоже может содержать ошибки.

UFO just landed and posted this here
Проще доказательство написать, ей-богу.
если проще, то напишите

Делать рефакторинг с другими типами тестов.

Как делать рефакторинг с помощью функциональных тестов, если наружу у данного блока торчит одна ручка?

Пример блока с одной ручкой.

Если наружу у модуля торчит только одна ручка — то тем проще. Нужно описать подробно, что она делает и все юниттесты выкинуть за ненадобностью.

И да — я это вполне проделывал.
Отлично

Так как проверить корректность его работы?
Сравнением результатов работы со спекой на эту самую единственную ручку, очевидно.
Как?
Конкретнее пожалуйста
Я, честно говоря, не понимаю, в чём тут проблема. Если наружу торчит одна ручка, это вообще идеальная ситуация. Вы сразу увидите, что блок работает не так. А чтобы локализовать проблему, возникшую при рефакторинге, просто возьмите вместо юнит-тестов глаза и мозг. Вы же не делали рефакторинг с закрытыми глазами, случайно тыкая по клавишам? Значит, вы прекрасно знаете, что изменилось, и что могло поломаться. Ситуация, когда есть какой-то код, настолько сложный, что при рефакторинге вы понятия не имеете без тестов, что отвалилось, мне сдаётся совершенно надуманной. Хотя бы по той причине, что если вы не в курсе, как этот код работает, у вас есть ровно 0 аргументов за его рефакторинг и 100% против.
Вы же не делали рефакторинг с закрытыми глазами, случайно тыкая по клавишам?
В том-то и дело, что очень часто люди именно так и делают рефакторинг. Просто рандомно переносят строчки из одного места в другое, запускают набор тестов и «убеждаются, что всё работает».

Конечный результат — куча странных багов, которые никто не может поправить, так как откуда они возникают — непонятно, а как программа в принципе работает — не знает никто. То есть вообще никто.

Ситуация, когда есть какой-то код, настолько сложный, что при рефакторинге вы понятия не имеете без тестов, что отвалилось, мне сдаётся совершенно надуманной.
Вы слишком хорошего мнения о «комбинаторных программистах». Я наблюдал ситуацию, когда проект был нашинкован в лапшу с помощьё Guice и ответ на вопрос «откуда тут вообще берётся e-mail заказчика» не знал никто. Соотвественно попытка исправить баг, при котором почта улетала «не туда» — превращалась в тот ещё квест. При этом юниттестов было полно, всё было отлично покрыто ими. Для того Guice и прикручивался.

Хотя бы по той причине, что если вы не в курсе, как этот код работает, у вас есть ровно 0 аргументов за его рефакторинг и 100% против.
Наоборот — это было преподнесено как достижение: нам не нужно знать что и как тут работает, потому что у нас покрытие тестами больше 95%. Тот факт, что при этом, почему-то, никто не может баг исправить, за разумное време, никого совершенно не смущало.
Хотя бы по той причине, что если вы не в курсе, как этот код работает, у вас есть ровно 0 аргументов за его рефакторинг и 100% против.

Как раз наоборот. :) Если не можешь понять что делает код — его нужно рефакторить. А если понятно, то особо и рефакторить незачем.

Я тут бы поспорил. Мы же все как художники, вечно недовольны работой своих предшественников, и только ищем повод, чтобы её переписать :)
А это такая себе ловушка, которая обычно создает куда больше проблем, чем решает.

Это да. Но я больше о производственной необходимости, а не о повышении самооценки :)

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

Бывает объективно сложная бизнес-логика, в которой лень и некогда разбираться. Бывает 100500 краевых случаев, любой из которых может легко сломаться.

Бывает. Но тут как раз второй вопрос — зачем её вообще рефакторить, если вам банально с её работой некогда разбираться?

Чтобы рефакторить некоторую часть не обязательно разбираться в ней всей.

От функциональных отказываться нельзя никогда

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

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


Уменьшение вероятности ошибки далеко не единственный результат юнит-тестирования, строить выводы о миллиардных убытках на выбранной модели весело, конечно, но не совсем корректно.

Буду признателен, если поделитесь статистикой, подтверждающей это.

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

Но ведь именно это главный аргумент сторонников TDD, которые были упомянуты в самом начале.
Если кратко резюмировать их логику — написание тестов отдельно/после кода требует сильно больше времени и сил через правильное написание кожа и тестов в параллель.
Те результаты статьи совершенно правильные — написание тестов классическим образом — пустая трата времени и денег.
Быть может их просто как-то по другому писать надо?

Основные плюсы юнит-тестов вы так и не озвучили.
Дело в том что в отличии от других уровней контроля качества, юнит-тесты лежат максимально близко к коду. Если человек пишет юнит-тесты, то это непременно влияет на процесс разработки.
Вы говорите насколько написание юнит-тестов увеличивает затраты на задачи,
но не говорите насколько они уменьшаются при наличии ранее написанных тестов.
Можно вспомнить хотя бы про то, что наличие юнит-тестов позволяет не бояться вносить изменения в код, а так же про то что юнит-тесты заставляет программиста писать код лучше (кривой код очень сложно покрывать юнит-тестами).
А еще юнит-тесты можно применять в качестве документации к модулям системы, у новых участников команды не будет возникать вопросов как использовать тот или иной компонент.
А еще… Да много еще чего.
Короче говоря, юнит-тесты это больше про процесс разработки, а не про контроль качества.
Оглядываясь назад в те времена когда я не писал тесты, мне иногда кажется что я был очень смелым.
Но на самом деле тут должна быть картинка «Слабоумие и отвага» с Чипом и Дейлом.
Сегодня выполняя задачу без тестов я чувствую себя лгуном, так как говорю что задача сделана, а гарантий этого я не даю даже самому себе.

Да вы и сейчас не даёте никаких гарантий, ибо юнит-тесты не гарантируют, что всё в сборе будет работать.

Гарантия это про несение ответственности, а не про полное отсутствие багов.
Я конечно догадался что вы про интеграционные тесты. Просто на низком уровне интеграционное и модульное тестирование не так уж и сильно отличается. Если я мокаю в тесте одну из зависимостей с сохранением интерфейса и проверкой передаваемых в нее параметров, это все еще модульный тест или уже интеграционный?
Я читал вашу статью и хотя в целом я поддерживаю разделение элементов системы на модули и компоненты, концептуально это не играет роли, сотрите из своей схемы модули и компоненты превратятся в те же модули. К тому же, ваша схема неверная, у вас связи от модуля идут только к одному компоненту, в реальной жизни это не так. К примеру вызов библиотечной функции можно считать обращением к модулю и вряд ли вы захотите использовать библиотеку не покрытую тестами.
Я это к тому, что если считать компоненты независимыми черными ящиками со строгим контрактом, то ваши компонентные тесты вполне можно считать модульными, а если знать о наличии модулей, то эти тесты по определению интеграционные/функциональные.

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

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

У модуля вполне однозначное определение — это кусок исходника, а не часть приложения. И стандартную библиотеку тоже над мокать.Тем более, если эти стандартных библиотек больше чем одна. Многие понимают бессмысленность этого действа, поэтому для стандартной библиотеки делают исключение. Некоторые делают такое исключение и для сторонних библиотек. И лишь немногие идут до конца и понимают, что их библиотеки от не их библиотек ничем принципиально не отличаются и перестают мокать всё подряд. Однако уже с первой незамоканной зависимостью тест по факту перестаёт быть модульным. Но по инерции люди продолжают называть модульными тесты, где не замокана ни одна зависимость.

Окей, we need to go deeper.
Не нужно ли при написании «истинного» модульного теста еще мокнуть реализацию языка программирования, ведь от нее тоже много чего зависит? И если мы этого не делаем, то наш модульный тест по факту опять интеграционный.
И даже если мы выполним эту задачу (хоть это и невозможно), можно пойти еще глубже и начать думать что для «истинного» модульного теста нам еще нужно мокнуть «железо» на котором этот тест запущен.

Мы это с ним уже обсуждали. Модульных тестов не бывает. А так как их не бывает то и модулей не бывает (см его статью — модуль это то что можно протестировать отдельно). Ссылки на Фаулера с sociable unit tests не катят.

А давайте не утрировать?
Называть компонентные тесты интеграционными тоже не корректно.

У вас свое собственное определение компонентов. Обычно это — единица развертывания

А вы уверены, что это не у вас собственное определение?
Собственно суть компонента в том, что его можно запустить независимо от соседних компонент и компонент выше уровнем.

Ну и последнее, написание юнит-тестов не всегда замедляет решение задачи, если задача это не какая-то элементарная вещь, то новые юнит-тесты начинают работать еще до завершения текущей задачи и тем самым ускоряет ее решение, хотя тут вы вероятно мне не поверите.
Это все отлично. Только вот можете ли вы описать эту выгоду в денежных еденицах? Не «значительно лучше» и т.п. что есть качественная оценка… А количественная, например — внедрение юнит тестов позволит нам решить эту задачу, нанаяв всего 7 человек, вместо 15 при условии неизменного срока разработки? Или внедрив юнит тесты мы уменьшим время простоя нашего сайта с 48 часов в год до 8 и это принесет нам ххх дополнительной прибыли, которая будет больше, чем затраты на внедрение и поддержку юнит тестов…
Данную оценку вполне можно дать при достаточности исходных данных… Но их как правило достаточно не бывает.
Хотя вру, бывает — когда заранее известно что разрабатываемая система временная и будет выкинута или не будет никак дорабатываться. К примеру, системы-костыли необходимые только на переходный период изменения какого-то бизнес-процесса, мосты между legacy-системами, всякие хакатоны и т.п.
Прошу заметить, что я не утверждаю что в этих системах не нужны тесты, я говорю о том, что «стоимость» написания или ненаписания тестов тут можно как-то оценить.
Если же система предполагает развитие, во что она потом превратится и нужны ли ей будут тесты большой вопрос, может быть бюджет на систему закончится еще до ее внедрения…
Без понимания «выгоды отказа от тестов в денежных единицах» и вы не способны достаточно внятно подтвердить точку зрения ненужности или вредности юнит тестов. Это проблема обоюдная, как у противников так и у сторонников покрытия тестами.
Без точных количественных оценок обе стороны будут просто гадать и делать субъективные оценки.

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

>Если человек пишет юнит-тесты, то это непременно влияет на процесс разработки.
Э… нет. Ну то есть, может и влияет — но не обязательно. Я много раз наблюдал код, написанный коллегами, который допустим нарушает принцип единственной ответственности. Например, один и тот же метод сначала строит список каких-то объектов (файлов на диске), а потом этот список обрабатывает (сортирует, фильтрует, ищет самый новый файл). На этот метод написаны «юнит тесты», которые сначала создают какие-то файлы, потом вызывают тестируемый метод, а потом проверяют правильность результата. Логика обработки — весьма мутная.

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

В процессе рефакторинга модуль был разбит на две части: получение списка файлов, и его обработка. Старые тесты отправились на помойку, так как фактически ничего не проверяли. Логика стала значительно более прозрачной, и гибкой.

Уже в процессе review и рефакторинга выявлено пара багов в той части, которая реально отвечает за обработку — скажем, самый новый файл нужно было искать по другому признаку, не дате модификации с точки зрения ОС, а имени файла и другим метаданным.
Именно такой рефакторинг был проведен с прицелом на то, чтобы итоговый код можно было тестировать. Но новые тесты я писать не стал, потому что проблем больше не наблюдается, а код стал тривиален настолько, что у меня теперь есть уверенность в его правильности.

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

Инструменты обеспечения гарантий :)

Ну вы знаете… Если у вас отличная архитектура и "чистый код", то что вы будете проверять юнит-тестами? Там и интеграционные тесты не нужны… Теоретически...

Собственно логику. Что чистый код реализует то, что нужно.

UFO just landed and posted this here

Какая спека? Тесты для меня (любые) — тоже своего рода спека. Масштаб, уровень только отличается. Юнит-тесты — спека на юниты :)

UFO just landed and posted this here

Обычно из придётся в терминах предметной области. Юниты бизнес логики должны быть тоже в терминах предметной области.

UFO just landed and posted this here

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


Не "a + b должно приводить к генерации add ax, bx, а "a + b это сложение, сложение должно приводить г генерации add ax, bx". Человеку неудобно будет работать со спекой, где грамматика и кодогенерация будет в одной большой куче.

UFO just landed and posted this here

Зависит от уровня спеки. На практике очень часто встречается, что бизнес-спек нет вообще, хоть в "аджайл-стартап" окружениях, хоть "на постсовком заводе" — это вопрос бизнес-культуры. А специфицировать ли создаваемые технические артефакты — вопрос культуры инженерной. И тесты — один из способов спецификации.

Обычная история — даже самый качественный инструмент, примененный неправильно, не решает проблемы, а только создает новые.
Ну да, можно и так упростить. Хотя я скорее имел в виду слегка другое — что инструмент у нас есть не один, и иногда следует выбрать вместо молотка отвертку.
UFO just landed and posted this here
Да, у меня (и многих до меня, и многих после, я уверен) был уже где-то тут комментарий о том, что достаточно гибкая система типов позволяет вообще не писать некоторые классы юнит-тестов, потому что начинает включать эти классы по факту своего (системы типов) существования. Точнее, предоставляет централизованную реализацию таких тестов.

Какие классы юнит-тестов позволяет не писать?

Большинство тестов на принимаемые и возвращаемые значения — уже в большинстве статически типизированных языков. При увеличении гибкости можно переносить в систему типов гарантии самих значений.


https://habr.com/ru/post/431250/#comment_19426486

Обычно тесты на типы принимаемыемых и возвращаемых значений не пишут. Да и сами функции/методы типы не проверяют специально чтобы выбросить TypeError, как правило. В лучшем случае явное приведение, а чаще неявное, по сути undefined behavior.


Ну и некоторые динамически типизированные языки довольно строго типизированные и TypeError выбросят при каких-то ошибках типов. Писать на эти кейсы тесты — проверять средства языка на ошибки.

Обычно тесты на типы принимаемыемых и возвращаемых значений не пишут.

В таком случае, вы верите, что там нет багов.


по сути undefined behavior

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


Для сравнения, для функции


getLength: Iterable[any] => PositiveInt

из моего примера теми самыми средствами языка (которые мы не тестируем) гарантируется 100% определённость в рантайме, потому что проверки системы типов не позволяют коммитить в рантайм код, который мог вызвать неопределённое поведение. И "хитрых патчей" с ней в центре тоже не возникнет, что, опять же, благо.

В таком случае, вы верите, что там нет багов.

Нет, не верю. Любые средства проверки на баги лишь уменьшают мою оценку вероятности их наличия. Я верю, что баги есть всегда, просто не всегда их получается обнаружить. Как в моём коде или коде коллег, так и сторонних средствах от средств проверки на баги до железа.


Верно, а если у функции есть диапазоны значений, на которых она не определена, то эти диапазоны следует фиксировать и в документации, и тестами.

В идеальной вселенной следует, да. В реальной хорошо если все области определения из требований документированы и протестированы.


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

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


В целом, я ничего не имеею против систем типов. Сам стараюсь использовать типизацию где могу, пока это не усложняет написание и, главное, чтение кода. Главный недостаток мощных систем типов в коде для меня — это сложно, а значит долго и дорого, как разрабатывать, так и потом развивать и поддерживать. Особенно в поддержке/развитии — рано или поздно, замотивированный "эффективным менеджером" разработчик (я — рано), начнёт делать явные приведения в статике, фейковые тайпгварды в рантайме и т. п. чтобы уложиться в приемлемые сроки, когда чужая (в том числе прошлого себя) проверка на типы не даёт выполнить задачу при наличии веры в то, что передаешь-обрабатываешь правильно, просто объяснить это тайпчеккеру не получается быстро из-за того, что объявления типов занимают больше места чем сам код и являются по сути write-once кодом.


Вот из вашего примера: я люблю пустые коллекции и обязательно где-то приведу свой условный NonNegativeInt к вашему PositiveInt или наоборот.

рано или поздно, замотивированный "эффективным менеджером" разработчик (я — рано), начнёт делать явные приведения в статике

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

Сколько стоят юнит тесты?

они бесценны, это часть процесса программирования, а не включаемая опция.

Их можно пропустить, если цена ошибки небольшая… можно просто делать смоке тесты бещ формальностей.

Их можно пропустить, если цена ошибки небольшая…

А можете привести формальную методику вычисления стоимости ошибки пожалуйста?

Да ну вот, например:

Спасибо. Замечательно! А какую из этих методик лично вы или ваша команда используете для оценки рисков в проекте? А если не секрет во сколько обходится сама оценка рисков?

Управление рисками и FMEA. Да, на анализ рисков ресурсы тратятся, сколько не могу точно сказать.
Все риски, отслеживаются, делаются митигейшен планы, смотрится вероятность, категория, влияние, тип… Для критических компонентов, есть отдельные требования, тесты и специальное ревью — Safety Review.
Но нам без этого никуда, потому что это должно быть по процессу для разработки устройств и софта по стандарту IEC 61508.

Если же говорить про риски, то, их определяют еще до начала проекта, когда проект даже не начался, а только смотрится его необходимость для бизнеса, в том числе проводится симуляция, т.е. что будет, если пролетим с прогнозом в меньшую строну, в большую. Что будет, если риск сработает. Но обычно только 3 кейза смотрится, типа все плохо, все нормально(по плану), и все будет очень хорошо (Хотя это не всегда хорошо, потому что, например, производство может не справиться..) Это все делается в системе, так что, главное данные правильно занести и риски определить. На основании симуляции, делается вывод какие шаги процесса можно пропустить. А шагов там, если не соврать более 100, и понятно дело ни в одном проекте еще все 100 шагов никто не делал :)
Например, вот для софта на этапе еще до кодирования и разработки архитектуры:
Анализ влияния на безопасность
Требования к программному обеспечению
Пир ревью
Требования к Программно-аппаратному интерфейсу
Пир ревью
Спецификация цифрового протокола
Пиир ревью
План модульных тестов
Пир ревью
План системного тестирования
Пиир ревью
План тестирования критических к безопасности компонентов
Пиир ревью

Процессы программирования разные бывают. И программирование, использующее тесты, определенно отличается в цене от программирования, их не использующего. Вопрос только в какую сторону.


Так что пусть это и не опция, но свою цену они имеют.

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

А вы хорошо знаете тех, кто вас минусовал?

Вопрос только в какую сторону.

А там нет универсальной стороны, что самое интересное. Зато есть очень многоарная функция цены от длительности разработки, качества исполнителей, качества постановки задачи, кривой обучения исполнителей деталям требования и "что заказчик действительно имел в виду", погоды, да и один Яхве знает чего ещё. Нелинейная. И при этом большая часть её аргументов становятся исчислимыми только в некрологе проекта, да и точный вид уравнения неизвестен пока никому (или очень хорошо скрываем).

Я согласен. Причем на мой взгляд, это одна из самых приятных частей процесса программирования. Ибо тест представляет собой проектировочный чертеж, черновик, технический набросок части кода, которая будет затем написана.


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

Хочу еще добавить пример. Firmware для промышленных датчиков, цена ошибки: если компания делает 100000 датчиков в год по 300 доларров за штуку, то пропущенный баг, который мог быть отловлен на этапе юнит тестирования, принесет только прямых затрат на 30 млн долларов. А если еще посчиать репутационные убытки и падение продаж из за этого в следующих годах… то и все 100 млн долларов и возможно полный крах компании. Поэтому, юнит тесты для ПО таких датчиков нужны 100%.

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

Слои юнит тестирования не исключают друг друга.
Тем же датчикам надо ещё физические тесты устраивать, когда жестокая реальность(кривой пайки) потопчется по полностью протестированному коду.

Дело в том, что это два разных вида тестирования.

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

Системное тестирование — это черный ящик, тестировщик понятия не имеет как работает программа, он просто смотрит, что при подаче давления столько-то, на выходе датчик выдает расход столько-то и это укладывается в погрешность расчета. Он не может подать ни бесконечность, ни NAN, и о понятия не имеет как считает функция…

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

Возможно пример с датчиками всего лишь достаточно прост, и в его случае в принципе всего два уровня — юнит-тесты и системное тестирование. Я говорил про более общую ситуацию.

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

Количественная оценка средних временных затрат на написание юнит-тестов (около 30%) также как и оценка положительного эффекта, который они дают в дальнейшем (в виде снижения затрат на последующую модернизацию и развитие проекта) была у Роберта Мартина в Clean Architecture. Не уверен, но вроде бы он там ссылался на исследования.
На мой взгляд, это главная задача юнит-тестов — поиск регрессии. И этот эффект проявляется в будущем, а, значит, также должен быть учтён, дисконтирован и приведён к настоящему моменту. По аналогии с NPV.

Типы избавляют от необходимости писать 90% юнит-тестов. При этом влияние на качество кода они оказывают не ниже, а документация из типов намного лаконичнее и понятнее.

UFO just landed and posted this here

Именно по этому самые глюченный системы были написаны на строго типизированных языках.

UFO just landed and posted this here
Насколько я знаю Curiosity написан на state-charts, которые вообще не так чтобы язык программирования, зато надежности дает — огого! Boingи вроде летают на Rational Tau, которые в ядре своем — SDL диаграммы.

Окей — в Rust например система типов достаточно ясно дает понять, когда ты не прав. Но то никоим образом не защищает от того, что бы просто делаешь что-то не так чтобы очень правильно.

UFO just landed and posted this here

Для автомобилей пишут на JS


Ну и что-то мне подсказывает, что довольно много прошивок различных космических аппаратов было написаны или на нетепизированных ассемблерах или на очень слаботипизированном Си.

UFO just landed and posted this here

Деталей не знаю, но доступ к датчикам автомобиля (скорость, обороты, открытые/закрытые двери, уровень топлива в баке и т. п.) точно был.


P.S. Python — строго типизированный язык, кстати.

UFO just landed and posted this here

Знаю, но не скажу. :) Но скорее второй вариант.


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

Python сильно типизированный язык, правда динамически.

Очень интересно почитать когда система типов покрывает 90 и особенно 100% тестов.

Типы могут помочь найти некоторые ошибки, хорошим примером был бы Mars Climate Orbiter когда накосячили с единицами измерения. Система типов не очень поможет с ошибками в логике (написал < вместо > ) либо расчетах (неправильный знак, константа, логика) кроме случаев неправильной размерности единиц.

Система типов может дать ложное чувство что раз собралось то правильно работает, в итоге код на питоне но покрытый тестами (без них в питоне никак) может быть надежнее.
UFO just landed and posted this here
Или что ваш локфри-алгоритм не имеет рейс кондишонов, ABA и прочих прелестей.

А не посоветуешь, что почитать про то, как такое верифицировать? В Software Foundations, например, так далеко не заходят.

UFO just landed and posted this here

Крайне редко приходится писать юнит-тесты на типы на js и php. И обычно там, где типы не помогут: на границах тестируемой системы. А по опыту работы с TS — документация из типов так себе, как только выходит за уровень скалярных типов, классов/интерфейсов и простейших дженериков. Иной раз кучу времени тратишь на то, чтобы просто скомпилироаалось, делая "фейковые" проверки на типы в рантайме. Банальная работа с json restish ендпоинте может превратиться в боль, если делать честные проверки.

Банальная работа с json restish ендпоинте может превратиться в боль, если делать честные проверки.

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

Да просто тип описать, соблюдя инварианты, чтобы к нему привести уже может быть больно. А ещё больше боли прочитать то, что описал.

А что у вас там за типы такие болезненные?

TypeScript. Одни наследуемые фабричные методы чего стоят.

type Rule<T> = (value: T) => bool;

class Validator<T> {
  private constructor(private rule: Rule<T>) {}

  public static create(this: {new(): Validator<T> }, rule: Rule<T> ): Validator<T> {
    return new this(rule)
  }

  public static createAlwaysValid { this: { create(): Validator<T>}}: Validator<T> {
    return this.create((value: T) => true )
  }
}

(писал без IDE)
здача сделать наследника StringValidator extends Validator чтобы фабричные методы возвращали тип StringValidator

Я не очень понял зачем вам отдельные объекты для валидаторов. Тем более создаваемые через фабрики.

Ну не буду же копипастить рабочий код. Что в голову пришло. Замените Validator на любой другой класс.

А на любом другом классе вам нужно принимать тип this в качестве статического параметра:


static make< Instance >( this : { new() : Instance } ) : Instance {
    return new this
}

Я так и сделал в create. Беда в том, что конструкция { new(): Instance } ) говорит компилятору ровно то, что говорит: new вернёт инстанс конкретного класса. Если не переписывать create/make в наследниках, то для тайпчекера их вызов будет возвращать инстанс родителя, а не наследника. Если где-то нужно использовать не new а create/make, то нужно его указывать вместо new как тип this. Ну нет в тайпскрипте (до 3.0 100%, сейчас 90%) простого способа указать, что new или статический метод класса возвращает инстанс того класса, на котором вызван, а не накотором определён. В моих терминах — нет в тайпскрипте аналога static и даже self типов PHP.

Вы невнимательно посмотрели мой код :-) Обратите внимание на место объявления статического параметра.

Первый параметр статического метода. Не вижу особой разницы, кроме того, что у вас Instance обычный типа, а у меня Validator — дженерик. Разве ваш код выдержит наследование без переопределения make в наследнике?


Ну и вторую проблему как решать, если в статическом методе нам нужен не new, а другой статический метод, а лучше все :)

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

Давно есть https://www.ecma-international.org/ecma-262/5.1/#sec-4.3.27


Плохо он учитывает в случае методов возвращающих инстанс класса, которому метод принадлежит. Нет конструкции, позволяющей указать в классе, что возвращается инстанс класса, в контексте которого метод вызван. Псевдопараметер this в таких случаях костыль, причём очень трудоёмкий костыль.


Причём в javascript всё с типами в таких кейсах нормально.

У меня много тут проблем. Одна из них: this тут не имеет тип класса.
Не работает


    static make< This >( this : new() => This ) {
        return new this
    }
    static make2< This >( this : new() => This ) {
        return this.make()
    }
}

Вот спасибо! Занёс в снипеты в раздел "это нужно просто запомнить". Согласитесь, что конструкция неочевидная и в мануале ничего об этом нет (по крайней мере найти не понятно как). Я верю, что по частичкам из разных глав как-то можно собрать, но для частого случая это как-то перебор.

Формально да, обычное, но дело не в том как работает. Дело в том, что в голову должен прийти такой вариант комбинации минимум четырёх концепций, да ещё в условиях когда в мануале для этого указана только две https://www.typescriptlang.org/docs/handbook/generics.html#using-class-types-in-generics

Всего-то возвращаемый тип добавить (: This)...

ну конечно же не избавляют. тесты проверяют логику работы, а не просто, соответствие типам. + дают возможность переосмысления написанного кода. разумеется они замедляют скорость разработки в моменте. но они дают возможность регрессивного тестирования всего написанного функционала. что касается типов, то они, в особенности приправленные дженериками всегда будут увеличивать когнитивную сложность кода.
За 13 лет работы я прошел этапы восприятия юнит-тестов от «что это вообще» до «без некоторого разумного покрытия юнит-тестами модуль не считается разработанным».
На том и стою.
Вот только не надо обобщать.

Во-первых, есть типы, и языки со строгой типизацией. А также другие инструменты, типа статического анализа, или code review например.

Во-вторых, бывают разные модули. Скажем, вполне реальный пример — модуль, который генерирует SQL запросы, SELECT нетривиального вида. Примерно с 10 разных видов. Попробуйте написать assert на то, что получившийся запрос является правильным, а я послушаю.
Попробуйте написать ассерт на то, что MS Word работает правильно??

В вашем случае это не один ассерт, а интеграционное или функциональное тестирование. Юнит-тестами надо покрывать каждый метод (функцию). Я верю, что у вас этот модуль не в одной функции на 100500 строк.
Этот модуль — 50 строк на каждый вид запросов, т.е. всего примерно 500.

>В вашем случае это не один ассерт, а интеграционное или функциональное тестирование.
Я именно это и хотел продемонстрировать. Это вполне замкнутый модуль, и довольно небольшой. И мы ни с кем тут не интегрируемся непосредственно, на выходе просто строка запроса. Но написать сюда нормальный юнит тест, чтобы не ставить рядом четыре разных базы данных — довольно проблематично. Поэтому тестируется он только в составе всего приложения.

Плохая архитектура? Ну да, наверное, местами. А где взять хорошую, если мы реально работаем с базами, с разными, и должны учитывать их специфику, которая выражается в том числе в разном синтаксисе запросов?
Генератор sql запросов — интерфейс.
Генерация sql запроса для разных баз выполняется разными имплементациями генератора.
Каждая имплементация имеет свои собственные тесты.
Как результат — все хорошо с тестами.
>Как результат — все хорошо с тестами.
Да? Ну так я жду от вас ассерт, что созданный запрос содержит именно нужные колонки, например. И нужные условия внутри where.
Возможно мы с вами по разному понимаем генератор sql-запросов.
Ниже VolСh пример привел, не понимаю, почему он плохо переживает минимальные изменения.
Потому что лишний пробел, или регистр одной буквы, или поменять поля местами, этот тест сломает. В основе теста же сравнение строк, откуда и вытекает адовая хрупкость.
Именно. При этом реальный запрос, как его понимает скажем оракл, вовсе не является регистрозависимым. А точнее, является, но не всегда и не везде. И всех этих тонкостей сравнение строк не учитывает совсем.

Ну вообще, тесты генератора запроса SQL не должны полагаться на сравнение строк, как вы уже достаточно убедительно доказали (и это, вообще, следует из знания домена SQL), Иначе такие тесты действительно слишком легко поломать, и слишком тяжело поддерживать. Юнитом здесь не должен, соответственно, быть генератор целиком — учитывая, что сервис всё-таки создаёт запросы не из строк обычной конкатенацией, у него должна быть некая реализация AST, и некие правила её создания. В таком случае мы получаем уже две категории тестов:


  • проверки промежуточного AST при заданных параметрах. Именно то, что будет проверять наличие в запросе правильной проекции, правильных предикатов, объединений, etc.
  • проверки перевода отдельных компонентов AST в строку для разных диалектов SQL, которые ваш генератор поддерживает

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

Я не вижу никакого реального примера. Ну или точнее так — к нему нужны обширные пояснения. Вот это вот например что: 'SELECT ...'?
UFO just landed and posted this here

Что-то вроде


assert(generateQuery(Query.SelectUsersWithLimit, {limit: 10}, DbEngine.MySql).sql === 'SELECT ...')

никак не подойдёт разве для уровня юнит тестирования? В интеграционном или функциональном проверяем, что нет синтаксических ошибок и т. п., а на уровне юнит-тестов, что генерируется тот запрос, который мы хотим сгенерировать. На уровне юнит-тестов мы не будем проверять насколько хорошо мы знаем конкретный диалект SQL или насколько хорошо написали миграции БД, и насколько хорошо сама СУБД работает :)

>генерируется тот запрос, который мы хотим сгенерировать
Этот подход и применяется. Только он очень плохо переживает изменения запроса. Даже самые минимальные.

А зачем менять запрос-то?

Требования изменились.

Требования изменились — изменяется и тест. Это логично, если относится к тестам, как к формализованным требованиям.

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

class QueryBuilder
{
    function getSql()
    {
        $sql = $this->buildSelect()
            . $this->buildFrom()
            . $this->buildWhere()
            . $this->buildGroupBy()
            . $this->buildOrderBy()
            . $this->buildLimit()
        ;
        ...
    }
    ...
}

assertTrue($queryBuilder->buildWhere() == 'a = 1 AND b = 2');
Вы слегка упрощаете, но в целом это ближе всего к работающему решению. Хотя базовая проблема хрупкости все равно остается: попробуйте заменить

a = 1 AND b = 2

на

1=a
and 2=b

, и посмотрите что будет с тестом. Семантика не изменилась, а строка при этом совершенно другая.

А зачем? Код генератора же все равно работает одним и тем же образом, например в сравнении сначала ставит поля потом значения. У нас же цель проверить работу кода, а не семантику. Поменяется работа кода, поменяется тест. Для гибкости можно добавить upper case и значения через именованные параметры.


assertTrue(
    strtoupper($whereSql) == strtoupper('a = :a AND b = :b')
    && $whereParams == [':a' => 1, ':b' => 2]
);

Другой генератор может в каком-нибудь JiraQL запросы генерировать, не тащить же всю его семантику в приложение.

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

Я бы хотел примерно вот чего. Т.е. чтобы ассерт например мог проверить, что в запросе имеется хинт, и он правильного формата для конкретной СУБД. А от этого текущие тесты очень и очень далеки.

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

UFO just landed and posted this here
Получается, вы два раза копипастите реализацию генератора

Откуда 2 раза? Реализация генератора только в классе генератора.


он сначала сравнит a, потом сравнит b, не заковычит их и укажет именно в таком порядке по разные стороны от знака равенства

Так нам это и нужно. Мы проверили, что результат генерируется правильный, и зафиксировали в тестах. Если результат изменится, тесты упадут. Если программист сделал это намеренно, поправит тест, если ненамеренно, поправит код.


Вы же в других тестах тоже конкретные значения проверяете, а не завязываетесь на то, что в том коде куда они передаются "2+2" и "4" может считаться одним и тем же результатом.

UFO just landed and posted this here
Если вы захотите учитывать разные типы данных, с разными наборами полей, с таблицами с пересекающимися именами полей, с правильной регистро(не)зависимостью

Так я о том и говорю, что это все не нужно. Зачем? Генератор для заданных настроек генерирует один конкретный вариант с конкретным типом данных, конкретным набором полей, конкретными названиями таблиц или их отсутствием, и конкретным регистром. Если б он случайные варианты выдавал, тогда да, но по условиям это ведь не нужно.


Я уже третий раз задаю этот вопрос, ответа почему-то нет. Можете конкретный пример привести, когда это приведет к проблемам?


Вам нужно (и у вас определение правильности) — чтобы a шло до b в AND, чтобы константа стояла справа от знака равенства, и чтобы не было кавычек?

Мне нужно, чтобы генератор на вызов $query->where(['a' => 1])->andWhere(['b' => 2]) генерировал валидный SQL-запрос. Генератор всегда генерирует один и тот же текст. Вот уж спека "генерировать разные варианты валидного текста в зависимости от фазы луны или других случайных факторов" действительно выглядит удивительно. Зачем поддерживать разный синтакс и реализовывать в тестах подмножество генератора или даже независимый от генератора парсер, если возвращаемое значение всегда должно быть одно и то же?


Вы же в других тестах тоже конкретные значения проверяете
Нет

"то я получу такой же объект"
"объект с такой же версией контента, но с обновлённой метаинформацией"
"он сам их генерит и прогоняет тесты"


И как же спрашивается он их проверяет?) Уж не конкретные ли значения сравнивает? Нет никакой разницы, генерируем мы результат во время выполнения тестов или вручную заранее задаем. На конкретные настройки генератора результат всегда будет один и тот же. Поэтому и проверять можно только его.


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


Как видите, никаких конкретных значений.

sort (nub lst) — а это, я так понимаю, "значимое подмножество генератора", которое генерирует правильный результат другим способом? Ну так и в моем варианте так можно сделать.


for ($i = 0; $i < 10; $i++) {
    $a = rand();
    $b = rand();
    $queryBuilder->where(['a' => $a])->andWhere(['b' => $b]);
    $sql = $queryBuilder->buildWhere();
    assertTrue($sql == sprintf('a = %d AND b = %d', $a, $b));
}

Тоже никаких конкретных значений.

UFO just landed and posted this here
Нужно хотя бы потому, что вам все эти тесткейсы нужно будет написать руками.

Так и в случае с проверкой через парсер нужно, никакой разницы нет.


А что если там поле 'c' вместо 'a', то всё равно всё хорошо, вы тоже тест писать будете или доверитесь?

Не понял этот вопрос, "там" это где?
Если вызывается $query->where(['a' => 1])->andWhere(['b' => 2]), и должно генерироваться a = 1 AND b = 2, а генерируется c = 1 AND b = 2, то это ошибка, и сравнение строк ее поймает.
Если вызывается $query->where(['c' => 1])->andWhere(['b' => 2]) и должно генерироваться c = 1 AND b = 2, а генерируется a = 1 AND b = 2, то тоже поймает.
Если вызывается $query->where(['с' => 1])->andWhere(['b' => 2]), и генерируется c = 1 AND b = 2, то тест пройдет, и это правильно.


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

Ну упорядочит он вызов $query->where(['a' => 1])->andWhere(['b' => 2]) в b = 2 AND a = 1, результат-то все равно при таком вызове всегда будет один и тот же, его и надо проверять. Поменяли код, поменяли тесты к этому коду, они же тоже в этой библиотеке находятся. Это никак не противоречит тому, о чем я говорю, удивителен для меня другой случай. Более того, если нам нужен именно такой порядок, а сгенерируется вдруг a = 1 AND b = 2, то это ошибка, и тест должен ее ловить, а вашем варианте будет считаться правильным кодом.


А оно не должно. Нет в спеке упомянутого мной в прошлом комментарии и процитированного вами.

Если оно "не должно", значит у вас генератор на один и тот же вызов $query->where(['a' => 1])->andWhere(['b' => 2]) возвращает разные результаты. Для этого надо писать специальный код с рандомизатором или еще чем-то, а значит это должно быть в требованиях. Такого в требованиях нет, значит предполагается, что код всегда работает одним и тем же образом.


Есть. Вы не будете писать 100-1000 тестов руками, а quickcheck'у не жалко.

Эм, как quickcheck влияет на факт "На конкретные настройки генератора результат всегда будет один и тот же"?
Если никак, то разницы нет. Разговор же не о том, писать тесты руками или генерировать автоматически.


Если синтаксис некорректный, то сохранение в базу сломается, но если синтаксис корректный, то гарантий на сохранение в базу всё равно нет.

Это все верно, только изначально разговор был про юнит-тесты на генератор SQL-кода без поднятия БД. Там не было условия, что он только INSERT-ы генерирует, соответственно и сохранять нечего.


Нет, это один из предикатов на поведение функции.

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

UFO just landed and posted this here
А вы оба эти теста пишете? А для d тоже напишете? А для e?

Всё, что должно быть написано для проверки через парсер, будет написано и для проверки через строки. Хотя бы потому что в парсер передается строка.


Но это уже не будет тестом, проверяющим семантику запроса. Это будет тестом, проверяющим оптимизатор

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


Порядок тестировать ему не нужно в этом юните.

А в другом юните как вы будете его проверять? Уж не сравнением ли строк, раз парсер не подходит?) Вы сначала придумали какой-то оптимизатор, потом другой тест, тем самым отодвинули проблему в сторону.


Зачем рандомизаторы какие-то? Может, он внутри себя статистику держит.

Поэтому я и написал "еще чем-то".


Или проверяет, не конфликтует ли имя поля с другими известными полями в других таблицах (и тогда a заменится на table.a).

Ну допустим, и? Результат одинаковых вызовов все равно будет одинаковый. Вот у нас вызов в тесте
$query->from('table')->where(['id' => 1])->join('table2', [...])->andWhere(['id' => 2])
и генератор к обоим id добавляет имя таблицы. Возвращаемая строка всегда будет
'SELECT ... FROM table1 JOIN table2 ON ... WHERE table1.id = 1 AND table2.id = 2'
хоть в тесте вы его вызовете, хоть в коде приложения, хоть 10 раз, хоть 20. Её и надо проверять.


но эта задача настолько тривиальная и легко доказываемая по индукции, что я вообще на неё тесты бы отдельные писать не стал.

Ага, только по условиям у нас несколько баз, и для разных баз нужно проверять, что из одного и того же AST сгенерировался синтаксис, подходящий для конкретно этой базы. QueryBuilder это кстати и есть упрощенный AST. Если вам эти тесты не нужны, о чем тогда разговор. Кому-то нужны, и их надо как-то писать.


Чем независимее ваши предикаты от реализации, тем сложнее сделать одну и ту же ошибку в формализации спеки.

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

Сложно представить ситуацию. Типа "сейчас у нас все запросы с WHERE 1=1, даже если других условий нет, нужно убрать 1=1 полностью и WHERE вообще если других условий нет"?


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

Давайте я поясню с другой стороны. Есть к hamcrest matchers для json или xml, например. Они понимают, что внутри json есть разные эквивалентные формы записи одного и того же, например, что 'aaa' это тоже самое, что и «aaa», при условии что строка внутри не содержит апострофа или двойной кавычки, а если содержит — то они заменяются чем-то.

Вот для SQL я хотел бы тоже самое. Чтобы инструмент понимал сам, что для оракла «aaa» это колонка, и она case sensitive, а колонки aaa и AAA это одно и тоже. А для MS SQL [aaa] например. Что если в имени колонки есть русские буквы, то на выходе должно быть select «абвгд», а никак не select абвгд. Я бы хотел сравнение в ассерте структуры запроса, а не его текстового представления. Я бы хотел возможности написать ассерт, который проверит, что запрос выбирает скажем 5 колонок. Или не выбирает некоторые технические колонки.

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

Ну вот есть онлайн что-то вроде http://www.dpriver.com/pp/sqlformat.htm. Наверняка есть подобные форматтеры в виде либ/модулей/утилит, пускай и под каждую базу заточенные. Не поможет их использование для обработки результат от билдера и сравнение с эталонной строкой?

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

Типичный пример: тут. И какая это версия языка? А хз, судя по коммитам — возможно 10g.

А вот тут вообще грамматика для Sybase.

И так четыре раза.

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

Да нет, я не про парсеры, а про форматтеры. Вывод генератора для конкретной базы форматируем и сравниваем с форматированной искомой строкой.

Насколько я понимаю, форматтер должен понимать структуру. То есть парсер там внутри.

Именно. задачу парсинга вы делегируете форматтеру.

UFO just landed and posted this here
Ну, я таки надеюсь найти готовый, на который уже написаны ;)
UFO just landed and posted this here

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


Другое дело что надо ещё проверять что СУБД интерпретирует все это вместе так как надо — но это можно сделать на других уровнях тестовой пирамиды

Вот сколько слышу аргумент про статическую типизацию и никак не могу его понять. Ну то есть да, rust ван лав и все такое, но за мои 2 года разработки на питоне я с ошибкой типизации столкнулся 1 раз (после апдейта библиотека вместо set стала требовать обязательно list). А вот сколько ошибок вида «надо добавить объект в 2 разных hashmap по разным ключам, скопирую строку и исправлю что надо, а на деле половина исправлена, а половина нет» я видел я даже посчитать не смогу. И они прекрасно прошли и анализаторы и code review и строгую типизацию прошли бы если бы она была.
А что тут особо понимать? Статическая типизация ловит значительную часть ошибок. 100%? Ну скорее нет, чем да — языки, где система типов достаточно сильная для такого, все еще не слишком практичные для повседневного применения. Да и не ловят они логических ошибок, о которых вы пишете.

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

Является ли статическая типизация полной заменой тестов? В типичном на сегодня проекте — наверное нет. Является ли она инструментом повышения надежности? Вполне да. Я примерно это и хотел сказать — что есть и другие инструменты, кроме тестов, и их тоже нужно рассматривать, когда мы решаем, на что нам потратить свое время и деньги.
Проблема то в том что по моему опыту это хорошо если одна ошибка, отлавливаемая статической типизацией на 100 других. И это я приуменьшил скорее всего. Т.е. в языке с динамической типизацией без тестов будет 101 ошибка, в языке с статической типизацией — 100. Мне кажется это просто примерно тоже самое что говорить «да зачем мне пристегиваться, у меня ж ABS\курсы экстремального вождения\иконка есть». И проблема то в том что я хоть какую-то пользу вижу только в синтетических мысленных экспериментах где, например, у всех переменных свой тип. Тогда да, многие логические ошибки можно будет отловить статической типизацией, но к реальности такой пример не имеет никакого отношения.
Вы про какой опыт, тот который 2 года питона? Точнее, откуда ваш опыт со статической типизацией берется в данном случае, который вам подсказывает, что ошибок будет 100?

Ваши соотношения — они скорее всего типичны для вас с вашим опытом, и вашего инструмента. Допустим, я по сравнению с моими более молодыми коллегами некоторых классов ошибок, которые они делают, вообще не допускаю. Но наверняка делаю какие-то свои, другие.

>я хоть какую-то пользу вижу только в синтетических мысленных экспериментах
Чтобы получать пользу от статической типизации, тоже нужен определенный опыт. Даром это не дается. Вот скажем это хороший пример, хотя и сильно непрактичный ;)

Пользу-то получить опыта особого не надо, а вот чтобы пользы было больше чем "не даром" — это да. При том, что опыт такой нужен всех, кто должен с этим кодом работать.

Я написал просто соотношение ошибок. Что если взять сферический продукт в вакууме и при соотношении 1 к 100 (из всех найденных ошибок написанных десятком разных людей только 1 была связана с типами, а все остальные — нет) то и получится, что для условного питона ошибок будет 101 (100 логических + 1 связанная с типами), а в языке со статической типизацией будет на 1 ошибку меньше, т.к. логические ошибки от типизации не зависят.

А про пользу статической типизации мы говорим в разрезе тестов и корректности работы программы. И говорить что статическую типизацию можно применять как альтернативу тестированию это ну как следующая ситуация: «теперь рулевое колесо и ручник обтянуты ухватистой резиной чтобы не при каких обстоятельствах они не могли проскользнуть в руке. Это повысит вашу безопасность на 1%! Над удалением циркулярное пилы напротив головы водителя работы все еще ведутся».

Так откуда статистика-то такая? У меня вот статистика обратная: я допускаю ошибки, связанные с типами, в 10 раз чаще чем логические.


Особенно заметно это было пока я под Sharepoint решения писал: бакенд как скомпилируется, так уже почти рабочий. А фронт только задеплоил — так сразу ошибка всплыла, поправил одну букву и снова можно идти чай пить...

Кстати, многие не считают ошибки типа NPE ошибками типов, потому что привычная им система типов not nullable или вообще не отличает от nullable, или в конфиге указано не отличать. То есть под ошибками типов имеют в виду прежде всего мешанину строк и чисел, инстансов разных классов и т. п.


С другой стороны, недавно полдня дебажил потому что TypeScript не различил типы string и String.

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

Ну и потом — в одном случае замена set на list или наоборот всплывет при выполнении (возможно — через пару лет), а в другом — при компиляции. И бага в этом месте просто не возникнет — вы это выловите и устраните.
UFO just landed and posted this here
Надо бы хотя бы ридми написать для начала :)
UFO just landed and posted this here
Не, ну у него же есть хоть какие-то особенности, вызванные тем, что оно например на Идрисе? Вот про них и написать.
UFO just landed and posted this here
интересно без вдохновления FizzBuzzEnterpriseEdition, описание типов для FizzBuzz получится понятнее версии с тестами?
UFO just landed and posted this here
UFO just landed and posted this here

Легко.


expect(
  runInPgDatabase(
    PgSqlGenerator.generate('...')
  )
).to.be.eventually.fulfilled;

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


В докере всё это разворачивается за день вместе с упражнениями по конфигурации каждой базы внутри докера.

1. Докер у нас запрещен безопасниками.
2. Насколько я знаю, полноценного docker image для оракла не существует, по лицензионным соображениям. Те что имеются, ставят базу при первом запуске. Из интернета.
3. Все кто лезет в интернет, в нашем интранете пролетают как фанера над Парижем.
4. Превращать юнит-тест в полноценный интеграционный путем разворачивания четырех разных БД (и не факт, что ими ограничится) — не слишком хорошая идея, как по мне.

Хотя вполне возможно, что в каком-то случае такой вариант и подошел бы.

А почему запрещен именно докер, а не его реестр?

Я же не безопасник. Запрещен, и все. Вместе с остальными виртуальными машинами.

А реестр просто недоступен.
Все это есть. Я как раз пишу на языках со строгой типизацией, использую статический анализатор, и это не отменяет необходимость юнит тестов. И ревью тож практикую. Первое, на что смотрю при ревью — это именно тесты.

Ваш пример с генератором SQL достаточно интересен. Возможно, юнит тестом тут не обойтись — придется использовать интеграционный. А если в качестве СУБД поиспользовать встраиваемую СУБД, например H2, то интеграционный тест будет не шибко сложнее юнит теста.

А еще можно попробовать разбить генератор SQL на 2 компонента — генератор модели и транслятор модели уже в SQL. Относительно дорогое решение, но в некоторых случаях оправданное, и позволяет обойтись юнит тестами.
>не отменяет необходимость юнит тестов
Я лично в общем-то нигде не имел в виду, что их нужно непременно отменить. Я говорил слегка о другом — что это не единственный инструмент, и нужно вкладывать силы в разные. И этот вот пример — он в целом про то, что иногда чистые юнит-тесты писать очень сложно. И в итоге — невыгодно.

Если совсем упрощенно — генератор SQL (ну или там JSON) — это в простейшем случае просто конкатенация строк. Но уже парсер того же самого JSON далеко не так прост. Понятно, что зачастую парсер JSON доступен из коробки, но вот для SQL это уже совсем не так. И тут куча тонкостей. Диалекты в первую очередь.

Скажем, что мне даст H2, если запросы пишутся специально под Oracle, DB2, MS SQL, Hive? Ну да, часть из них в общем-то являются чистым SQL-92, возможно для таких идея с H2 и прокатит. Но некоторые возможности с H2 совсем несовместимы.

Про генератор модели и транслятор в SQL уже предложили, чуть в другой форме. Да, эта идея выглядит обещающе.

В примере с генератором очень похоже, что сначала написали генератор, а потом начали думать, а как его тестировать. По-моему хороший пример чем полезно TDD

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

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

Ну и что нам тут даст TDD? Откладывание написания кода на месяц, пока не напишем первый тест? :)

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

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

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

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

На самом деле было бы интересно послушать ваши критерии разумности покрытия.

Они простые.
Минимум — демонстрация замысла автора. В прямом смысле — глядя на тест должно быть понятно, что и зачем автор хотел сделать.
Остальное определяется возможностью повторного использования компонента.
Если компонент идет «в народ» — тесты должны быть ковровыми. В остальных случаях возможны компромиссы.

Мне нравится принцип: продакшен баг должен быть покрыт тестом.

Согласен. Но это не должно быть единственным поводом для написания тестов. )

Да, это в дополенении к обычному покрытию.

UFO just landed and posted this here
Как делать рефакторинг без юнит тестов, но с интеграционными, функциональными и нагрузочными тестами?

Так же как с юнитами.

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


Ну вот написал класс, что должно произойти дальше? Можно деплоить на прод глянув на код внимательным прищуром?


Мы на втором курсе ВУЗа все пихали в main() и вызывали. Немного позже снизошло понимание, что этот main() поправленный под нужды запускаемого класса, и есть юнит-тест.

Ну, можно писать тесты до кода. TDD так и рекомендует — написали тест, убедились, что он красный, написали реализацию, убедились, что тест позеленел. Смыть, повторить.
Возможно, я не совсем понял вопрос, но когда речь идет об обычном веб-проекте последовательность может быть следующая:

1) Программист пишет код, запускает сайт через отладчик и смотрит, что получилось. Если что-то вылетело, исправляет.
2) Когда он считает, что фича готова, он отправляет код в репозиторий. На его коммит автоматически запускаются интеграционные тесты. Если тесты проходят, код попадает в репозиторий, если нет, он должен либо исправить ошибки, либо обновить тесты.
2а) В случае, если практикуется код ревью, после тестов код может попасть на ревью и быть одобрен или отклонен.
3) В трекере он помечает задачу как готовую к тестированию. Когда будет готова сборка с его изменениями, она попадает в тестовое окружение QA и тестируется. Если что-то не так, задача возвращается к разработчику.
4) Заканчивается итерация, тестировщики проверяют всю систему в сборе, а не только отдельные фичи.
5) Система выкатывается на стейджинг где проверяется ее работоспособность в реальном рабочем окружении.
6) Трафик с продакшн серверов перенаправляется на стейджинг сервера и если все прошло удачно, они теперь становятся продакшн серверами, а старые — стейджингом.
Обычный веб-проект — слишком размытое понятие.
Известный мне типовой процесс:
Разработчик пишет код + юнит тесты.
Все юнит-тесты прогоняются локально, может даже при каждой сборке.
Локально же разработчик проверяет свою разработку, если это возможно. Часто используется разработческий серкер, на которые выкатывают обновления для тестирования в преднастроенном окружении и для совместной проверки.
После принятия решения о готовности разработки, код отправляется на контрольную сборку, вручную или через пулл-реквест.
Контрольная сборка проходит на отдельном билд-стенде, с обязательным выполнением всех юнит-тестов.
Только при успешном прохождении юнит-тестов на стенде и одобренном code-review код попадает на следующие этапы тестирования.
Первый этап тестирование — стенд системных тестов. Там запускаются интеграционные и автоматизированные тесты.
И только после успеха на этом стенде разработка попадает на стенд ручного тестирования.
Ну и т.д.
Понятно, что в конкретном случае набор шагов индивидуально подбирается, но общая последовательность примерно такая.

У меня возникает ощущение, что у вас весьма однобокое представление о разработке и "обычном" веб-проекте.


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


Давайте посмотрим на пример поинтереснее. Вот нынче микросервисы в моде. Их обычно каким-нибудь MOM (Message-Oriented Middleware, AWS SQS или RabbitMq какой-нибудь) связывают друг с другом и они по-разному обмениваются сообщениями друг с другом.


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


Программист что-то там поправил, звёзды сошлись, и оно даже компилируется (пусть дело происходит на Java, чтобы не набежали адепты Idris с немым вопросом "если компилируется, чему там не работать?").


Теперь вопрос: что должно дальше произойти?


Взять дебаггер и посмотреть что происходит? На каких тестовых данных будем проверять? В базу их надо предварительно положить? А запрос из MOM тоже руками отправлять? А как бы убедиться, что это изменение регрессий не добавило, и остальные 146 полей по-прежнему возвращают то, что должны?
Давайте предположим, что мы жутко экономим, и в самом деле заставили живого человека всё это делать руками, а проверять — глазами.


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


Сколько таких итераций надо сделать, чтобы разработчик потерял бдительность и стал работать еще медленнее?


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


Сколько времени потратит следующий разработчик для добавления еще одного следующего поля, на ручные упражнения с регрессией по вышеописанной задаче?


Спору нет, я немного утрировал и перегнул палку — согласно свежим политическим сводкам, кирилицу, конечно можно не проверять. Но, думаю идея ясна.

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

А кто пишет интеграционные тесты?

Разработчики, в том числе лиды.
Есть мнение мыслить так. Юнит тесты — это стартап. Дальше, думаю, понятно.

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


Юнит тесты дают разработчику практически моментальную обратную связь и фикс ошибки почти ни чего не стоит. Д: добротность Закоммиченный баг может приводить к простоям других разработчиков. Отработка бага на следующих уровнях идёт с запаздыванием, требует вовлечения большего числа людей (тестировщик, дефект аналитик, ПМ, тимлид, ..). Такая обратная связь поступает когда разработчик с большой вероятностью переключился на другую задачу и для исправления требуется перепланировать спринт и переключать контекст.


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

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

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

Во-вторых — очень спорное, вплоть до неправильного, рассуждение про юнит-тесты.
Начать хотя бы с того, что они не проверяют качество продукта от слова совсем. Если их писать в описанной тут парадигме — получится фигня, бесполезная примерно всегда. И это ключевая ошибка многих, кто их внедряет.
Правильные юнит-тесты контролируют два параметра: соответствие модуля (класс, функция, модуль) спецификации, и неизменность (сохранность) этой спецификации при изменении кода.
Первое даёт более быстрый старт разработки разработки, особенное если использовать TDD.
Второе сокращает издержки на поддержку целостности кода при модификациях или изменении требований. Часто в разы, порой на порядок.
Грубые примеры: программист доработал кусок класса, упало 10 тестов, он быстро исправил ошибки и с большой вероятностью ничего не сломал. Или изменились требования — исправили тесты, по следам исправили только тот код, которые не соответствует новой спецификации. Попробуйте посчитать экономический эффект в этих случаях.
Есть и более тонкие примеры. Изменились требования — поправили тесты — поправили код — упали ещё тесты — смотрим на эти тесты и понимаем, что возникло противоречие в спецификация — идём к аналитику (или даже заказчику) и выясняем лажу до того, как она окажется в проме. Да, бывало и такое.

Ну и в-третьих, кумулятивный эффект — очень непонятный расчёт эффекта основанного на неверных предположениях о сути оцениваемых явлений, а в конце и вовсе фарс вида «надо ли писать тесты, если заказчик лопух».
>мне кажется, формулы взяты не те, которые бы следовало…
Да не, формулы возможно слегка упрощены, и данные, на которых основаны выводы, взяты с потолка, чтобы пояснить мысль, но в целом они отражают важное свойство — тесты вы пишете сегодня (и платите за это сразу), а профит возможно получаете потом. И этот фактор как правило имеет место. Ну, кроме TDD, когда считается, что тесты помогают разрабатывать основной код, и профит тоже сразу.

>Начать хотя бы с того, что они не проверяют качество продукта от слова совсем.
Ну а соответствие модуля спецификации вы совсем не считаете относящимся к качеству? :)

Один из видов отношения к тестам: тест — это формализованная спецификация с бесплатной возможностью проверить код на соответствие ей. Мы пишем тесты как спецификации, а не как средство проверки кода на соответствие спецификации.

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

Проблема понимания никуда не денется пока исходные требования пишутся на естественном языке. Или на искусственном, но не позволяющим современными популярными средствами выявить неполноу и/или противоречивость. Это и к систем типов относится. Толку в ней, если средств автоматической проверки в режиме статанализа нет?

UFO just landed and posted this here

Я ваше предложение разработать систему типов понял как предложение разработать систему типов, DSL на котором бизнес будет писать требования. А мы потом реализовывать эти требования на мэйнстрим языках.

соответствие модуля спецификации вы совсем не считаете относящимся к качеству?
вы потеряли важное уточнение, которое я написал:
они не проверяют качество продукта

Модуль это не продукт. Использовать юнит-тесты как инструмент контроля качества продукта некорректно.

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

Я об этом все время и толкую :) Улучшение кода происходит тогда, когда над ним думают. И по большому счету — только от этого.

Скажем, такой слегка упрощенный пример — наблюдал в проекте двоих коллег, более молодых, и с недостаточно, скажем так, параноидальным подходом. Им допустим ничего не стоит написать в коде что-то типа Integer.valueOf(строка), считая что строка всегда будет в правильном формате. Иногда это получается почти подсознательно. Но и TDD — один из способов этого добиться, сознательно пытаясь понять, как этот метод может упасть, и что может пойти не так. В приличной же системе типов компилятор должен нам подсказать, что функция String->Integer определена не для всех возможных строк.
В приличной же системе типов компилятор должен нам подсказать, что функция String->Integer определена не для всех возможных строк.

Я вполне допускаю существование типа ConvertableToIntString.
Только что компилятор сможет сказать про условный new ConvertableToIntString(stringVal), где stringVal — строковое значение приходящее извне?
А ему не обязательно знать, что вот эта строка конвертируется. Ему нужно сказать, что вот в этом месте есть возможность, что она не сконвертируется. Что это будет, условно, не Integer, а скажем Try. Если он подскажет нам не забыть этот вариант — это уже приличное улучшение.
UFO just landed and posted this here
Классно конечно. Но значение stringVal известно только runtime.

Я так понял что оно будет проходить через convert и делиться на два варианта (either) — либо оно конвертится в число или нет. Если конвертится то система типов дальше сможет использовать тот факт что строка конвертится. Но при этом непонятно какой в этом прок — что мы дальше будем делать с этой строкой если число уже получили

UFO just landed and posted this here
UFO just landed and posted this here

Как по мне, то если на большинстве задач разработчика 30-40% времени уходит на юнит-тесты, то что-то идёт не так. Скорее всего с архитектурой (трудоёмкий сетап чаще, трудоёмкая проверка результата реже), но часто и с тестированием внутренностей класса, сокрытием под видом юнит-теста теста интеграционного, а то и более высокого уровня и т. п., стремление к формальному 100%покрытия…


С другой стороны, автор явно недооценивает роль юнит-тестов, сводя её к предотвращению продакшен-багов.

Разумеется, если нужно написать юнит-тесты или что-либо еще, придется либо выделить больше времени на проект, либо нанять дополнительных разработчиков.
O, rly? Открою вам страшную тайну — вам не нужны юнит-тесты, если вы не думаете о стадии maintenance. Если вам нужно только сделать и забыть, то нет смысла писать юнит-тесты.
По моим наблюдениям типичным количеством затрат времени разработчика на юнит-тесты можно назвать около 40% времени на разработку самой фичи,
Во-первых, а вы замеряли, насколько сокращается чистое время написания кода, если он пишется уже после написания тестов? Во-вторых, а вы замеряли, насколько больше времени потребуется на рефакторинг того же куска кода через 5 лет если он не покрыт тестами? Если нет, то 40% — это пальцем в небо. Если да, то где расчеты?
Бизон достаточно неуклюжая и плохо управляемая на операционном уровне компания, бесконтрольный найм сотрудников уже привел к тому, что в ней работает 600 программистов… и которые тратят на юнит тесты около 30% рабочего времени.

Каков NPV от использования юнит тестов в Бизоне?

При чем тут юнит-тесты? Вы посчитайте NPV бесконтрольного найма лучше, хе-хе

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


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

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

Вернёмся к слабой связанности. Она позволяет уменьшить воспринимаемую сложность программ, работать с большими программами как с композицией маленьких. Зачем это нужно? Затем, чтобы когда вы просите программистов починить компонент А, не сломался компонент Б. Какова от этого выгода? 25%? Вы серьёзно? При том, что 80% процентов времени средний программист занимается отладкой, что же он отлаживает? Я бы сказал, на особо неудачных проектах решение таких проблем занимает до 95% времени, на удачных его получается снизить процентов до 15-20. Разница производительности разработки на проектах со слабо связанной архитектурой и без не на проценты, а в разы. Другое дело, что только тестов не достаточно, нужна экспертиза по построению этой самой архитектуры, а если вы считаете производительность проектов так как считаете и даже не задаёте вопросы как выстроить архитектуру — думаю все мои объяснения пройдут впустую.

Что касается затрат на тесты — они сильно преувеличены. Например, на моём последнем проекте мы настроили скаффолдинг (работы на 3 дня на yeoman), и теперь базовые юнит тесты создаются автоматически для каждого нового компонента. Их можно расширять и дополнять, но в более чем половине случаев стандартных тестов достаточно, чтобы проверить, что компонент изолированно работает, что и есть самое главное в юнит тестах. Таким образом мы почти ничего не тратим на создание юнит-тестов, их создаёт машина и человек подключается только тогда, когда это действительно нужно, взамен мы получаем уверенность, что каждый из наших компонентов работает и отлавливаем большое количество ошибок ещё на этапе прекоммит-хука. Если любая из этих ошибок попала бы в QA или в Prod, ей пришлось бы пройти долгий цикл: отлов тестировщиком, формализация, постановка задачи, приоритезация задачи, понимание задачи разработчиком, попытки воспроизвести баг, исправление, код ревью. Это в лучшем случае пол дня и куча вовлечённых специалистов. А с автотестами баг закрывается через 15 минут после возникновения тем же разработчиком, который его создал.

Тесты можно не заводить или на очень маленьких проектах, или на временных с низкой ответственностью. После долгого опыта разных подходов я иногда завожу тесты даже на домашних проектах, а на корпоративных это must have в большинстве случаев, от этого все выиграют. Странно, что в 2019 году это ещё нужно кому-то объяснять.

А можете показать эти автогенеренные тесты?

Очень простые, вроде такого для визуальных компонентов:
it('renders without crashing', () => {
    mount(<MyComponent />);
});

или такого для модулей данных
it('init() runs without crash', async () => {
    await myModule.init();
});
it('reset() runs without crash', async () => {
    await myModule.reset();
});

Можно спорить о целесообразности тестов без единого ассерта, но они бесплатные и иногда защищают от детских ошибок. Плюс когда файл и основная структура уже созданы — проще написать что-то ещё.

И много у вас компонент без параметров?

Простите за грубость, но мне показалось, что автор написал статью как настоящий шарлатан: сначала вбросил невалидные тезисы, которые принял как данность, а потом накатал простыню с умными терминами и формулами.
Во-первых, откуда вы взяли 40% на юнит тесты? По моим личным и коллег наблюдениям, код чуть более сложнее чем Hello world с тестами пишется быстрее чем без них. Особенно это касается, если вы пишите метод, который изменяет состояние приложения. Каждый раз подчищать живую базу во время разработки — это же просто каторга! А если вам нужно еще добавить фичу, которая шлет email, когда изменения сделаны? Что, будете каждый раз высылать себе письмо?
Во-вторых, вы говорите, что сайт в дауне, это, конечно, плохо но не смертельно. Чтож соглашусь, не все пишут модули управления ракетами. А что если после очередного изменения магазин начитает слать посылки вместо адреса доставки на адрес инвойса? И это происходит в течение месяца. У нас была такая фигня, только перепутались email'ы. Ручное тестирование прошло нормально, потому что тестировщики всегда используют один и тот же email. А простой юнит-тест легко бы отловил эту ошибку. Заказчики тогда были в ярости.
Ну и в-третьих. Если у вас уходит 40% на тесты, то с вашим говнокодом точно что-то не так. На лицо толсные сервисы или контроллеры, срочно требуется инъекция CQS :)
То, что вы описали, является интеграционными тестами, а не юнит тестами.

Почему? По проведенной информации нельзя сказать был это юниттест или интеграционный

Каждый раз подчищать живую базу во время разработки — это же просто каторга! А если вам нужно еще добавить фичу, которая шлет email, когда изменения сделаны? Что, будете каждый раз высылать себе письмо?


Я имел в виду вот эти тесты.

Так это он про "без них" (хотя я не понимаю почему просил не восстановить базу из бекапа)

А если у меня нетривиальный DAO, который ходит в базу, тесты для него юнит или интеграционные?

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

Это надо мерять
Вот что нашел по быстрому https://stackoverflow.com/questions/237000/is-there-hard-evidence-of-the-roi-of-unit-testing


Yes. This is a link to a study by Boby George and Laurie Williams at NCST and a another by Nagappan et al. I'm sure there are more. Dr. Williams publications on testing may provide a good starting point for finding them.


[EDIT] The two papers above specifically reference TDD and show 15-35% increase in initial development time after adopting TDD, but a 40-90% decrease in pre-release defects. If you can't get at the full text versions, I suggest using Google Scholar to see if you can find a publicly available version.


Автор, к сожалению, почти не приводит ссылки на источники даже там, где мог бы по крайней мере попытаться.

40%, а то и 60-70 легко можно получить если на проект с сильносвязанной архитектурой посадить писать тесты человека, который книжку или пару статей о них прочёл. И не факт, что от этих тестов хоть какой-то выхлоп будет в принципе.

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

Откуда у вас эта информация?


SQLite is open-source but it is not open-contribution. All the code in SQLite is written by a small team of experts. The project does not accept "pull requests" or patches from anonymous passers-by on the internet.

https://www.sqlite.org/hirely.html


In order to keep SQLite completely free and unencumbered by copyright, the project does not accept patches. If you would like to make a suggested change, and include a patch as a proof-of-concept, that would be great. However please do not be offended if we rewrite your patch from scratch.

https://www.sqlite.org/copyright.html


Посмотрим кто коммитит в тесты:


$ fossil time -p test/ -n 0 | grep -Po '\(user: \K\S+' | sort | uniq -c | sort -nr
   4710 drh
   2608 dan
    912 danielk1977
    244 mistachkin
     77 shaneh
     44 shane
     31 shess
      9 adam
      3 aswift
      2 tpoindex
      2 rdc
      2 pweilbacher
      2 dougcurrie
      2 dan)
      1 peter
      1 mihailim
      1 kwel
      1 icculus
      1 chw

Это все коммитеры тестов за 19 лет разработки.

Ок, но все же:

As of version 3.23.0 (2018-04-02), the SQLite library consists of approximately 128.9 KSLOC of C code. (KSLOC means thousands of «Source Lines Of Code» or, in other words, lines of code excluding blank lines and comments.) By comparison, the project has 711 times as much test code and test scripts — 91772.0 KSLOC.



90 миллионов строк тестов! Чем еще объяснить такую расточительность на не самом сложном проекте? Ни одна коммерческая компания на такое бы не пошла, это возможно только при наличии бесплатной рабочей силы.
Тема близка для меня, сам недавно делал доклад на тему «Место автоматизации тестирования в процессе разработки». Обосновывал экономическую целесообразность автотстов, не только юнит, но и приемочных и интеграционных. Вобщем если кому интересно и есть что добавить, то вот докладик cloud.mail.ru/public/CHzm/iNg98Zq4u

А есть на нероссийских хостингах?

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

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

А вы сможете вашу библиотеку классов для процессинга платежей протестировать юнит-тестами? Ведь процессинг предполагает тесное взаимодействие с разными системами — гейтами платежных систем, криптографическими модулями, СУБД и т.д. Вы всё это будете подменять моками с неизвестной достоверностью? Может, тут как раз интеграционный тест напрашивается?
А вы сможете вашу библиотеку классов для процессинга платежей протестировать юнит-тестами?

А чем вы предлагаете тестировать поведение методов отдельных классов?


Вы всё это будете подменять моками с неизвестной достоверностью?

Почему с неизвестной?


Может, тут как раз интеграционный тест напрашивается?

Интеграционные тоже обязательно будут.

Articles