LINUX.ORG.RU

TDDшники, а расскажите про свою религию?

 , ,


4

2

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

В целом, вопросные тезисы и тезисные вопросы таковы:

  • С чего начинать? Как выбрать фичу, к написанию теста на которую приступить в первую очередь? Как не попасть в ситуацию «написал приёмочный тест вместо красивого чёткого TDD, ну ты лох», о которой многие упоминают, но не рассказывают, как правильно? Практически все мануалы показывают написание теста сразу для фичи верхнего уровня, что, КМК, приёмочным/дымовым тестом и является.
  • Написание теста для функционала, решение которого нетривиально, вынуждает писать тонны заглушек просто чтобы «скомпилируйся наконец уже пожалуйста». Количество заглушек умножается на количество «baby steps» при рефакторинге и добавлении новых тестов, генерируя адский объём механической работы. Определённо, значительная часть таких затыков порождена моей дизайнерской криворукостью, но мя не вижу способа полностью избежать этого рака.
  • Как TDD предлагает «пробивать» инкапсуляцию, когда функционал под тестом оказывается нетривиальным и требует вынесения части функционала в новую сущность? Многие статьи демонстрируют погрузку болта на внутреннюю сложность реализации, тестируя только контракт, что выглядит очевидно неправильным. Имеет ли концептуально реализация право порождать новый тест? Нужно ли, когда в ходе рефакторинга или позеленевания тестов требуется создать что-то новое, откладывать текущую работу над реализацией и идти писать новый тест для свежеобозначившейся проблемы? Что делать, когда поймёшь, что погряз в огромном объёме некомпилируемого кода и незапускающихся тестов?
  • Некоторые авторы предлагают следующий рекурсивненький жизненный цикл: ставим задачу верхнего уровня, решаем её. Если не удаётся за вменяемое время написать тест/реализацию, дропаем текущие наработки, собираем митап и распиливаем её на подзадачи, далее работаем с ними. Это выглядит минимально-рабочим, но вызывает вопросы: как планировать время на реализацию фичи, как рефакторить функционал более верхнего уровня, если он окажется концептуально неправильным, как избежать лавинообразного рефакторинга с проблемой кучи некомпилируемого кода, чем безумно дорогая по времени перековка какашки в конфетку лучше, классического предварительного планирования с UML и быстрого написания прототипов отдельных штуковин.

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

Change my mind, как говорится, если есть желание. Мя ещё не зафиксировал какого-то конечного мнения о сабже, но первые впечатления смешанные.

Ответ на: комментарий от Shadow

Аналог моего примера, но для римских чисел. Человек пишет муть, которая работает сначала только для 1, потом для 1-3, потом для 4, …, потом для 19. На этом видео закончилось и, например, для 40 программа по-прежнему выдаёт неправильный результат.

monk ★★★★★
()
Ответ на: комментарий от anonymous

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

anonymous
()
Ответ на: комментарий от anonymous

Гм.
Все таки вот антересно

double dValue = 0.29999999999999999; float fValut = (float) dValue;

Результат fValut == 0.300000012

Чуден Днепр при тихой погоде ...

Конечно нужно привести значение fValut к нужной значности, но пост не об этом …

anonymous
()
Ответ на: комментарий от anonymous

Результат fValut == 0.300000012

Всё правильно. Строго по аксиомам в IEEE 754.

monk ★★★★★
()

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

ЗЫ: Однажды попросил большого любителя TDD претендующего на работу написать тест для проверки реализации sin(). Очевидно, реализация sin() которая возвращает 2 если аргумент в точности равен 890384.32199809 является ошибочной и тест должен такой косяк мгновенно выявлять. Ну все помнят факап с Intel FDIV. Крендель почему-то сразу слился в закат со своим TDD.

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

Stanson ★★★★★
()
Последнее исправление: Stanson (всего исправлений: 2)
Ответ на: комментарий от Stanson

Какая вообще методика может отловить единичный баг чёрного ящика на огромном диапазоне? Ладно если там float какой на входе, можно просто перебрать все значения, а если длинная математика?

izzholtik ★★★
() автор топика
Ответ на: комментарий от izzholtik

Какая вообще методика может отловить единичный баг чёрного ящика на огромном диапазоне?

Доказательство кода, например. Влёт проверяются такие штуки. А TDD отсасывает в сторонке.

