IT-инфраструктура для бизнеса и творчества
Разработка
nin jin

Деконструкция TDD

Test Driven Development. Суть этого подхода заключается в ритуализации процесса разработки. То есть в некритическом безусловном выполнении определённых простых действий.

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

Видео запись разбора. А ниже — его текстовая расшифровка.

Суть TDD

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

И тут сразу возникает вопрос впрос на миллион...

Что делать, когда тест изначально зелёный?

Варианты ответов...

Сломать код
Удалить тест
Это невозможно
Показать результаты
Переголосовать
Проголосовать

Если сломать код, то тесты естественным образом покраснеют. А после того как мы откатим изменение, тесты снова станут зелёными.

Можно удалить тест. Ведь если нет теста, то нет и проблемы с его изначальным цветом.

Наконец, моё любимое: по TDD такого быть не должно. Где-то ты накосячил, что у тебя так получилось. Покайся, грешник.

А теперь, внимание, правильный ответ: все эти варианты - это полная беспросветная чушь. Хотя именно один из них, как правило, слышишь от адептов TDD.

Парадокс воронов

Говоря про ломание кода, нельзя не упомянуть про парадокс воронов. Суть его в том, что задаётся вопрос: "Все ли вороны чёрные?". И для ответа на него берутся нечёрные предметы. Например - красные яблоки. И каждое такое яблоко как бы подтверждает тезис о том, что "все вороны чёрные", ведь яблоки не чёрные и при этом не вороны. Что-то тут не так с логикой, не правда ли?

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

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

Иначально зелёные тесты неизбежны

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

  1. ❌ ⇝ ✅
  2. ❌ ⇝ ✅
  3. ❌ ⇝ ✅
  4. ✅ ❓
  5. ✅ ❓
  6. ✅ ❓
  7. ✅ ❓
  8. ✅ ❓

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

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

Правильный TDD

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

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

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

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

В такой форме TDD уже можно применять с пользой. Однако...

TDD приводит к куче лишней работы

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

Давайте рассмотрим типичный сценарий написания простой функции...

| Итерация | В начале | В процесссе | В результате |----------|-----------------|-----------------|-------------------------- | 1 | ❌ | ❌ | ✅ | 2 | ✅❌ | ❌❌ | ✅✅ | 3 | ✅✅❌ | ❌❌❌ | ✅✅✅ | 4 | ✅✅✅❌ | ✅✅❌❌ | ✅✅✅✅ | 5 | ✅✅✅✅❌ | ✅✅✅✅❌ | ✅✅✅✅✅ | 6 | ✅✅✅✅✅❌ | ❌❌❌❌❌❌ | ✅✅✅✅✅✅

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

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

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

Когда TDD полезен

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

  • ✅ Исправление дефектов
  • ✅ Заранее известный контракт
  • ✅ Не заставить себя писать тесты

Если к вам пришёл баг, то вы его можете сперва формализовать в виде теста, а далее исправлять код, пока все тесты не позеленеют.

Кроме того, в ситуации, когда контракт вам известен заранее, вы можете сразу написать все тесты, а потом уже весь код, который им соответствует. Формально, это будет не TDD, так как вы не будете менять код после добавления каждого теста. Однако, это самый что ни на есть Test Driven, так как тесты будут диктовать вам реализацию.

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

Программировать ли по TDD?

Если кто-то вам скажет, что он "программирует по TDD", то можете быть уверены, что он попросту не ведает, что творит. У TDD есть ряд фундаментальных проблем, поэтому его применение оправдано лишь в весьма ограниченном числе случаев. И то, не в той форме в которой ему как правило учат многочисленные коучи.

  • ❌ Ритуализация
  • ❌ Явно некорректный код
  • ❌ Бесполезная работа
  • ✅ Там, где это уместно
  • ✅ Не зацикливаться

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

Что ещё посмотреть по TDD?

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

