Pull to refresh

Comments 64

Не то чтобы согласен с автором.
В процессе написания теста (да что там — в процессе обдумывания теста) вы уже представляете себе то или иное поведение функции. Если функция full_name класса User должна возвращать строку, состоящую из first_name и last_name, разделенных пробелом, то смысла в тестировании такой функции нет. Вы сразу понимаете, что такая функция тривиальна, для этого не нужно как-то описывать ее поведение. Если же в будущем появится необходимость учитывать титул, второе имя, отчество или еще что-то, усложняющее функцию и добавляющее некую разветвленность в ее поведении — пишете тест и потом начинаете реализацию.
Оперируя понятиями «причина» и «следствие», автор сам их подменяет. Вы ведь пишите код не потому, что у вас есть тест. Вы пишите тест потому, что у вас есть необходимость в функционале, который вы знаете изначально.
Отлично, вы видите что этот код тривиален *сейчас*, а что будет после рефакторинга? Скажем вы решили через год что вам не нужен last_name а нужен middle_name, suname и other_names[]. Как вы узнаете что full_name больше не работает так как задумывалось?
После рефакторинга код не может стать менее тривиальным, иначе это не refactoring, а refucktoring. В остальном:
Если же в будущем появится необходимость учитывать титул, второе имя, отчество или еще что-то, усложняющее функцию и добавляющее некую разветвленность в ее поведении — пишете тест и потом начинаете реализацию.
Мне даже стало интересно: а как вы узнаете, вы узнаете что full_name больше не работает так, как задумывалось, если у вас есть тесты?
Изменение кода с изменением семантики — это уже не рефакторинг. Рефакторинг — изменение кода без изменения функции, реализуемой этим кодом.
>>это ужасный совет для новичков.
Это самый правильный совет для новичков! Т.к. новичок может начать тестировать ВСЕ!!! А т.к. программный код достаточно часто меняет, то свалившихся тестов может быть слишком много и это деморализует! Может даже привести к выводу «Да ну нафиг эти тесты, замучаешься потом фиксить». Когда пишешь тесты только и только для более менее серьезной фукнции, то получается компромисс между «уверенностью, что более менее все ок» и «сопровождением тестов, т.к. они периодически ломаются», а этот компромисс приводит к тому что всегда можно найти багу за приемлимое время, при этом не потратив чересчур много на написание тестового кода, который хоть и важен, но все-таки не фича продукта!
Извольте. Тесты тоже нужно уметь писать. И если новичок не начнет учиться писать тесты, то потом будет сложнее. Лично я очень жалею что в институте, этот вопрос обходили стороной. Разработка включает в себя не только программирование, но и тестирование. И эти штуки лучше писать вместе.

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

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

>>Так же тесты помогают при обновлении используемых сторонних библиотек.
С этим никто не спорит. Мой комментарий не об этом.

>>ем такое отследить кроме тестов, я не знаю.
Тест тесту рознь! Существует достаточно много типов тестов. То что пишет программист это модульный тест.

Задача программера разрабатывать, а не тестировать! Не надо ВСЕ и вся тестировать! То что вы можете допустить багу, это уже учитывается и именно поэтому существуют отделы качества, где специалисты тестирования думают над тем как Вам помочь найти те баги, которые Вы сделали. Разработчик должен протестировать в достаточном кол-ве чтобы за приемлемое время понять насколько качественно он сделал свою работу.

Я к тому, что не надо впадать в крайность! Лучше потратить время на обдумывание архитектуры, чем на механическое вбивание новых тестов для тривиальных функций. Не бойтесь пропустить 1-2 важных функций. Бойтесь лучше другого, ситуации в которой Вы продумали не качественную архитектуру из-за нехватки времени. Посидите лучше 5 мин. за листком бумаги с карандашом рисуя архитектуру, чем на вбивание тестов, которые можно было бы не писать. Лучше после листка бумаги напишите юнит-тест на будущую серьезную функцию.
Вот тут-то и ошибка.
На листке бумаги — рисунки, а не архитектура. Дядя Боб ни раз повторял, что всё, что он рисует на листках не реализуется в коде в реальности.
Архитектура — это код, форма, которую он принимает. Здесь речь идёт о практике TDD. Эта практика подразумевает, что архитектура выращивается через тесты.