Ладно если там float какой на входе, можно просто перебрать все значения, а если длинная математика?

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

Stanson ★★★★★
()
Последнее исправление: Stanson (всего исправлений: 5)
Ответ на: комментарий от Stanson

Если весь процесс написания софта основан на TDD - то это с большой вероятностью херак-херак и в продакшен

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

untitl3d
()
Ответ на: комментарий от Stanson

Доказательство кода, например. Влёт проверяются такие штуки.

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

monk ★★★★★
()
Ответ на: комментарий от monk

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

Нет. Процессор это тоже код, нынче. Verilog какой-нибудь. А компилятор для таких штук не обязательно должен быть gcc, или тем более llvm которые доказать практически нереально. Вполне достаточно какого-нибудь tcc или ещё более простой штуки. Кроме того, для простые ответственные вещи типа sin() можно вообще на ассемблере написать исключив компилятор вообще. Кстати, вот ассемблер вполне можно полностью протестировать как раз этими самыми тестами.

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

Stanson ★★★★★
()
Последнее исправление: Stanson (всего исправлений: 3)
Ответ на: комментарий от untitl3d

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

Впрочем, TDD на собеседовании - это однозначно зашквар.

Stanson ★★★★★
()
Ответ на: комментарий от Stanson

Нет. Процессор это тоже код, нынче. Verilog какой-нибудь.

Так ты про реализацию sin в процессоре? Потому что код https://github.com/lattera/glibc/blob/master/sysdeps/ieee754/dbl-64/s_sin.c пройдёт доказательство, но на процессоре с ошибкой FDIV не пройдёт тест.

monk ★★★★★
()
Ответ на: комментарий от Stanson

«Доказательство» - NP-полная задача. Успехов что-то доказать не чём-то более сложном, чем простейший микроконтроллер.

anonymous
()
Ответ на: комментарий от anonymous

Успехов что-то доказать не чём-то более сложном, чем простейший микроконтроллер.

Процессор - это двоичный автомат на элементарной логике. Оно 100% доказуемо.

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

Stanson ★★★★★
()
Ответ на: комментарий от Stanson

Однажды попросил большого любителя TDD претендующего на работу написать тест для проверки реализации sin(). Очевидно, реализация sin() которая возвращает 2 если аргумент в точности равен 890384.32199809 является ошибочной и тест должен такой косяк мгновенно выявлять. Ну все помнят факап с Intel FDIV. Крендель почему-то сразу слился в закат со своим TDD.

TDD нужен не для того, чтобы находить ошибки в существующем коде, тем более в функциях стандартной библиотеки. Юнит-тесты являются характеристическими, они выявляют ошибки, внесённые новым кодом. В случае самописного синуса, например, мы берём для теста ЛЮБЫЕ ТРИ УГЛА и ожидаемые значения. При всех оптимизациях или рефакторингах функции синуса у нас должны получаться те же значения при тех же углах (или даже с допустимой погрешностью). Вот и всё.

Kogrom
()
Ответ на: комментарий от monk