{ "author_name": "nin jin", "author_type": "self", "tags": [], "comments": 1, "likes": 0, "favorites": 3, "is_advertisement": false, "subsite_label": "dev", "id": 143127, "is_wide": true, "is_ugc": true, "date": "Sat, 18 Jul 2020 09:55:58 +0300", "is_special": false }
(function () { let cdnUrl = `https://specialsf378ef5-a.akamaihd.net/SelectelBranding/images/` let previousArticleNumber = null let currentArticleNumber = 0 let platform = 'Desktop' let articles = [ { name: 'camera', url: `${cdnUrl}CameraCat`, text: 'умную камеру для\u00A0наблюдения за\u00A0котиками', link: 'https://vc.ru/selectel/306690', num: 3 }, { name: 'chill', url: `${cdnUrl}ChillCat`, text: 'трекер, который подскажет, когда пора отдохнуть', link: 'https://vc.ru/promo/288561-eye-tracker', num: 1 }, { name: 'cloud', url: `${cdnUrl}CloudCat`, text: 'котика: даёшь ему «пять», а\u00A0он делает бэкап в облако', link: 'https://vc.ru/dev/294799-maneki-neko', num: 2 } ] let buttonCycle = document.querySelector('.button--cycle') let buttonChoose = document.querySelector('.button--choose') let buttonMobile = document.querySelector('.button--mobile') let textField = document.querySelector('.selectel-footer-subtitle') let imageAgent = document.querySelector('.image--agent') let banner = document.querySelector('.selectel-footer') buttonCycle.addEventListener('click', cycleClick) buttonChoose.addEventListener('click', () => sendEvent(`Promo ${articles[currentArticleNumber].num} Left`, 'Click')) buttonMobile.addEventListener('click', () => sendEvent(`Promo ${articles[currentArticleNumber].num} Left`, 'Click')) let media = window.matchMedia("(max-width: 570px)") media.addEventListener('change', matchMedia) function matchMedia() { if (media.matches) { platform = 'Mobile' } else { platform = 'Desktop' } update() } matchMedia() function cycleClick(event) { sendEvent(`Promo ${articles[currentArticleNumber].num} Right`, 'Click') if (event) { event.preventDefault() event.stopPropagation() } window.open('https://vc.ru/tag/selectelDIY', '_blank') //cycle(event) } function cycle(event) { // incrementArticleNumber() textField.innerHTML = generatedText() imageAgent.src = articles[currentArticleNumber].url + platform + '.svg?3' imageAgent.setAttribute("class", "") imageAgent.classList.add('image--agent', articles[currentArticleNumber].name) banner.href = articles[currentArticleNumber].link } function update() { banner.href = articles[currentArticleNumber].link imageAgent.src = articles[currentArticleNumber].url + platform + '.svg' textField.innerHTML = generatedText() } function incrementArticleNumber() { previousArticleNumber = currentArticleNumber if (currentArticleNumber >= articles.length - 1) { currentArticleNumber = 0 } else { currentArticleNumber++ } } const sendEvent = (label, action = 'Click') => { const value = `SelectelDIY — loc: Footer — ${label} — ${action}`; if (window.dataLayer !== undefined) { window.dataLayer.push({ event: 'data_event', data_description: value, }); } }; function generatedText() { let defaultText if (platform === 'Desktop') { defaultText = `Мы тут собрали %text%. Хотите научим?` } else { defaultText = `Мы тут собрали %text%.` } return defaultText.replace('%text%', articles[currentArticleNumber].text) } function getRandom(min, max) { min = Math.ceil(min) max = Math.floor(max) return Math.floor(Math.random() * (max - min + 1)) + min } (function create() { currentArticleNumber = getRandom(0, articles.length - 1) cycle() let page = document.querySelector('.page--entry') if (page) { function insertAfter() { let parents = page.querySelectorAll('[data-id="7"]') let referenceNode = parents[0] referenceNode.parentNode.insertBefore(banner, referenceNode.nextSibling); loaded() } setTimeout(() => insertAfter(), 0) } }()) function loaded() { banner.classList.add('loaded') } loadImages([ `${cdnUrl}CameraCatDesktop.svg`, `${cdnUrl}ChillCatDesktop.svg`, `${cdnUrl}CloudCatDesktop.svg`, `${cdnUrl}CameraCatMobile.svg`, `${cdnUrl}ChillCatMobile.svg`, `${cdnUrl}CloudCatMobile.svg?3`, ]) function loadImages(urls) { return Promise.all(urls.map(function (url) { return new Promise(function (resolve) { var img = document.createElement('img'); img.onload = resolve; img.onerror = resolve; img.src = url; }); })); } }())
0
1 комментарий
Популярные
По порядку

Какой чудесный буллшит. 

1
Читать все 1 комментарий
Умная боксерская груша, которая научит вас драться онлайн

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

Предсказать и оптимизировать: плавим сталь с помощью Data Science

Как создавали программу, которая помогает металлургам экономить 3-5 млн долларов в год

Как улучшить идеально настроенную ecom-рекламу? Удваиваем онлайн-продажи обувной федеральной сети всего за месяц

Хитрим с Google Merchant, усмиряем алгоритмы, делимся лайфхаками. Как за неделю обрушить рекламу обувной федеральной сети, но удвоить с неё продажи всего за месяц. Свежий кейс агентства МАКО в 2021 г.

У альфа-банка стоит ограничение на отображение баланса в пуше, его не видно полностью

Скорее всего, Альфа-банк провел свой аналог "Игры в Кальмара", где разыграл суперприз. Их версия отличается от корейской тем, что там не надо никого убивать и проходить испытания. Вы, как обычно, пользуетесь картой и получаете шанс выиграть супер приз! Когда мне пришел этот пуш, я сразу понял, что не зря был вашим клиентом столько лет! Вы…

«AliExpress Россия» получил контроль над KazanExpress, который хвалил за эффективность и работу с малым бизнесом Статьи редакции

Сооснователь маркетплейса Линар Хуснуллин рассказывает, что он считает особенным в своём проекте.

Почта России запустила доставку из отделений роботами Яндекса

Почта России первой из почтово-логистических компаний начала доставлять посылки с помощью беспилотных роботов-доставщиков Яндекса. Проект реализован при поддержке Фонда «Сколково». На первом этапе 36 роверов будут осуществлять доставку из 27 отделений в Москве. Воспользоваться услугой можно будет через приложение Почты на Android, на старте…

Кикшеринг станет ещё доступнее, но для крупных игроков места больше нет: интервью с инвесторами кикшеринга «Юрент» Статьи редакции

Андрей Азаров и Михаил Гейшерик рассказали, зачем «Юрент» купил сервис аренды обычных велосипедов, продолжится ли бурный рост кикшеринга и что этому может помешать.

Михаил Гейшерик и Андрей Азаров
Секретная сделка Google и Facebook и другие факты из нового антимонопольного разбирательства

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

Улыбнитесь, вас снимают: как биометрия меняет жизнь банковских клиентов

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

Классификация текста с Elasticsearch

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

null