чем на механическое вбивание новых тестов для тривиальных функций

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

То что пишет программист это модульный тест.

Программисты не пишут интеграционные тесты? Да ну?)))
>>Вот тут-то и ошибка.
Никакой ошибки нет :)

>>На листке бумаги — рисунки, а не архитектура. /* Пропущено */
>>Дядя Боб ни раз повторял, что всё, что он рисует на листках не реализуется в коде в реальности.
Ну и не стоит париться ) Когда человек рисует, у него работает моторика. Мозг начинает лучше работать! Он «включается в процесс осмысления задачи», это как дополнительный этап. Он начинает лучше понимать задачу. Именно лучшее понимание задачи и есть задача результат который следует ждать от рисования.
Другой этап мышления и обдумывания задачи это попытаться воспользоваться тем чего еще нету именно для этого мы и пишем предварительный юнит-тест, только так мы можем столкнуться с тем что кой-чего не хватает «Фигасе, а как мне прикажешь передать ссылку на… ?».

Т.е. цепочка рисунок -> попытка протестировать дает нам более качественное понимание того что мы делаем.

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

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

  1. Рисунки на бумаге — не архитектура. Архитектура может быть выражена только кодом, формой которую он принимает.
  2. Рисование на бумаге, помогающее TDD — практика Agile Modeling.
  3. Программисты пишут не только юнит-тесты.
Вы знаете. Я не хочу спорить. У меня есть проекты которые написаны вообще без тестов, есть которые пишутся с тестами, при чем тесты пишутся на основе требований. И придумывание архитектуры, ни коем образом не влияет на написание тестов. Если упрощать, то процесс выглядит так.

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

Либо вы привираете, либо пишите хэлоу-ворлд, либо ваш заказчик мёртв. Последний маловероятный вариант — вы опровергли утверждения о том, что невозможно предвидеть всё наперёд, невозможно построить приложение полностью удовлетворяющее SOLID-принципам (ведь приложение, полностью удовлетворяющее SOLID-принципам — оксюморон).
Имеется в виду отдельно взятый момент. А не весь период. Потом меняется требования, переписываются тесты, переписывается код.
Да, новичок может тестировать всё. Но его ошибка чаще в том, что он тестирует то, что уже оттестировано так или иначе, тестирует не свой код или пускай свой, но тот, который должен тестироваться на другом уровне. Примеры такого тестирования (в привязке к веб-разработке на «LAMP»): тестируется через phpunit результат mysql_query("SELECT id, name FROM table"); или $templater->render('item.tpl', ['id' => $id, 'name' => $name]);. То есть новичок думает, что пишет юнит-тест, но на самом деле это уже как минимум интеграционный получается. Тестируются библиотеки, SQL-запрос, шаблон, но никак не логика PHP-кода (вернее она только косвенно тестируется). Новичку нужно было написать мок на эти функции, который был проверял, что вызывается mysql_query() с параметром «SELECT id, name FROM table» или $templater->render с параметрами 'item.tpl' и ['id' => $id, 'name' => $name], но не то, что они возвращают.

Новичку при разработке программы по TDD нужно четко в голове держать роль, которую он исполняет: когда он выступает как программист (PHP в примерах), а когда как DBA (или SQL-программист) или как верстальщик, и тестировать код программы, а не код SQL-запросов или шаблона. И четко понимать, что тестировать на уровне юнит-тестов он должен только свой код, а не код стандартной либы или шаблонизатора. Отсутствие этого понимания приводит к хрупким тестам и, как следствие, демотивирует, а не тесты на геттеры/сеттеры.
Вы ведь пишите код не потому, что у вас есть тест. Вы пишите тест потому, что у вас есть необходимость в функционале, который вы знаете изначально.