… 100500 коммит:

 if p % 50250 == 0 && q % 50250 == 0:
 ...
 100500 строк
 ...```

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

Kogrom
()
Ответ на: комментарий от monk

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

А что мешает сначала написать тест с корректными значениями и корректными ответами?

goingUp ★★★★★
()
Ответ на: комментарий от LINUX-ORG-RU

я атеист, прежде всего. у меня нет никакой религии. к тестам отношусь серьёзно, но без фанатизма. они нужны при проектировании, чтобы выбрать более подходящее решение (чтобы потом не было мучительно больно за выбор какой-то глючной фигни). потыкать палочкой разные библиотеки, разные реализации, алгоритмы и т.д. хотя я видела, как на тестах выезжали очень большие проекты с жуткой текучкой кадров. но там были обычные юнит-тесты, никакого «драйва» они не создавали. просто когда у тебя проект на 50-60 мегабайт кода, который писали 20 лет разные люди, то по-другому никак. тесты не спасают от говнокода или ошибок, но, по крайней мере, гарантируют, что рандомное изменение в одном месте не повалит весь карточный домик и разъярённые юзеры не будут писать рекламации пачками. чем больше проект - тем больше смысла в автоматических тестах. там и QA подключаются, и всякая автоматическая сборка налаживается. а им нужно как-то проверять, что всё собралось нормально. поэтому тесты просто формально нужны, чтобы убедиться, что после какого-то коммита ничего сильно не поломалось.

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

Iron_Bug ★★★★★
()
Ответ на: комментарий от goingUp

Без TDD, вы, конечно же, написали бы нормально)

Без TDD первым пунктом описание алгоритма для всего диапазона входных данных.

Например, в примере с римскими числами и TDD, скорее всего вместо полутора десятков тестов будет один на число 24, но алгоритм сразу будет писаться на весь допустимый диапазон (если в задании написано, что до 39, то в том виде, как в примере).

monk ★★★★★
()
Ответ на: комментарий от goingUp

А что мешает сначала написать тест с корректными значениями и корректными ответами?

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

monk ★★★★★
()
Ответ на: комментарий от Kogrom

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

Это не про TDD, а про тесты и регрессионное тестирование. Тесты можно и для существующего кода написать. Например, для sin очевидные тесты sin(0), sin(pi/2), sin(pi), sin(pi/4), sin(100500*pi).

monk ★★★★★
()
Ответ на: комментарий от anonymous

Расскажите поподробней про это мифическое существо!

У Вас в школе не преподавали математику?

monk ★★★★★
()
Ответ на: комментарий от Shadow

ты хочешь сказать, такого tdd много в жизни?

Кто же его знает, как там в жизни. По инструкции к TDD надо сначала писать тест, а потом код. Получаем тесты для «черного ящика».

monk ★★★★★
()
Ответ на: комментарий от monk

Дайте, пожалуйста, математическое определение для «правильно». Пока забудем про математическое определение «алгоритма».

anonymous
()
Ответ на: комментарий от anonymous

Дайте, пожалуйста, математическое определение для «правильно».

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

monk ★★★★★
()
Ответ на: комментарий от monk

Алгоритм

Я же сказал, что пока забудем про «алгоритм».

для всех входных значений выдаёт результат, требуемый заданием для данных входных значений.

То есть проходит все тесты, которые заранее определены. То есть термин «правильно» определено через «тесты». Так?

Допустим, что множество тестов (входные данные - результат) конечно, ну по кайней мере счётно.

anonymous
()
Ответ на: комментарий от anonymous

То есть проходит все тесты, которые заранее определены. То есть термин «правильно» определено через «тесты». Так?

Только если постановка задачи задана списком тестов.

Допустим, что множество тестов (входные данные - результат) конечно, ну по кайней мере счётно.

Возможных тестов всегда конечно (у нас конечное количество возможных входных данных, так как память конечна).

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

На языках с достаточно мощной системой типов (Agda, Liquid Haskell) можно автоматически доказать, что реализация алгоритма сортировки соответствует определению.

monk ★★★★★
()
Ответ на: комментарий от anonymous

Я же сказал, что пока забудем про «алгоритм».

Тогда что должно быть правильно? Хорошо, пусть будет текст программы.

monk ★★★★★
()
Ответ на: комментарий от monk

Только если постановка задачи задана списком тестов.

Как в «постановке задачи» определена «правильность»?

Возможных тестов всегда конечно

Не существует «генератора тестов»?

для любого

опасно!

отсортированный массив (то есть множество элементов то же и они упорядочены)

Проверка (тест) на присутствие определенных свойств у результата при входных данных, имеющих определенные свойства?

anonymous
()
Ответ на: комментарий от monk

Но задание крайне редко задаётся тестами. Как правило задание вида «отсортировать (определение сортированности прилагается) элементы (любого) массива по возрастанию

Сортировка массива идеальное задание для TDD.

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

Пишутся тесты вида:

for (int i = 0; i < 100; ++i) {
    auto random_vector = generate_random_vector(random(1, 10000));
    auto copy_vector = random_vector;
    my_sort(copy_vector);
    assert(std::ranges::is_sorted(copy_vector));
    assert(std::ranges::is_permutation(copy_vector, random_vector));
}

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

property based тесты отлично гармонируют с TDD.

def gcd(p, q):
  if p % 50250 == 0 && q % 50250 == 0:
  ...
  100500 строк
  ...

И вообще, какой-то у тебя китайский TDD. Везде где упоминают TDD, говорят про три шага.

  1. Пишем тест который падает.

  2. Пишем код, чтобы тест проходил.

  3. Рефакторим этот код, чтобы тест по прежнему проходил.

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

Видимо с кем ты общался о TDD забывали про третий шаг…

Вот как пример: https://imgur.com/a/wNWun4U

скрины из первого попавшегося видео о TDD: https://youtu.be/_M4o0ExLQCs

fsb4000 ★★★★★
()
Последнее исправление: fsb4000 (всего исправлений: 1)
Ответ на: комментарий от anonymous

Как в «постановке задачи» определена «правильность»?

???

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

Не существует «генератора тестов»?

Он может сгенерировать только конечное число тестов.

Проверка (тест) на присутствие определенных свойств у результата при входных данных, имеющих определенные свойства?

Это позволяет написать генератор тестов. Кстати, в TDD использование генератора не подразумевается.

monk ★★★★★
()
Ответ на: комментарий от fsb4000

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

Ещё будут нужны тесты для is_sorted и is_permutation.

И на хабре в статье написали кучу таких генераторов тестов для функции sum(a, b), которая должна вернуть a + b.

const [n1, n2, n3] = [rand(), rand()]
const actual = sum(n1, n2)
const expected = sum(n2, n1)

equal(actual, expected)

const [n1, n2, n3] = [rand(), rand(), rand()]

const left = sum(sum(n1, n2), n3)
const right = sum(n1, (sum(n2, n3)

equal(left, right)

Оказалось, что по TDD достаточно реализации sum(a, b) { return 0 }.

monk ★★★★★
()
Ответ на: комментарий от monk

Постановка задачи и есть правильность.

«Постановка задачи» может быть только «правильной»?

Он может сгенерировать только конечное число тестов.

За конечное время. (Тут упираемся в определение «алгоритма»)

Кстати, в TDD использование генератора не подразумевается.

Например, fuzzing применяется в TDD.

anonymous
()
Ответ на: комментарий от fsb4000

И вообще, какой-то у тебя китайский TDD. Везде где упоминают TDD, говорят про три шага.

Так я «китайский TDD» и получил через эти 3 шага. Для человека, который не очень понимает, как пачку условий рефакторить в цикл. И которому важно каждый день/час сделать пару коммитов (за это зарплату платят).

К тому же даже если он умеет рефакторить, но не знает математику, то получится (после 5-6 шага) что-то вроде

def gcd(p, q):
  for i in range(min(p, q), 1, -1):
    if p % i == 0 && q % i == 0:
       return i
  return 1

что ещё бесконечно далеко от нормальной реализации. Но тесты проходит.

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

Зависит от тестов, которые сформированы при TDD. Вот заменили в memcpy внутри алгоритм, тесты проходит, а в других местах всё поломалось.

monk ★★★★★
()
Ответ на: комментарий от monk

Оказалось, что по TDD достаточно реализации sum(a, b) { return 0 }

Может надо было изучить свойства групп (их вроде немного)?

anonymous
()
Ответ на: комментарий от anonymous

«Постановка задачи» может быть только «правильной»?

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

За конечное время.

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

monk ★★★★★
()
Ответ на: комментарий от monk

то может быть и неправильной.

Ты уж определись.

На реальном компьютере.

Что такое «реальный компьютер»? Квантовый считается? Аналоговые счетные машины?

anonymous
()
Ответ на: комментарий от anonymous

Может надо было изучить свойства групп (их вроде немного)?

Ассоциативность проверена, аксиома существования обратной операции на тест плохо ложится.

monk ★★★★★
()
Ответ на: комментарий от anonymous

Ты уж определись.

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

Прилагательное «правильный» применённое к прочим объектам может иметь другие значения. Применённое к постановке может подразумевать, что для данной постановки существует правильный алгоритм.

Что такое «реальный компьютер»? Квантовый считается? Аналоговые счетные машины?

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

monk ★★★★★
()
Ответ на: комментарий от monk

У тебя противоречивая «постановка задачи», когда одновременно «правильно» и «неправильно».

anonymous
()
Ответ на: комментарий от anonymous

У тебя противоречивая «постановка задачи», когда одновременно «правильно» и «неправильно».

Что одновременно правильно и неправильно?

monk ★★★★★
()
Ответ на: комментарий от monk

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

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.