Специалисты из нескольких ВУЗов Европы – Давиде Фуччи, Джузеппе Сканиелло, Симоне Романе, Мартин Шеппэрд, Бойсе Сигвени, Фернандо Уйагуари, Бурак Туран, Наталья Юристо и Марку Ойиво – провели очередное исследование на тему эффективности тестирования ПО. Они рассмотрели методологии Test Driven Development (TDD) и Test Last Development (TLD).
Исследователи сравнивали их по двум показателям – суммарная скорость разработки продукта и качество исходного кода. Первая методология (разработка через тестирование – TDD) вновь не оправдала возложенных надежд: популярная ранее схема тестирования после разработки (TLD) оказалась не менее эффективной. Так что по указанным выше показателям существенных отличий они не обнаружили.
В таком случае чем же объясняется вспышка интереса к TDD, когда она только появилась? Эта методология возникла в 2000-х, так что теперь элемент новизны можно смело сбросить со счетов. Тем не менее, предметом споров она остается до сих пор.
Автор TDD Кент Бек выделяет пять основных этапов при использовании методологии на практике:
• Написать новый тест-кейс;
• Убедиться, что запуск нового теста приведет к сбою;
• Написать новый или модифицировать код так, чтобы тест прошел успешно;
• Перезапустить все остальные тесты и подтвердить успешное их прохождение;
• Сделать рефакторинг кода, устранив избыточность.
TDD и TLD имеют свои преимущества и недостатки. Причем, преимущество одной методологии зачастую является недостатком для другой.
• Скорость разработки тестов
В случае с TDD инженерам приходится тратить на 16% больше времени, чем с TLD. Это объясняется дополнительным затратами на переключение разработчиков между написанием тестов и основного кода.
• Порог вхождения
У TDD выше порог вхождения, так как это не только методология тестирования, это иной подход к разработке ПО. Поэтому разработчику необходимо время, чтобы понять его и начать использовать на практике.
• Производительность и сопровождение
Благодаря TDD снижаются затраты на сопровождение программного продукта. Обычно при использовании этой методологии количество тест-кейсов примерно на 50% больше, чем в случае с TLD. Это дает большее покрытие и подразумевает повышенную надежность продукта. Поэтому и сопровождать такое ПО легче. За счет хорошо продуманной архитектуры производительность системы, разработанной с использованием, TDD обычно выше.
• Объем кода
Применение TLD позволяет существенно сократить объем кода по сравнению с TDD. Кроме того, код с TLD зачастую имеет более простую структуру.
• Внесение изменений
С TDD разработчики обычно могут быть уверены, что изменения не вызовут нежелательных эффектов. Все необходимые тесты запускаются после каждого внесенного изменения. Благодаря разработке через тестирование отладка ПО в целом становится более прозрачной и осознанной.
В любом случае каждый проект индивидуален, но общие закономерности существуют — так же, как и существуют различные компромиссы.
Компромиссы
Иван Хватов:
И то, и другое – крайности. Всё зависит от ситуации. В целом, тесты – это очень больная и холиварная тема. Вопрос ещё не закрыт о том, какие тесты вообще писать надо или не надо: юнит, функциональные, или интеграционные (или все).Приведем пример возможного диалога между менеджером и архитектором.
А: — Хотим ли мы 100% покрытий?Широко распространено мнение о том, что дизайн среднего качества обеспечит покрытие ключевых кусков кода на 80-85%, а каждые последующие 5% будут отнимать все больше и больше ресурсов (труда и времени).
М: — Да, конечно.
А: — Отлично, только это затянет разработку вдвое и усложнит сопровождаемость тестового кода на порядок.
Более того, автор TDD Кент Бек как-то высказывал мысль, что ненужные тесты можно удалять. Такие тесты он называет delta coverage – дополнительное покрытие, обеспечиваемое конкретным набором тестов. Если это дополнительное покрытие равно 0, то тест можно смело удалять.
Кент Бек — разработчик программного обеспечения, создатель таких методологий разработки ПО как экстремальное программирование (XP) и разработка через тестирование (TDD). Бек был одним из 17 специалистов, подписавшихAgile Manifesto в 2001 году.
Лишние тесты приводят к дополнительным затратам на сопровождение. Именно поэтому, тесты, полученные на ранних этапах обычно являются слишком наивными и могут быть удалены. Особенно это касается методологии TDD.
Мартин Фаулер привел два простых эмпирических правила, которые определяют нижнюю и верхнюю границы покрытия: если вы не уверены в своих изменениях, и многие из них приводят к сбоям, но не ломают тесты, значит тестов слишком мало. Если же при каждом изменении ломается слишком много тестов, то, возможно, тестов слишком много.
Мартин Фаулер — автор ряда книг и статей по архитектуре ПО, объектно-ориентированному анализу и разработке, языку UML, рефакторингу, экстремальному программированию, предметно-ориентированным языкам программирования.
Когда стоит использовать TDD?
На графике изображена скорость развития двух проектов. Синие принесли в жертву дизайн и тесты. Их цель – как можно скорее реализовать максимальный объем функциональности. Красные, напротив, уделяют большое внимание дизайну системы и её тестированию.
Очевидно, что синие оставляют в коде много «технических долгов», чем обрекают себя на трудности при расширении системы в будущем и проблемы с её поддержкой.
Но за счёт чего красные умудряются держать систему гибкой и двигаться со стабильной скоростью? Одна из причин — это использование TDD, пишет Александр Бындю, эксперт по Agile и Lean.
Он приводит ряд преимуществ TDD в проекте красных:
1. Самое простое – в таком проекте больше тестов, по сравнению с обычным подходом. С одной стороны, само по себе количество тестов не гарантирует более высокого качества проекта и отсутствия ошибок. С другой, всё зависит от того, насколько хорошо вы умеете писать эти тесты. Много хороших тестов сэкономят вам много времени.
2. Код становится более качественным. Это связано с тем, что модульное тестирование подразумевает слабую связанность различных модулей системы, иначе будет очень сложно написать эти модульные тесты. Поэтому приходится применять, например, принципы проектирования классов S.O.L.I.D:
Принцип единственности ответственности (The Single Responsibility Principle)В конечном счёте, это даст всей системе большую мобильность и гибкость.
Принцип открытости/закрытости (The Open Closed Principle)
Принцип замещения Лисков (The Liskov Substitution Principle)
Принцип разделения интерфейса (The Interface Segregation Principle)
Принцип инверсии зависимости (The Dependency Inversion Principle)
3. Мы можем перестраивать наш проект сколько угодно, адаптировать его к новым требованиям и не бояться, что после очередного рефакторинга мы потеряем какую-то уже работающую функциональность. Почему мы можем себе это позволить? Потому что после рефакторинга запустим все тесты, и они нам покажут (зеленой полоской), что все бизнес-функции до сих пор работают.
Поэтому, по мнению Бындю, TDD вынуждает писать более качественный код, который удобнее тестировать. А это значит, что мы оставляем в коде меньше технических долгов.
Когда же TDD начинает обгонять? Тогда, когда синяя и красная линия пересекаются. В это время команда синих вовсю начинает платить за долги. Отсюда вывод о границах применимости TDD:
Вы можете писать код без TDD, если уверены, что до пунктирной линии дело не дойдет. Например, вы пишете небольшой проект для автоматизации внутренней работы, или создаете сайт-визитку.Надо понимать, что TDD – не гарантия качества вашего кода, но с ним проще держать систему в тонусе, заключает он.
Не одно «но»
Дэвид Ханссон предупреждает, что увлеченность тестированием и попыткой протестировать все в полной изоляции (когда мы тестируем лишь текущий класс, без его зависимостей или вспомогательных низкоуровневых классов) приводит к нагромождению ненужных слоев абстракции и переусложненному дизайну.
Давид Хейнемейер Ханссон — датский программист, автор веб-фреймворка Ruby on Rails, основатель Instiki wiki и автогонщик, победитель в классе LMGTE Am (2014) и серебряный призёр LMP2 (2013) чемпионата мира по гонкам на выносливость, победитель 24 часов Ле-Мана 2014 года в классе LMGTE Am.
Однако у Мартина Фаулера есть важная оговорка на этот счет:
Когда система начинает разрастаться, то проблема не в количестве уровней абстракции, а в количестве уровней изоляции. То есть, сам факт того, что класс А вызывает метод класса Б, который обращается к классу В не представляет проблему до тех пор, пока между каждым из этих классов не появляется уровень изоляции в виде интерфейса.С другой стороны на эту тему смотрят некоторые пользователи «Хабра».
Мнение пользователя Volch:
Нужно понимать, что применение таких методологий как TDD предполагает, что автотесты пишут разработчики, а не тестировщики. Это методологии программирования и кодирования, а не разработки готового продукта, в которую вовлечено множество людей от ПМов и техписателей до админов и техподдержки. Разработчики же зачастую относятся к внедрению TDD-like как взваливанию на них обязанностей тестировщиков.Мнение пользователя Tagakov:
TDD действительно не то, что должны делать тестировщики. Их задача писать функциональные тесты и, если хватает квалификации, интеграционные. TDD не TDD, но разработчики испокон веков занимались тестированием, запуская свой код и проверяя как он работает, юнит-тестирование это скорее формализация этого процесса.Мнение пользователя Kunis:
Если тест написан до и предоставлен программисту в качестве исходных данных, он становится «техническим требованием» (requirement). Разумеется, тот, кто пишет тест, должен заранее написать и запрограммировать все требуемые моки. Ха-ха, не хотел бы я быть тем программистом, в обязанности которого входит программировать тесты для других.
Ну, а может ли сам программист написать тесты к своему коду заранее? По моему опыту, нет и нет. Программисты (и я и подавляющее большинство, с кем я это обсуждал) просто думают в обратном направлении. Да, поначалу люди пытались честно следовать методологии. Но спустя какое-то время, начинали сначала писать код, а потом на него дописывать тесты. А еще спустя какое-то время, тесты потеряли свою разносторонность и дотошность. А потом и вовсе писались «штоб было», раз уж просят.
А оно вообще надо?
Адаптация TDD
Свое мнение также высказывал один из апологетов разработки через тестирование Александр Люлин:
Я не использую TDD в его классическом понимании. Вообще, вряд ли кто-то из профессионалов рассматривает энциклопедические статьи в качестве руководства к действию. Мы свой подход «выстрадали» в рамках реализации успешного проекта, поэтому за нами реальный опыт, а не «тупое использование чужих идей».
Скорее, мы используем синтез из TDD и собственных представлений о том, как нужно разрабатывать ПО. Даже если эти «внешние идеи» исходят от очень умных людей, их следует критически осмыслить и адаптировать к реальной компании, существующей команды и стратегии развития и обеспечения качества.
Олег Балбеков, СЕО Vexor:
На наш взгляд жестко разделять данные подходы довольно сложно и вообще не нужно. Так повелось, что в нашей команде мы обычно совмещаем использование обоих подходов в одном проекте, и даже иногда при разработке одного блока кода.
Ну, например, когда мы пишем часть проекта для работы с сервисами через API, мы пишем сначала тесты и уже потом код (TDD). Однако после реализации часто бывает так, что написанных заблаговременно тестов недостаточно и приходится их дописывать, добиваясь 100% покрытия (TLD).
Иногда даже на очень простые контроллеры (обычный ресурс с базовыми CRUD), сначала пишутся простые тесты, а потом уже код (TDD). Это нам дает уверенность в том, что ничего не забыто в первоначальной реализации. В дальнейшем, при развитии проекта, в большинстве случаев тесты пишутся уже после написания кода (TLD).
Часто есть задачи реализации алгоритма на известные входные/выходные данные, тут тоже в основном используется TDD подход. Если есть готовые входные и выходные данные, то по ним очень легко написать простой тест, а уже потом реализовать этот алгоритм, будучи уверенным в результате.
Чаще всего TLD подход применяется во время рефакторинга. Сначала мы покрываем тестами существующую реализацию и уже потом переписываем ее.
Если говорить про соотношение использования TDD и TLD в проектах, то выигрывает обычно TLD. Разработчиков, которые просто пишут тесты всегда больше тех, кто в состоянии «думать тестами» и писать тесты заблаговременно.
Комментарии (66)
renskiy
25.10.2016 20:02Мне кажется автор методологии TDD не совсем правильно поступил, когда выделил в этой концепции конкретные этапы. TDD все-таки не процесс, а принцип разработки ПО. А суть принципа в том, что тесты являются неотъемлемой частью кода, а не его опциональным дополнением.
При этом не так важно в какой очередности будут добавляться тесты к коду. Главное тут то, что и написание кода, и тестов к нему — есть единая активность, а не две разные задачи. Я хочу сказать, что в TDD тесты и код пишет один и тот же человек, и тесты в этом случае являются способом проверки работоспособности кода (гипотезы).ApeCoder
26.10.2016 08:53TDD это Test Driven Development — то есть тесты должны двигать разработку — то есть быть впереди. Если вы хотите убрать этот принцип, тогда надо придумывать другое название.
Я хочу сказать, что в TDD тесты и код пишет один и тот же человек, и тесты в этом случае являются способом проверки работоспособности кода (гипотезы).
Так же чтобы убедиться в корректности дизайна, документировать и т.д. См. Why we test
renskiy
26.10.2016 10:51TDD это Test Driven Development — то есть тесты должны двигать разработку — то есть быть впереди
Все зависит от интерпретации на самом деле. Driven не всегда означает pull (тянуть за собой), вполне себе неплохо получается и при использовании push (толкать впереди себя). Поэтому я считаю, что driven в контексте TDD должно означать «разработку через тестирование», что не указывает явно на то, в какой конкретно момент должны появится тесты — до кода или после.ApeCoder
26.10.2016 10:54Интересно, как тесты, которых еще нет могут управлять разработкой
renskiy
26.10.2016 11:09Я использую тесты для того, чтобы не запускать код самому для того, чтобы убедиться, что он работает. И это TDD, потому что без тестов такой вариант не работает. Без тестов мне пришлось бы на своем рабочем окружении поднимать сложную инфраструктуру из огромного числа сервисов и зависимостей, плюс проходить вручную каждый пограничный кейс и проверять как в этом случае поведет себя код — адская действительность многих разработчиков, которые не используют в своей работе тесты, то есть TDD.
И мне не обязательно в этом случае писать тесты до кода. Если задача маленькая, то тесты просто дописываю в конце, если сложнее — делю на этапы: написал метод/класс, сразу написал на это тест и т.д.poxu
26.10.2016 11:16адская действительность многих разработчиков, которые не используют в своей работе тесты, то есть TDD.
То, что вы пишете тесты не делает разработку управляемой (driven) тестами. Аббревиатура TDD используется чтобы описать совершенно другие аспекты раработки.
renskiy
26.10.2016 11:26управляемой
да, возможно, если подойти с этой стороны, то разработка не управляется тестами в моем случае. Но для чего тогда вообще управлять разработкой? Смахивает на микро-менеджмент какой-то.
И опять же, driven — не означает «управляемый», это скорее «движимый». Так вот, в последнем случае, написание тестов после кода очень даже неплохо «двигает» разработку вперед.
Аббревиатура TDD используется чтобы описать совершенно другие аспекты раработки
Вот поэтому в своем первом комментарии я и высказал мнение, что автору данного понятия не стоило выделять конкретные этапы или аспекты данного подхода. Небольшое отличие — и это уже не TDD, все в точности как со Scrum, его тоже в эталонном виде никто не применяет.poxu
26.10.2016 11:49И опять же, driven — не означает «управляемый», это скорее «движимый».
В случае с TDD это скорее управляемый, чем движимый.
автору данного понятия не стоило выделять конкретные этапы или аспекты данного подхода
В TDD важно, что код пишется в первую очередь такой, чтобы его было удобно
тестировать юнит-тестами. Если убрать этот аспект, то получится не TDD. Просто писать тесты недостаточно.renskiy
26.10.2016 12:15Просто писать тесты недостаточно.
Здесь соглашусь. Но каждый сам выбирает способ достижения результата. TDD — один из таких способов, но и там возможны варианты — именно в этом заключается моя основная мысль. Я не против писания тестов до кода, но это в основном работает только в тех случаях, когда задача тривиальна, либо полностью спроектирована заранее (включая необходимые диаграммы классов).
VolCh
26.10.2016 09:57В случае TDD, когда тесты пишутся перед кодом — тесты являются прежде всего не способом проверки гипотезы, а формализацией самой гипотезы. Грубо, переводом гипотезы с человеческого языка (а то и с мыслеобразов) на язык символьной логики, не допускающий неоднозначных трактовок.
Но вообще, гипотеза не очень хорошая аналогия, по-моему, даже теорема ближе: дано и доказать — тесты, собственно код — доказательство.renskiy
26.10.2016 10:24Если сравнивать с теоремой, то тесты являются доказательством работоспособности кода, а не наоборот, разве нет? Заставлять человека думать иначе — это значит сознательно вводить его в когнитивный диссонанс (все давно привыкли, что доказательством работоспособности любого проекта являются успешно завершенные тестовые испытания).
А сравнение кода с гипотезой мне больше нравится, потому что в большинстве случаев таковой (гипотезой) код и является, до тех пор, пока его не запустишь, либо не проверишь при помощи тестов.VolCh
26.10.2016 14:43Думаю, следует разделять тесты (их код) и прогон тестов. В TDD успешный прогон тестов является доказательством работоспособности кода, но сами тесты доказательством не являются, они являются утверждениями, которые доказывает код, когда выполняется.
poxu
26.10.2016 14:50В TDD успешный прогон тестов является доказательством работоспособности кода
Не совсем так. Неудачный прогон тестов является доказательством, что код не работает.
VolCh
26.10.2016 15:26Не счёл нужным уточнять, что является доказательством работы кода в рамках зафиксированных тестами.
tundrawolf_kiba
26.10.2016 14:52> тесты являются доказательством работоспособности кода
Нет, прохождение тестов — является доказательством соответствия требованиям. А доказательством работоспособности кода — является успешная компиляция. Впрочем ни первое, ни второе не гарантирует того, что может присутствовать ошибка в требованиях или даже в изначальной постановке задачи.poxu
26.10.2016 14:54А доказательством работоспособности кода — является успешная компиляция.
Ну это как-то через край.
Баян даже.
Программистстко — человеческий словарь
Работает — компилируется
Не работает — не компилируется
Классика
wheercool
26.10.2016 14:53Теоремы тут не причем. Если прибегать к метафорам из математики, то тесты в TDD — это аксиомы, или инварианты (кому как удобно оперировать терминами).
Весь смысл TDD как раз таки и заключается в том, чтобы выделить набор утверждений для системы к-ым она должна удовлетворять, а затем уже писать саму систему.
Еще можно провести аналогию с use case. По-факту use case один в один ложится в тест и тест является с одной стороны формализацией требований к программе, а с другой возможностью подтвердить наличия этого свойства у системы (на самом деле только выявить случаи отсутствия этого свойства)
zenkz
25.10.2016 20:35TDD или TLD, как мне кажется одинаково эффективны в умелых руках и одинаково неэффективны в руках неумелых. Намного важнее правильно выбрать архитектуру проекта (в которой должна быть достаточная гибкость и тестируемость).
TDD повышает покрытие тестами, но понижает скорость работы. TLD позволяет покрывать только нужные участки кода и не писать тесты ради тестов. Поэтому тут скорее важно стремиться к тем 85% покрытия тестами. Пусть каждый разработчик решит для себя сам, как ему удобнее. (Или выбрать общий подход в рамках команды).
К примеру у меня не получаются хорошие TDD тесты, но с TLD проблем никогда не возникало. К сожалению научиться писать сразу хорошие тесты очень тяжело и это приходит только с опытом работы. А в некоторых проектах можно вообще обойтись без автоматизированных тестов или написать только интеграционные.Serg046
26.10.2016 13:49По личным ощущениям
тесты ради тестов
куда чаще появляются как раз при TLD, из-за стремления к хорошему покрытию. При TDD такое стремление не будет так явно выражено, а по науке так его и вообще не должно быть (само по себе все хорошо покроется).
SergeyT
25.10.2016 21:02+1А можно я подкину пару ссылок на уже готовые размышления по этой теме: Размышления о TDD, TDD: Test-Driven vs. Type-Driven development, Is TDD Dead, Часть 5.
А вообще, я очень рекомендую ознакомиться с оригинальным трудом Кента и эпичной баталией под названием Is TDD Dead, чтобы увидеть, насколько автор этой практики является здравомыслящим и прагматичным человеком. Который, кстати, неоднократно писал и говорил о том, что этот подход с короткими итерациями тест-код подходит именно к нему и что у других разработчиком может и должно быть другое мнение по этому поводу.
qw1
25.10.2016 22:16Можно комбинировать TDD и TLD. Сначала написать минимальный тест, который нужен, лишь бы настроить окружение и запустить код, а также приблизительно задать API. Затем написать реализацию, а тест использовать для отладки кода. Затем, когда реализация написана, дописать assert-часть теста.
Fedcomp
26.10.2016 07:58Т.е по сути писать код внутри контролируемого теста который тоже пишется на лету. Я так делаю например. У меня динамический язык поэтому я могу еще в любой момент залезть и попробовать код в консоли, если с ним все ок, то засунуть в реализацию/тест.
Terras
25.10.2016 23:44+3Читал книгу про TDD-козла, все время не покидало чувство, что это какое-то садомазо.Т.е. начинаем решать задачу не с задачи, а с его решения — мозг просто отказывается это признавать «логичным» и всячески сопротивляется.
vadim_shb
26.10.2016 01:04Похожие ощущения приходят при изучении много чего нового… Функционального стиля программирования, например… Или наследования в JS… После пары месяцев привыкаешь, понимаешь что к чему и научаешься извлекать пользу и из этого… TDD в своё время было одним из самых тяжелых для усвоения, и одним из самых просвещающих вещей (в отличии от прототипического наследования). Попробовать все-же стоит.
lair
26.10.2016 09:31Вообще-то, наоборот. Тест — это и есть формализованная постановка задачи, а решение — это тот код, который тесту удовлетворит.
VolCh
26.10.2016 10:05Задача — написать код делающий что-то. Тест — формализация этой задачи до такой степени подробности, чтобы даже компьютер мог проверить решена задача или нет. Вспомните школьную математику: задача написана человеческим языком типа «найти площадь прямоугольника со сторонами 5 и 10 см», мы её переписываем формальным типа «Дано: a = 5, b = 10, Найти: S», а в конце учебника ответ «50 см2». Но учителя просто переписанный ответ не устроит, ему нужно решение, общая формула и нам нужно в учебнике найти формулу которая для 5 и 10 даст 50 :)
charypopper
26.10.2016 11:28Здравствуйте. Скажем если взять пример из физики, то решая задачу и получив ответ, мы выполняем проверку размерности, тоесть выполняем проверку, действительно ли то что мы нашли, соответствует ответу, хотя бы по возможности. И мы не можем это сделать, до написания решения, поэтому даже в таких простых задачах, я вижу не только тесты для формализации задачи.
Serg046
26.10.2016 14:04Эм, так все правильно. Сначала пишем тест на ответ — провал. Пишем простую реализацию. И с пониманием того, что реализация не идеальна, а если придерживаться науки (я не ярый сторонник), то она вообще наиболее простая (т.е. вполне возможно, что просто возврат результата), начнут появляться новые тесты, проверяющие дополнительные св-ва системы.
VolCh
26.10.2016 14:46Мы получаем ответ не из решения, а из раздела учебника «ответы на задачи». А когда-таки решим, то сравниваем наш ответ с заранее известным.
Lure_of_Chaos
26.10.2016 01:55+3Я ленив. ОЧЕНЬ ленив. Особенно писать тесты. Особенно в соответствии с TDD: т.е. до того, как напишу код, до того, как пойму, что вообще буду писать. Особенно при том, что в процессе разработки я активно
перемалываюрефакторю код. Инлайн\выделение методов, свертывание и развертывание объектов (в последовательности и более простые структуры), перенос-переименование классов и т.д. согласно ходу мыслей — настолько, что уже через час код может драматически измениться.
И в этой связи написание\изменение тестов может практически остановить этот процесс. А потому я и не пытаюсь следовать заповедям TDD.
И все же я пишу тесты. Это происходит тогда, когда мне необходимо убедиться, что мои предположения в коде верные, а защитные механизмы (типа assert) слишком дороги, чтобы ими присыпать функции, словно снегом. Мне тесты нужны, чтобы или выделить маленькую часть и проверить те детали реализации, которые уже не различает замыленный глаз (выход за пределы массива, переполнение или потеря точности нулевые указатели и т.д.). Т.е. тестом я поднимаю не все приложение, а беру нашпигованный логикой сервис, эмулируя окружение и возможные граничные ситуации, и смотрю, насколько хорошо класс с ними справляется. Я пишу тесты, чтобы ничего не забыть.
Или наоборот, я пишу тесты не на каждый объектно-функциональный чих, а на длинную связку процессов, таким образом повышая вероятность отсутствия (но, конечно же, не полное отсутствие!) ошибок — т.е. если результат сложного процесса корректен, то, скорее всего, корректна также и работа его составляющих.
Иногда я пишу тесты для того, чтобы понять, что и как я буду кодить — что уже ближе к правильному подходу. Иногда проще написать такой тест, который мне покажет, как написать гибкий, расширяемый, а главное — тестируемый класс. Он же мне поможет избежать создания как и чересчур зависимого класса, так и слишком универсального — конечно, для этого тест должен моделировать более-менее правдоподобную ситуацию (или несколько)
Такой подход позволяет уделять меньше времени написанию тестов только ради покрытия, а также меньше потея из-за мелочей, держа в уме только примерный алгоритм, без деталей — а уж детали выплывут при запуске тестов, и останется только их исправить, чтобы все в конце концов работало, а силы ушли на обдумывание решений, а не на реализацию деталей.
Таким образом, на выходе получаем достаточно надежный код, который будет в большинстве случаев работать, при минимуме тестов и утечкой сил на их написание, а также минимум адаптации старых тестов к новому (чит. сырому) коду, а потому легче не только писать тесты, но и адаптировать их, усложнять и лучше проверять основной код.
Это особенно важно, когда нет времени\сил\желания на скрупулезность (ведь должны быть готово уже вчера!), ведь тестирование слилось в процессе с разработкой — ни до, ни после, а именно во время (да и вовремя) разработки, при этом свежая функциональность работает в большинстве очевидных сценариев.
И только когда основная часть выверена, тогда можно заниматься повышением покрытия сверх 80%, описывая никому не нужные тесты, просто ради того, чтобы были, а также проверяли корректность простых ситуаций.arvitaly
26.10.2016 07:33+4TDD не про тестирование, а про разработку. TDD про формализацию требований к каждому участку кода до (или во время) его написания. И это те тесты (спецификации), которые переписываются вместе с кодом, выкидываются точно так же часто, как и переименование функции.
Именно TDD-подход дает понять что вы хотите написать до того, как сядете писать, но:
1. Заставляет делать это формально, а значит сам код уже будет написан гораздо более обдуманно.
2. Заставляет писать код, с заранее заложенными возможностями тестирования.
И то и то, прямое следствие лени и нежелание держать в голове формальные требования и следить вручную за зависимостями, глобальными переменными и т.д. Пусть этим занимается компьютер! Например, использование статической типизации уже шаг к TDD.
И еще раз, TDD предполагает постоянное переписывание самих тестов в процессе написания кода. Однако, если идти сверху вниз, от системных тестов к unit-тестам, то и количество переписанного кода будет минимальным.
Резюме. TDD — это дисциплина разработчика, а не набор инструментов для тестирования.
a-motion
26.10.2016 11:37-3> использование статической типизации уже шаг к TDD
А перхоть ваша статическая типизация еще не лечит?
Слушайте, ну есть у нее свои плюсы (мало и спорные, но есть). Но вот не нужно, пожалуйста, лепить ее в любую щель. Вот я прямо сейчас смотрю на тест, который проверяет, что моя функция корректно обработает _любой_ тип принимаемого параметра. И это, да, TDD. Такой контракт, знаете ли.Fen1kz
26.10.2016 14:08+1Зря минусуете. Как так, я на js пишу приложеньку через TDD (прям вот реально сначала тесты), а вот такого важного шага к TDD не сделал.
Serg046
26.10.2016 14:11А как раз в этом контексте вспомнить это вполне уместно. Такие вещи как статическая типизация и контракты кода помогают снизить кол-во необходимых проверок в тестах.
Почему-то вспомнилась эта статья Сергея — Контракты vs Юнит тесты. Там интересные размышления на эту тему.a-motion
26.10.2016 15:22Помогают. Никто не спорит. «статическая типизация и контракты кода помогают снизить кол-во необходимых проверок в тестах» относится к переходу на TDD примерно так же, как «мерседес более комфортный автомобиль в сравнении с жигулями» к «я решил продать велосипед и купить машину».
Шаг к TDD, например, в 99% случаев может быть осуществлен только в рамках одного языка, потому что смена языка обычно не менее судьбоносное решение в продакшене, и принимать их вместе просто страшно. Я могу в приказном порядке допускать задачу к выполнению только после ревью тестов. А вот заставить людей фигачить на хаскеле или расте — не могу.
Ну это если говорить про реальные use cases, а не про диванных теоретиков со статической типизацией.
VolCh
26.10.2016 15:31и принимать их вместе просто страшно.
С одной стороны, да, страшно. С другой — как раз при написании кода с нуля по TDD она наиболее эффективна, а на новом языке код вы будете писать с нуля, причём уже имея довольно четкое представление о том, что нужно написать (что редкость в целом). И собственно переходу может помочь, выявляя некоторые неочевидные особенности нового языка на стадии написания кода.a-motion
26.10.2016 16:23Большое спасибо за ценный совет. Когда вы будете согласны спонсировать остановку разработки на неопределенное время, потому что «тимлид решил все переписать с нуля», я с удовольствием вернусь у рассмотрению этой блистательной идеи.
Нет, серьезно, если мы обсуждаем мир розовых единорогов — то конечно, я бы сразу на языке, который появится спустя 10 лет после старта проекта начал бы писать.
В реальном же мире микросервисы — да, перевожу на другие языки потихоньку. Но заставить хорошего в целом программиста взять и выучить эрланг, например, я не могу. Или Elm для новой витрины — очень меня всем устраивает. Но наш вполне неплохой в своем деле джсист, увы, не готов грызть гранит. Вы предлагаете разогнать к чертям команду? Или что?
При том, что, повторяю еще раз (я все еще помню, с какого тезиса стартовала эта ветка, да) статическая типизация — совсем не панацея и на бэкенде, например, мне пока представляется скорее злом. Ну как злом, так, не сто?ит выделки. А вот законодательно «сначала тест, потом код» — запросто. Безо всякой статической типизации, насчет которой я могу и ошибаться, но в контексте данной дискуссии это вообще абсолютно нерелевантно.VolCh
27.10.2016 08:02Я скорее о том, что если принято решение перевести сервис (не важно, монолит, SOA или микро) на новый язык, но добавить ещё и TDD с первого коммита не так страшно, как может показаться с первого взгляда.
a-motion
27.10.2016 08:39А, ну это-то безусловно, это настолько очевидно, что даже и упоминания не сто?ит. Только при чем тут это?
Лечить перхоть усекновением головы, конечно, вариант. Но вот фразу «использование статической типизации уже шаг к TDD» — к которой я и прицепился — профессионал с опытом работы написать не может, только диванный теоретик. Потому что «а давайте заиспользуем с сегодняшнего дня статическую типизацию» — это глупость вот прямо со всех сторон. Во-первых, решения о выборе языка принимаются не по принципу приятственной типизации. Во-вторых, покупка яхты — это, конечно, шаг к «научиться плавать», но для большинства людей есть более приемлемые способы. В-третьих, TDD вообще никак не связано с типами; в ЛИСПе, например, тип вообще один, строго говоря. И идите скажите этим бедолагам, что шаг на пути к TDD для них — перейти на Rust.
А так-то, разумеется, при переходе на другой язык форсировать TDD так же просто, как и без перехода на другой язык. Но поскольку мы тут о TDD, я и посчитал предложение статически затипизировать окружающий мир в качестве первого шага — абсурдом.
VolCh
27.10.2016 09:49Ну, как минимум, есть динамические языки (или их расширения) позволяющие внедрять статическую и(или) строгую типизацию (или их подобие типа type hint в PHP) плавно, не с нуля переписывая весь код. И как раз при таком переходе, особенно для традиционно интерпретируемых языков типа PHP и JavaScript, её внедрение с обязательным шагом статического анализа перед, хотя бы, мержем в основную ветку, а лучше до первого реального запуска, близко к внедрению TDD по влиянию на цикл разработки. Не обязательно такая статическая типизация первый шаг, он может быть после внедрения TDD, когда устаешь писать тесты типа «метод должен бросить исключение TypeError если ему передано не целое число» и добиваться их выполнения, но по влиянию на процесс разработки они близки, если не было ни тестов, ни какого-нибудь статического анализа.
Не также просто форсировать, а проще, вернее решение переписать кодовую базу с нуля упрощает форсирование TDD и снижает риски фейла перехода на TDD. Язык вторичен в данном случае, первично решение всё переписать с нуля. Но TDD способно помочь именно переходу на новый язык или, не столь радикально, но новый фреймворк и прочий стэк без смены языка.a-motion
27.10.2016 10:45Да никто не спорит, что type hint в частности и понимание типов вообще хорошо. И статический анализ — безусловное добро (в отличие от строгой типизации, кстати). Не спорит никто.
Именно поэтому во всех динамических языках, рожденных с пониманием того, зачем они нужны, статический анализ и контракты есть или из коробки, или были добавлены на очень ранних стадиях. Вместо тестов типа «метод должен бросить исключение TypeError если ему передано не целое число» люди давно придумали mutation-based тесты (https://github.com/mbj/mutant — это для руби, уверен, что что-то подобное есть и для языков, которые исторически заимствуют инфраструктуру у руби, наподобие PHP и JS).
Но все это вообще не о том. TDD заставляет сначала думать об архитектуре, а потом — о коде. Код с тестами (хоть тысячу раз статически типизированный) приводит к неоптимальным решениям, потому что построить грамотную архитектуру в голове очень сложно (читай: невозможно).
Это тот же эффект, что и в случае резиновой уточки на столе: проговаривая постановку задачи, формализуя ее, мы фактически на 90% воплощаем ее решение. Остается только набросать код. Например, я всегда готовлю красивые презентации по новым архитектурным решениям, и в процессе подготовки последние зачастую претерпевают серьезные изменения. Когда объясняешь тезисы для неподготовленной аудитории — многие казавшиеся очевидными вещи становятся не такими очевидными, а зачастую — и неверными. Тесты — то же самое. Лишний способ заставить самого себя пройти тест (пардон) на продуманность архитектуры.
Язык и его типизация на данном этапе (становления и формализации проблемы) — дело вообще стопиццотое, о чем я с самого начала и твержу.
0xd34df00d
28.10.2016 04:30+1А что эта функция делает, если не секрет?
a-motion
28.10.2016 11:03Не секрет, конечно. Это DSL поддержки pattern-matching’а в языке, который его из коробки не поддерживает.
0xd34df00d
30.10.2016 23:01Ну, это общее описание. А что функция возвращает, какой алгоритм реализует?
DistortNeo
26.10.2016 19:57+2Или наоборот, я пишу тесты не на каждый объектно-функциональный чих, а на длинную связку процессов, таким образом повышая вероятность отсутствия (но, конечно же, не полное отсутствие!) ошибок — т.е. если результат сложного процесса корректен, то, скорее всего, корректна также и работа его составляющих.
А теперь представьте, что проектом стал заниматься другой человек, и у него перестал проходить один из подобных высокоуровневых тестов. Поиск ошибки в этом случае будет значительно затруднён.
TheShock
26.10.2016 02:23+3Многим людям лень писать тесты когда код уже написан и вроде работает, хочется поскорее приступить к другим задачам, да и начальнику сложно обьяснить, почему тебе нужно еще Н времени на тесты, когда уже все написано, думаю, это важные причины, почему в методологии тесты пишутся до кода — чтобы они наверняка были написаны. Хотя лично я обычно покрываю тестами код уже когда он есть.
poxu
26.10.2016 09:46Научное исследование из начала статьи проводилось на студентах, то есть на людях, без особого опыта разработки и которые, не факт, что вообще владеют TDD. Им TDD только мешать будет.
Aingis
26.10.2016 13:38Всё логично, если где-то будет внедряться TDD, то там, где люди скорее всего не владеют TDD. Вот и возникает вопрос: даёт ли TDD какую-либо выгоду, чтобы оправдать затраты на внедрение? Именно TDD, то что тесты дают выгоду, уже давно выяснили.
poxu
26.10.2016 14:28Если мы смотрим выгоду на период внедрения новой методики, то её не будет — будут только затраты. Выгода появится только когда народ методикой овладеет.
Но, судя по исследованию — у людей, которые методикой не вполне владеют — этих затрат нет. Так что выгода может быть и существенной.
JediPhilosopher
26.10.2016 16:06Где-то встречал изречение что TDD — это как оливки, в детстве их все терпеть не могут, а вот взрослея начинают любить.
TDD хорошо заходит у тех, кто уже походил по граблям плохо оттестированных проектов и попытался делать тестовое покрытие для уже написанного тесно связанного legacy-кода. Тогда приходит понимание и приятие данной технологии, не формальное, а идущее так сказать от сердца.
А у студентов опыта нет или мало. Они и тесты скорее всего писать не умеют как надо и делают это чисто формально (сколько раз я такое видел — тестами покрыты совершенно ненужные тривиальные вещи типа геттеров-сеттеров, зато не покрыты важные сценарии использования или краевые условия).
Думаю если бы взяли команду опытных разработчиков результат вполне мог бы выйти иным.Aingis
26.10.2016 16:47Опыт сам по себе позволяет писать хороший код. А вот добавляет ли к этому что-нибудь TDD — вопрос.
JediPhilosopher
26.10.2016 17:07+1Мой личный опыт показывает что да.
У меня сейчас есть несколько небольших проектов в виде вебсервисов, которые обрабатывают различные запросы, перенаправляют их на другие апи и пишут логи. Объем работ там примерно одинаковый во всех, при этом я на них испробовал все три варианта: самые старые вообще без тестов, те что поновее — я покрывал тестами после написания, самые новые сделаны по TDD. Схожие задачи, один и тот же разработчик, разные подходы. Могу сказать что последние — наиболее стабильные и изменения в них вносить проще и безопаснее всего.Serg046
26.10.2016 17:29А промежуток времени? Быть может качество пришло с полученным опытом?
JediPhilosopher
26.10.2016 17:31Год-полтора где-то. Не так много на фоне моего общего опыта (около 8 лет).
Aingis
26.10.2016 18:12«Если вы смотрите на свой код годовой давности и вам не стыдно, то что-то не так». Полтора года — большой промежуток.
Serg046
26.10.2016 17:29А вот тут снова вопрос про то, что называть хорошим кодом. Мне, например, нравится ответ, про тот, который хорошо тестируется.
Qilicad
26.10.2016 11:29Пишу тесты только на сложную логику, вычисления, расчёты. В какой момент — зависит от ситуации. Иногда сразу видно, какой будет функция и можно сначала написать несколько тестов, написать функцию, дописать тестов ещё. А иногда, вот так понапишешь тестов, потом начнёшь писать функцию и поймёшь, что нужно передавать ещё вот этот и этот параметр, и вообще функцию лучше разделить на части, и все тесты становятся полностью непригодными. Тогда лучше сначала написать функцию и уже после — тесты. Естественно, держа в уме, что эту функцию придётся тестировать.
kulikovDenis
26.10.2016 11:29Пока пишешь код понимаешь, что вообще нужно было сделать, и как нужно было, но переписывать уже нет ни возможности ни желания.
Поэтому этапа осмысления, формирований требований и архитектуры замечательно выносится в предварительное написание тестов.
В проектах с большой текучестью, это осмысление постфактум может плохо кончится для проекта.
Соответственно задачи с ГУИ выпадают из ТДД, ГУИ сложно поддаются покрытию, в любом случае, и различные системные сервисы, которые замечательно покрываются.
numitus2
26.10.2016 12:24Использую TDD только когда пишу код с нетривиальной логикой, и сложными функциями, где можно легко допустить ошибку в реализации. Вижу рядом много дебилизма, когда пишутся тесты к функции из одной строчки которая вызывает внешний сервис который к тому же замокан(Mocked). Считаю что это перебор.
el777
28.10.2016 11:26На текущий момент — да. Но в будущем, если проект будет развиваться, то могут быть самые неожиданные изменения, кто-то решит, что сюда надо передавать все по-другому. Сам внешний сервис изменит API, и придется сюда лезть и фиксить. Если оно все плотно не покрыто тестами, то в другом месте может оторваться.
Fesor
28.10.2016 11:52вы всковырнули довольно жирную проблему. Люди слишком много мокают и мокают то что не нужно. Мокать нужно только то, что общается с внешним миром (по отношению к тестируемому модулю). И это отнюдь не все.
Например сущности ORM-ок любят мокать. А ведь это тупо данные. Мы таким образом создаем лишнюю зависимость тестов от реализации.
Fesor
26.10.2016 17:41Применение TLD позволяет существенно сократить объем кода по сравнению с TDD.
За счет чего простите? За счет ненаписанных тест кейсов? При TDD "лишние тесты" тоже стоит удалять. Да и есть еще много разных способов уменьшить количество кода для тест кейсов. Например — property based testing. Спасает как дополнение к триангуляции тестов.
renskiy
Пришлось искать информацию о том, что такое TLD во внешних источниках. Догадаться конечно можно, что это противоположность TDD, но из названия это не очень очевидно.