Изначальный функционал — абстракция, лежащая на бумаге. Только код является функционалом и архитектурой. Таким образом, перенести абстрактное корректное «изначальное поведение», в код — трудная задача. Эту задачу призван решать TDD и TDD пытается формализовать процесс переноса «абстрактного корректного изначального поведения», делая тест причиной, а код следствием, что согласуется с одним из основоположений TDD.

p.s. Извиняюсь, промазал уровнем.
Я не сторонник догм. Однако мне всегда казалось, что TDD пытается механизировать процесс написания кода. Именно об этой механизации я и напоминаю в комментарии выше. А насколько догматично стоит подходить к TDD — дискуссионный момент. Собственно, здесь мы видим заочную дискуссию дяди Боба и Марка.
Омфг. Вас послушать, так вы пишите тесты, находясь в нирване и даже не представляя, какой функционал этим тестом пытаетесь описать. Но, держу пари, это не так, и до написания тестов вы себе представляете, что должна делать будущая функция. Вы прекрасно понимаете, что сейчас вам нужна функция для, к примеру, импорта файла с товарами в БД. Вы это поняли не потому что у вас есть такой тест, а потому, что заказчик дал вам задание реализовать импорт товаров в БД. Это сложная функция, вы на нее напишите тест, и я на нее напишу тест. Иная ситуация: для упрощения себе жизни вам нужна функция (как я уже выше приводил) сложения first_name и last_name в full_name. Вы знаете, что должна делать эта функция не потому, что есть такой тест, а потому что вам эта функция нужна именно для этих целей. Вы понимаете, что функция — элементарна.

def full_name
  "#{first_name} #{last_name}"
end

Вы бы стали писать для нее тест? Я — нет.
Стал бы. Кстати, про возврат
public int Year
{
    get { return 2013; }
    set { }
}

Марк не издевается. Это известная практика при использовании TDD. Это и есть «маленькие формализованные трансформации». Этим пользуются и Марк и Роберт Мартин и Рой Ошероув (и я и много кто ещё, полагаю).

Именно издевается. В противном случае я могу сказать, что любую функцию со входными данными практически невозможно протестировать. Возьмем, например, функцию full_name. Пишем тест:
it "should return full name based on first name and last name" do
  full_name("John","Doe").should == "John Doe"
end

Ок, пишем код, руководствуясь тестом и не включая мозги:
def full_name(first_name,last_name)
  "John Doe"
end

Тест прошел, но функция не работает как надо. Нужно проверить с другими входными данными:
it "should return full name based on first name and last name" do
  full_name("John","Doe").should == "John Doe"
  full_name("Jane","Smith").should == "Jane Smith"
end

Ок, тест валится, правим функцию еще раз, но мозг по прежнему не включаем (зачем? у нас же есть тесты):
def full_name(first_name,last_name)
  if first_name == "John" and last_name == "Doe"
    "John Doe"
  else
    "Jane Smith"
  end
end

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

В народе говорят: «Заставь дурака богу молиться, он и лоб расшибет».

А вот еще ссылка на статью по теме: habrahabr.ru/post/143616/
Ура! Есть еще вменяемые разработчики! Побольше бы таких как Вы!
Вы можете посмотреть подкасты с Роем Ошероувом и Робертом Мартином, где они используют эту технику.

if\else — это перебор, а идея минимальных приращений — нет. Смысл в том, что вы пишете ровно столько, сколько достаточно для прохождения теста. Ваш говнокод с If\else, следуя TDD необходимо отрефакторить, что отрефакториться в нормальный код. Но вы не имеете права сразу писать generic-код.

Я понимаю как всё это звучит. Но как бы всё это ни звучало странно прислушайтесь к тому, что говорят Кент Бек, Р. Мартин, Р. Ошероув, Марк Симен. Они не идиоты и далеко не все из них являются коучами.

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

Думаю, этого вполне достаточно.
Этого недостаточно.

Это подкаст Роберта Мартина.

Могу найти и подкаст Ошероува.

Речь не идёт о 100% покрытии кода. Речь идёт о применении тех или иных практик в различных ситуациях. Единственное о чём говорит Марк — это то, что со свойствами не всё так просто «что я их тупо не тестирую, они протестируются через другие тесты».

А я говорю о том, что эти подходы имеют право на жизни. Ещё как имеют. И люди ими успешно пользуются.
Парсер лох, съел ссылку. Появилась.
Тем не более, и К. Бек и Р. Мартин говорят, что тестировать каждую строку кода не нужно. Вы же на протяжении N комментариев пытаетесь доказать обратное, в конце концов еще и прикрываясь их именами. Причем, за все это время вы мне так и не объяснили, зачем писать тест для элементарной функции, длина которой едва ли больше названия теста для нее.
Отнюдь, я нигде не доказывал, что тестироваться должна каждая строчка. Где же я это доказывал? Я просто обсуждал TDD и его практики. Поступать можно по-разному.

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

Это объясняется в этой статье и в комментариях к этой.

p.s. Прочитайте все мои комментарии выше и вы поймёте, что я не zealot.
Здесь говорится о том, что я не могу предугадать тривиальность функции. Я утверждаю обратное, о чем и говорил выше. Возможно, я что-то упустил?
Статья намекает на то, что «тривиальность» величина непостоянная. Ещё хуже то, что тривиальность сложно аршином измерить.

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

> Ещё хуже то, что тривиальность сложно аршином измерить.
Возвращаясь к К. Беку. Я не буду писать тест, если уверен на 99.999%, что не ошибусь в функции, и если эти 0.001% не будут стоить мне или моему заказчику больших денег. Функция full_name, для которой вы предлагаете писать тесты — она самая и есть.
Как я, опять же, писал выше, когда к функции возрастут требования, я сразу это пойму.

А если ваш код будет править другой разработчик? Откуда ему знать есть ли у вас тесты или нет, он просто поправит и всё.

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

Но интересным остаётся вопрос практики: каков механизм понимания того, что вы 99% не ошибётесь? Почему не 93%? А если 93%, то почему не 37%?
Если другой разработчик решит править функцию до правки ее теста, то это будет последняя функция, которую он поправит в этом проекте. В противном же случае он заметит отсутствие теста и, если таковой действительно необходим, напишет.

Возвращаясь опять же к Кенту, это личный уровень уверенности. Я знаю, что вероятность ошибки в конструкции "#{first_name} #{last_name}" крайне мала. Максимум, что там может пойти не так — опечатка, которую я сразу замечу, т.к. интерпретатор выдаст ошибку. Ну или завалится один из интеграционных тестов.
суть тестирования не только нахождении/предотвращении багов. Но и в «закреплении» поведения.

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

Выдуманный пример, конечно. Но мало ли.

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

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

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

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

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

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


Мозг не причем. Функция пишется не для самосуществования. Зачем-то используется. И разработчик может увидеть в результате рефакторинга, что по сути, функция делает что-то другое. И не правильно называется.
не вижу бОльшей вины другого разработчика, который правит функцию без теста, чем того, кто не написал тест при написании функции сразу.
Я вижу. Мы тут все же TDD занимаемся, а значит при правке нужно первым делом править тест. Теста нет? (Функция элементарна, он ни к чему) Напиши тест, если новый требуемый функционал не тривиален. Не нужно бросаться в крайности.

Мозг не причем. Функция пишется не для самосуществования. Зачем-то используется. И разработчик может увидеть в результате рефакторинга, что по сути, функция делает что-то другое. И не правильно называется.
Это, опять же, не refActoring, а refUcktoring. Т.е. порча кода. Если какому-то разработчику вздумалось переименовать функцию или изменить ее функционал наобум, то ему хватит ума и удалить тест от старой функции и написать новый. Давайте для начала предположим, что не все разработчики идиоты, и не будем разрабатывать защиту от кодера-дурака.

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


Не получится ли, что поиск теста (вернее понимание, что его нет) займет времени больше чем его написание?
Открываем файл с тестами класса, ctrl+f, имя функции. Думаю, в пару секунд уложились. А теперь предположим, сколько времени займет написание + постоянное выполнение кучи ненужных тестов.
Это, опять же, не refActoring, а refUcktoring. Т.е. порча кода. Если какому-то разработчику вздумалось переименовать функцию или изменить ее функционал наобум, то ему хватит ума и удалить тест от старой функции и написать новый.

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

И про непротиворечивость утверждений вы наверное не поняли. Утверждения (т.е. тесты) прямо или косвенно могут иметь отношение к рассматриваемой одной функции. Поэтому, когда вы пишете тест перед написанием кода функции (какого-то ее функционала) — это одно. А когда вы начинаете переделывать функцию и начинаете с правки теста — это другое. Например, на вашу фукнцию уже с десяток тестов, причем косвенных. Те, которые там где-то интеграционно как вы писали проверяют и эту примитивную функцию. У там неявно тест подразумевает, что функция вернет «John Doe», а не «Doe John Doe». Меняя один тест, который, как вам кажется, тестирует нужный функционал (или дописывая ненаписанный тест), вы все тесты делаете противоречивыми. Т.е. разные утверждения (тесты) противоречат друг другу.
Стремно так кодить.

А вот, другой вариант. Вы пишете все тесты сразу. Хорошо, что на такие простые тесты времени много не надо. А потом, при изменении кода меняете сразу код. И тесты падают. Отлично! Меняем тесты.

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

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

Например, на вашу фукнцию уже с десяток тестов, причем косвенных. Те, которые там где-то интеграционно как вы писали проверяют и эту примитивную функцию. У там неявно тест подразумевает, что функция вернет «John Doe», а не «Doe John Doe». Меняя один тест, который, как вам кажется, тестирует нужный функционал (или дописывая ненаписанный тест), вы все тесты делаете противоречивыми. Т.е. разные утверждения (тесты) противоречат друг другу.
Омфг. Вот у меня есть тест, проверяющий правильность вывода списка пользователей. Он проверяет, что выводится имя и фамилия. До этого я «ручками» вставлял свойства «first_name» и «last_name». Со временем имена пользователей начали возникать то в одном, то в другом месте, и я, чтобы не нарушать принцип DRY, написал функцию-хелпер full_name. По факту эта функция проверяется во всех местах, где проверяется вывод пользователя. Если я напишу на нее отдельный тест, то это не отменит того, что она будет косвенно проверяться в остальных местах. Если я изменю эту функцию, то у меня завалится на 1 тест больше. Если я поменяю какой-то тест, то у меня станет на 1 противоречивый тест больше. Вот и вся разница.

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

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

Что за двойные стандарты? Если уж применяем TDD, то будьте добры начинать правку кода с тестов.

Как-то уже лет 8 не перечитывал Кента Бека и не могу такого вспомнить. Написание нового функционала начинается с написания теста. Там разве было «правка кода начинается с правки тестов»? Может я что-то упустил, но мне кажется совсем не логично. Тесты пишутся с нуля до кода, тесты правятся после падений. Вот так логично.
Если прямое написание функционала, то написать тест и запустить — это правильно. Тест упал и это внушает уверенность, что всё верно. Ведь вы пишете новый функционал. И если вдруг звезды сошлись так, что новый тест без функционала не упал, то это ппц как странно. Значит, вы многое не знаете о коде.

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

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

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

Тесты пишутся с нуля до кода, тесты правятся после падений. Вот так логично.
Есть 2 ситуации, в которых правится код: Рефакторинг/оптимизация и добавление/изменение функционала. В первом случае правка тестов не нужна, т.к. требования к коду не меняются. Во втором правка кода непременно должна начаться с изменения тестов, т.к. это стандартный цикл тест — код — рефакторинг — тест — … итд.

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

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

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

Чувствуете, чего я хочу от формулировки? То как я переформулировал мне тоже не нравится.

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

Надо изменить тест (изменить требование), полюбоваться на упавший тест, а потом изменить функцию и, возможно, полюбоваться на новые упавшие тесты, в которых функция «косвенно тестировалась».
Надо изменить тест (изменить требование), полюбоваться на упавший тест, а потом изменить функцию и, возможно, полюбоваться на новые упавшие тесты, в которых функция «косвенно тестировалась».

Да в общем, как хотите. Не принципиально. Я бы поступил наоборот. В случае прямого и явного изменения требования. Можно еще точнее — написать новый тест (не менять). А потом как обычно. Старый тест потом удалить. Суть как раз в том, что тесты должны падать как можно чаще. Их надо заставлять падать. Чтобы «ощущать, что вы на правильном пути». Тесты дают такое ощущение — ограничивают направления.
Исходя из этого, дергать на падения можно систему с любой стороны. Вы хотите изменить порядок конкатенации? Еще раз повторю — хреново, очень хреново, когда изменение логики кода не отражается падением тестов. Поэтому ничто не мешает это и проверить.
В данном случае я бы написал… Потому, как при локализации, например на русский, последовательность может поменяться… В русском last_name принято ставить впереди… )
Ну читайте же полностью, ну не объяснять же каждому отдельно… Да вы и сами написали об этом ниже. В случае локализации вы сам поймете, что функция перестает быть тривиальной, и напишите тест. А в данном отдельном случае тесты не нужны.
Это должно было звучать так: «В общем я с вами согласен, но в данном случае...» Не горячитесь… )
Я не горячусь ни в коем случае, дискуссия выходит крайне интересная. Просто объяснение одного и того же разным людям в рамках одной темы нарушает принцип DRY :)
Не стоит забывать ещё о том, что тесты — это документация. Вы ушли с проекта, пришёл другой. Теста нет. Что будет дальше будет решать случай.
В данном случае тест напоминает документацию в стиле «full_name — returns full name». Сомневаюсь, что хоть один разработчик напишет мне в скайп «Друг, я тут правлю твой код… А что за функция full_name?»
Мне кажется сложность написания тестов для всех геттеров и сеттеров слишком драматизируется.

В этом нашем похапэ можно сделать что-то такое например:
class FirstTest extends \PHPUnit_Framework_TestCase
{
  /** @dataProvider getters */
  function testMe($setter, $getter, $value, $exp = null)
  {
    $obj = new SomeClass();
    $this->assertEmpty($obj->$getter(), $getter . '() is empty');

    $obj->$setter($value);
    $this->assertEquals($exp ?: $value, $obj->$getter(), $getter . '() test');
  }

  function getters()
  {
    return array(
      array('setOne', 'getOne', 1),
      array('setTwo', 'getTwo', 2),
      array('setThree', 'getThree', 3, 4),
    );
  }
}


Добавить одну строчку для новой пары сеттер-геттер не такая уж страшная работа.
Я бы советовал не добиваться сокращения тестового кода путем усложнения. Пусть будет
class SomeClassTests extends TestCase {
    public void testSetOne() {
        SomeClass x = new SomeClass();
        x.setOne(1);
        assertEquasl(1, x.getOne());
    }
    public void testSetTwo() {
        SomeClass x = new SomeClass();
        x.setTwo(2);
        assertEquasl(2, x.getTwo());
    }
...
}

Много строк, много методов, зато когда упадет любой из тестов, вам нужно будет понять всего 3 элементарных строчки, а не алгоритм системы автоматической проверки. Который, кстати, может оказаться непригодным для каких-то условий!

Не пугайтесь, если на систему из 30 тыс строк, у вас 15 тысяч тестовых методов (т.е. 50 тыс строк тестов). Главное, чтобы все они были простыми.
>> у вас 15 тысяч тестовых методов (т.е. 50 тыс строк тестов)
Не потерять бы за этим всем самое главное качество юнит-тестов: Скорость их выполнения! ;)
В данном случае, я уверен: то что можно автоматизировать — нужно автоматизировать.

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

Ну и насчет понимания — этот тест не такой уж и сложный, код можно снабдить комментариями, а сообщения при ошибке точно дадут понять какой метод упал и почему. Дальше найти виновного уже дело техники.
Увы, этот тест не максимально очевидный и прямолинейный
Вот строчка
$this->assertEquals($exp ?: $value, $obj->$getter(), $getter. '() test');

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

Нет. Тесты не то место, где нужно делать что-то сложное.

Не потерять бы за этим всем самое главное качество юнит-тестов: Скорость их выполнения! ;)

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

Я предпочитаю компромисс. На каждое свойство свой тест (тестовый метод), но он из одной строчки типа testProperty($obj, $property_name, $value);
А-а-а-а-а!!!
a.Verify(Reflect<DateViewModel>.GetProperty<int>(sut => sut.Year));

Меня вымораживают такие тесты. Как это прочитать? Что здесь имеется ввиду?
"С помощью УтвержденияИзменяемогоСвойства удостовериться, что ЗапросСвойства типа int, вида sut.Year, взятый у отражения DateViewModel..." всё! Или, что УтверждениеИзменяемогоСвойства удостоверилось без возражений в.

И что будет в сообщении об ошибке когда тест не пройдет? «ЗапросСвойства типа int вида sut.Year не соответствует ожидаемому поведению из УтвержденияИзменяемогоСвойства»? WTF? Конкретнее!

Зачем нужно было отказываться от этого:
var sut = new DateViewModel();
sut.Year = 2010;
Assert.Equal(2010, sut.Year);

Именно так должны выглядеть тесты. Императивно, последовательно, линейно.
Читаем:
1. Взять для теста новый экземпляр DateViewModel созданный конструктором без параметров
2. Установить ему в Year значение 2010
3. Проверить, чтобы из Year вернулось значение 2010

Во-первых, ни один программист не прочтет это по другому. Во-вторых, любое падение этого теста будет банальным и прямолинейным.
«Не удалось создать объект для теста, по такой-то причине»,
«Не удалось установить значение 2010 свойству Year, по такой-то причине»,
«Из Year получено значение 10, тогда как ожидалось 2010»
.
Что-то мне подсказывает, что тут проблема семантическая и понятийная.

Getter'ы/setter'ы (далее буду использовать g/s) тестировать смысла нет, пока они дейстительно g/s. Первый же тест использующий это свойство, их действительно проверит. Написание отдельных тестов для g/s может банально нарушать принцип DRY.

Но фокус в том, что g/s в любой момент могут перестать быть тривиальными. Например. вы решили поменять структуру объекта. Вы перестали хранить значение свойства явно и сделали это свойство вычислимым. В этом случае, тесты конечно же просто необходимы. Но ведь это теперь уже не тривиальные g/s.

А ведь могут быть ситуации, вы можете никогда и не узнать, что g/s перестали быть тривиальными… Например, если вы пользовались сторонней библиотекой и её внутри модифицировали не меняя интерфейсов. Что в этом случае?

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

Похоже участники просто не договорились о понятиях и каждый говорит о какой-то своей ситуации… )
Роберт Мартин приводит в качестве аргумента то, что геттеры и сеттеры тестируются косвенно через другие тесты, но, несмотря на то, что это может быть верно на этапе объявления члена, не факт, что это будет верно сколь угодно долгое время. Спустя месяцы, эти тесты могут быть удалены, оставляя введённый тривиальный член непокрытым тестами.

Точто так же могут быть удалены и модульные тесты. Не забывайте что весь функционал в идеале должен быть покрыт приемочными тестами, которые косвенно протестируют тривиальный код.
Есть такой принцип «не усложняй!». Если геттер-сеттер выполняет тривиальную задачу, что вы тестируете в этом случае? Поведение платформы (.NET)? Мою IDE, сгенерировавшую к полям геттеры-сеттеры (JAVA)? Вот когда у вас появится необходимость нетривиальной реализации свойства, тогда и пишите к нему тест. Его наличие как раз и будет указывать, что свойство делает еще что-то, кроме простой операции с полем.
«каким образом на планете Земля» — забавно :-)
Sign up to leave a comment.

Articles