image
Simon Stalenhag?—?Tyrannosaurus (http://www.simonstalenhag.se)
“Будьте осторожны с использованием следующего кода?—?я лишь доказал, что он работает, но я не тестировал его” Дональд Кнут
Техника “Сначала Тест” (Test-First Design, далее TSD) появилась вместе с экстремальным программированием (Extreme Programming, далее XP, кстати, эта абревиатура никак не связана с Windows) и является одним из основных подходов этой методологии. Впервые книжное упоминание этой техники было в Extreme Programming Explained 1999 K.Beck

image

В основе техники лежит простая идея?—?перед тем как сделать что-то, напишите на это что-то тест.
“Никогда не пишите код без отказного теста.” Кент Бек

Позднее, техника TFD эволюционировала в технику “Разработки через тестирование” ( Test-Driven Development далее TDD), которая заняла ее место в списке подходов XP.

  1. Разработка ведется короткими циклами.
  2. Реализации функции предшествует отказной тест (красно-зеленый рефакторинг).
  3. Отказной тест только один.
  4. Реализация функции должна быть максимально простой и достаточной для прохождения теста (тесно связано с принципами KISS и YAGNI).
  5. После прохождения тестов начинаем безжалостный рефакторинг (на этом этапе мы избавляемся от дублей, тесно связано с принципом DRY)


image

Я не буду тут пересказывать прекрасный труд Кента Бека, тем кто хочет подробнее разобраться в подходе TDD прямая дорога в магазин за этой книгой (Test Driven Development: By Example 2002 K.Beck):

image

TDD не работает?


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

Самые прогрессивные деятели мира разработки программного обеспечения признавали и признают до сих пор, что TDD это очень действенный инструмент в арсенале программиста. Это не “серебряная пуля”, но время доказало, что это работает. Мое скромное мнение, что разработка через тестирование?—?это гениальная техника.

Однако, ситуация беларуского (или какого-нибудь другого похожего) аутсорса по этой части печальна. У нас никогда не было TDD. Никто никогда не воспевал ему оды. Наоборот, я постоянно слышал другое.

В 2010 году я поступил в штат сотрудников отдела QA компании *instinctools. Когда я начал глубже погружаться в автоматизацию, я начал задавать вопросы: “Почему наши разработчики не пишут тесты?” Ответ был, наверное, очевидный?—?заказчик не платит за это деньги.

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

Это дорого. Это не купит заказчик. У нас нет на это времени, мы знаем, что это хорошо, но у нас слишком жесткие сроки. Подобную функциональность не получится тестировать этим подходом. Это работает только для модульных тестов. Типичные ответы на вопрос: “Почему не TDD?”.

И при этом, все всеравно пишут тесты, но только делают это в стиле Test-Last.

У нас нет на это времени


Вставая на путь Test-Last разработчик теряет кучу бенефитов TDD.

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

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

Поэтому на тестирование инкапсулированных механизмов вы, скорее всего, махнете рукой.

image

В противовес этой технике, можно начать использовать TDD. Благодаря TFD и сквозному тестированию, покрытие будет стремиться к 100%. Разрабатывая функцию, вы будете проходить два этапа: проектирование интерфейса и реализацию. На стадии написания теста вам нужно понять, каким будет интерфейс вашей функции, что она будет получать на вход и что она будет возвращать. Каким будет формат входных и выходных данных. Спроектировав интерфейс и написав отказной тест, вы напишите простейшую реализацию, ограничения которой уже заданы в тесте. Это путь к простым и читаемым решениям. Вам останется только описать функцию в документации проекта.

image

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

Убирайте время на отладку и запуск приложения для проверки. Отладка и проверка реализованной функции делается через тесты. К слову, это самый быстрый способ.

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

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

Со временем и каждодневной практикой диаграмма стоимости примет следующий вид:

image

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

Стрелять из лука дело совсем не простое. Если его дать вам в руки, едва ли вы тут же попадете в яблочко. Нужны тренировки. Я помню, как я впервые попробовал стрельнуть, стрела улетела куда-то совсем мимо. Инструктор боялся, чтобы я не завалил какого-нибудь рыбака.

Ездить на машине после первого занятия в автошколе тоже вряд ли получится. Тоже самое и с TDD. Из черепашьих гонок, со временем, через тренировки, можно попасть на Формулу-1.

Это работает только для модульных тестов


Integration / System Tests: I/O-First


Хайп 2014-го вокруг “TDD is Dead” привлекателен. Я допускаю, что должны быть компромиссы. Эмулировать видеопоток в тестах крайне сложно (особенно, когда еще нет реализации функции, обрабатывающей этот поток). Или, например, соблюдать подход при тестировании безопасности (security testing). Информационные технологии стремительно развиваются. Дополненная реальность, блокчейн, компьютерное зрение?—?перед автоматизацией стоит ряд вызовов. Но я вас умоляю, если вы реализовываете простой сервер с CRUD-операциями, то написать интеграционные тесты в стиле TDD не составит труда. Вы знаете какой HTTP-глагол нужен той или иной операции, вы знаете какой статус должен быть в ответе. Продумайте формат HTTP-ответа. И отказной тест с 404 ошибкой готов. А теперь приступайте к реализации конечной точки. Не надо отговорок и усложнений на пустом месте. В случае системных тестов важно определить входные и выходные данные, это самое сложное, далее процесс ничем не отличается от модульных тестов. На тему, есть серия видео встреч от авторов подхода с оппозицией в лице DHH.

E2E: PageObject-First


Приемочные критерии (acceptance criteria) чаще всего принимают форму функциональных или end-to-end тестов. И в этой части, практически все разработчики не понимают, как можно написать тест на страницу, которой еще нет. Перед глазами сразу тонны HTML-разметки и CSS-верстки. Это частая ошибка новичков. Важны функции страницы, элементы управления, контент, а не разметка и верстка. Подобно тому, как дизайнер создает макет страницы, разработчик должен описать страницу шаблоном PageObject на основе дизайна или зарисовки. Это стадия настоящего проектирования. Вы декомпозируете страницу, разбивая ее на элементы и описывая их методами класса. Я бы назвал эту технику PageObject-First, и помните, тест является отказным, нам не нужно знать на этой стадии конкретные способы, с помощью которых мы обнаружим элементы управления, они реализуются позже. В этой части по прежнему можно разрабатывать в стиле TDD. Есть даже устоявшееся название подобной практике: “Разработка основанная на приемочных тестах (Acceptance Test–Driven Development / ATDD)”. Есть и другие вариации (я думаю, что такие аббревиатуры можно клепать каждый день, суть одна):

  • “Разработка основанная на тестах заказчика (Customer Test–Driven Development / CDD)”
  • “Разработка основанная на тестах пользовательских историй (Story Test–Driven Development / SDD)”

Это дорого


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

image

Как еще можно сэкономить? Давайте урежем время на документирование, но ценой чего? А почему бы не сделать тесты документацией?

Новое (на самом деле не такое уж и новое) расширение подхода TDD решает эту проблему. Вобрав в себя лучшие принципы Domain-Driven Design Эрика Эванса, TDD эволюционировал в “Разработку основанную на поведении или спецификации (Behavior-Driven Development / BDD или Specification-Driven Development / SpecDD)“

Дэн Норс впервые описал этот подход в 2006 году, презентовав его позже на конференции 2009-го года в Лондоне. Он же создал и один из первых фреймворков на основе этого подхода. Идея в названии. А давайте превратим наши тесты в спецификацию. Т.е. теперь, когда мы пишем отказной тест, мы делаем это через DSL (Предметно-ориентированный язык / Domain-Specific Language), попутно описывая поведение разрабатываемой функции на языке доступном всем.

image

image

Начало этой прекрасной идее было положено еще аж в 1987 году в языке Perl в виде Test Anything Protocol (TAP). Одна из первых реализаций человеко-читаемых результатов выполнения тестов, по сути, отчетов.

image

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

image

Test-Last



image

Так а что же Test-Last? Это история поросшая мхом? Ненужный мусор, который надо выбросить?

Я так не считаю. Для меня это неотъемлемая часть TDD. Звучит странно, не правда ли? Да, именно так. Я считаю Test-Last нулевым шагом в TDD. Именно с этой техники я советую всем начинать. Ведь все познается в сравнении. Только те, кто работал в этом стиле, а позже перешел на TDD, могут осознать всю ценность идей Кента Бека. Юному джедаю нужно побывать на обеих сторонах. Если вы сейчас работаете в стиле Test-Last, то продолжайте. Я серьезно. Старайтесь добиться как можно более полного покрытия, изолируйте тесты, пишите заглушки и пытайтесь дотянуться до самых спрятанных функций. При оценке и планировании, прикидывайте на глаз, сколько займет времени реализовать тесты.

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

***


Если у вас есть какие-то замечания или дополнения, я буду рад их увидеть в комментариях или пишите на artur.basak.devingrodno@gmail.com

Комментарии (37)


  1. fishca
    04.09.2017 08:32
    +1

    Кратко, но понятно, спасибо.


  1. DjPhoeniX
    04.09.2017 08:57
    +3

    К сожалению, во многих компаниях практикуется «DDD» (Deadline-Driven Development). Это когда «какие на… тесты, релиз должен был быть позавчера». А после релиза — новый пинок от продаванов «а давайте внедрим фичу XXX». Счастье, если на рефакторинг вообще время выделяют.


    1. hlogeon
      04.09.2017 22:57
      +1

      Ваша задача, как разработчика донести до руководства, что с таким подходом ОНИ теряют деньги. А любой бизнес хочет не терять, а зарабатывать. Начните диалог ;)


  1. Bonart
    04.09.2017 08:58
    +2

    Фишка совсем не в том, первым ли пишется тест, а исключительно в уменьшении размера итерации. Этакий repl для кровавого энтерпрайза.
    Еще нюанс — хороший дизайн от tdd сам по себе не появляется.
    Огурцы — это совсем не "понятный всем язык", а всего лишь формальный dsl, который притворяется естественным. Но забыть про его формальность не выйдет. Оверхед от его использования вместо основного языка программирования огромный, а толку чуть. Лучше основной язык снабдить удобными библиотеками.


    1. archik Автор
      04.09.2017 17:20

      Про огурцы согласен :) DHH на сессии с Фаулером и Бекома так и сказал, что для тех, кто это пользует, отведено отдельное место в аду :))

      Я тоже за dsl-конструкции в основном языке, сам, как JavaScript разработчик, пользуюсь библиотеками mocha и jasmine (и в модульных и в e2e тестах).

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


  1. FractalizeR
    04.09.2017 13:05

    Но это ни в коем случае не компсогнат, это, скорее, апатозавр

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


    1. archik Автор
      04.09.2017 13:26

      Все верно, жили в одно время, но «вес» имели разный. Разница в размерах


      1. FractalizeR
        04.09.2017 13:33
        +1

        Теперь понятно :) Спасибо.


  1. Nakosika
    05.09.2017 01:01

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


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


    1. t-nick
      06.09.2017 23:01
      +1

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


      1. Nakosika
        07.09.2017 19:12
        +1

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


  1. ush-alex
    05.09.2017 12:59

    Что делать когда сотни так образом написанных тестов? Как ими управлять? Где можно про это почитать?


  1. andreyverbin
    05.09.2017 12:59
    +1

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

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

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

    В результате:
    Было — unit тесты по объему были примерно 120% от продакшен кода, покрытие от силы 70%, баги тестами ловятся только по большим праздникам, зато unit тесты ломаются каждый час.
    Стало — интеграционные тесты примерно 20% от продакшен кода, покрытие более 80%, 95% поломок тестов означает реальный баг который надо фиксить.


    1. oxidmod
      05.09.2017 13:02
      +1

      Может вы не умеете в юнит тесты? ОО


      1. andreyverbin
        05.09.2017 22:50

        Допускаю, что так оно и есть, но скорее я просто имею дело с проектами, где юниты не очень то и нужны. Примеры такие:
        — Плагин для аутлука, большая часть кода работает с API аутлука, основные проблемы связаны с глюками аутлука.
        — Что-то похожее на IDE, большая часть кода это тупо GUI и настройка готовых контролов.
        — Соц. сеть, большая часть кода про запросы в базу, картинки и всякие интеграции.

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

        Характерны такие сценарии, небольшой рефакторинг ломает 10% тестов (поменялся внутренний интерфейс). А системой можно пользоваться, если показать ее заказчику, то он даже и не заметит, что по нашим данным 10% системы не работает. Вот какой смысл в таких тестах?


        1. oxidmod
          05.09.2017 23:00

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


          1. andreyverbin
            05.09.2017 23:24

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


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

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

            Положим, мы делаем сам алгоритм, а не процедуру извлечения его результатов из базы. Тут другая история, но тоже не про юнит тесты. Я предположу, что там что-то хитрое из арсенала современного machine learning. Весь современный ML он про линейные пространства и всякие дифференцируемые функции. Предполагаю, что в конечном итоге юзер и кандидаты в этот список превращаются в вектор, с помощью одного из методов ML. Так вот, этот «превращатель» буквально «выращивается» на тестовых данных. Опять же, юнит-тестить там нечего. Там надо выборки готовить, результирующую статистику собирать, графики уменьшения ошибки смотреть и т.д.


            1. lair
              05.09.2017 23:28

              Опять же, юнит-тестить там нечего.

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


              1. andreyverbin
                05.09.2017 23:38

                Затем в алгоритме поменялась размерность входных данных (было 200 элементов в векторе, стало 300), но ваши юниты об этом не знают, потому что все замокали.

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

                обычно не так, чаще будет как-то так
                1. Выбрали вектор юзера из базы
                2. Сделали выборку из таблицы друзей с помощью индекса на основе R-дерева или аналога. Получили список друзей.
                То есть прикладной программист этот вектор никак не интерпретирует.

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


                1. lair
                  05.09.2017 23:44

                  Затем в алгоритме поменялась размерность входных данных (было 200 элементов в векторе, стало 300), но ваши юниты об этом не знают, потому что все замокали.

                  Йеп-йеп, пирамида тестов потому и пирамида, а не один слой.


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


                  Выбрали вектор юзера из базы [...] прикладной программист этот вектор никак не интерпретирует.

                  А в БД вектор откуда взялся-то?


                  Можно замокать нашу базу, спору нет, но затем переименуют колонку,

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


        1. lair
          05.09.2017 23:20

          Допускаю, что так оно и есть, но скорее я просто имею дело с проектами, где юниты не очень то и нужны.

          Так зачем из этого делать вывод, что они нигде не нужны?


    1. lair
      05.09.2017 14:00

      Тестировать можно только реальную систему, даже если это значит, что тесты идут час вместо секунды. Час интеграционных тестов, это недели сэкономленные тестировщиками на регресс тестах

      Это хорошо звучит, когда у вас тесты идут час (хотя даже в этом случае вы этот час воруете у разработчика). А вот когда они идут день, начинается реальное торможение.


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

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


      1. Neikist
        05.09.2017 15:01

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

        Подкину к этому еще вариант: реальное API еще не реализовано.


      1. andreyverbin
        05.09.2017 23:02

        Это хорошо звучит, когда у вас тесты идут час (хотя даже в этом случае вы этот час воруете у разработчика). А вот когда они идут день, начинается реальное торможение.

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

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


        :) ясное дело, что тут надо будет стаб делать, голову никто не отменял.

        Если хотите, я не против юнитов, я против стабов и моков. Только без них юниты быстро превращаются в интеграционные тесты, как-то так.


        1. lair
          05.09.2017 23:19

          Отвалившийся тест почти всегда означает проблему с моками и стабами.

          Странно, а у меня отвалившийся тест обычно означает проблему с SUT.


          Как писать быстрые и изолированные тесты без стабов в типичном бизнес приложении не понятно.

          Так не надо писать без стабов, разве кто-то заставляет?


          Поменяли кусок системы, запустили тесты на этот кусок, зачем ждать день?

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


          1. andreyverbin
            05.09.2017 23:30

            Так не надо писать без стабов, разве кто-то заставляет?


            Поясните. Типичное веб приложение.
            1. Контроллер, который дергает сервис.
            2. Сервис, которые дергает репозиторий.
            3. Репозиторий, который дергает базу.

            Какая-то логика есть в сервисе, но она на 90% завязана на выборки из базы или из кеша. Что тут можно протестировать без стабов и как?

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


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


            1. lair
              05.09.2017 23:33

              Какая-то логика есть в сервисе, но она на 90% завязана на выборки из базы или из кеша. Что тут можно протестировать без стабов и как?

              Я вроде бы сказал, что без стабов тестировать не надо.


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

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


    1. azShoo
      05.09.2017 15:10
      +2

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

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


      1. andreyverbin
        05.09.2017 22:26

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

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

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

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


        1. lair
          05.09.2017 23:24
          +1

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

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


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

          Вообще-то, в этом тесте проверяются не моки, а то, что SUT ожидаемым образом взаимодействует с окружением.


          Юнит тест завязан на внутренние API, которое как раз при рефакторинге меняется чаще чем внешее.

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


        1. archik Автор
          06.09.2017 01:16

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

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

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

          Если же я ошибаюсь, и вы работали в TDD стиле, то, возможно, ваша проблема в ненужных тестах. В видео-сессиях «TDD is dead» Кент Бек высказал мысль: ненужные тесты нужно удалять (он это называет delta coverage – дополнительное покрытие, обеспечиваемое однотипными тестами, и если это покрытие равно 0, то такой тест можно смело удалять)
          т.е. тестируя функцию деления:

          test1: 6 / 2 = 3
          test2: 4 / 2 = 2


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


    1. lxsmkv
      07.09.2017 03:44

      Я думаю писать юнит-тесты стоит когда только разрабатываешь класс. Чтобы правильно его смоделировать. Эти тесты они как сертификат «в сценариях определенных тестами этот класс ведет себя надлежащим образом» или, говоря иначе «в сценариях не определенных тестами, этот класс может вести себя неизвестным образом».
      Был у меня случай нужно было написать «трансформатор» данных, я знал только business-rule
      и имел входные данные в сложносвязном xml. (комбинация из нескольких значений подается на вход, и одно из них на выходе должно быть доминантным) Так вот чтобы быть уверенным что моя конструкция для всех сценариев правильно работает, я писал на каждое правило несколько юнит-тестов. Не потому что TDD, а потому, что я сам ни черта не понимал эти правила. Я и по сей день не понимаю их до конца, но код работает корректно. Все довольны.
      И когда выйдет новая версия входных данных, можно с помощью этих тестов убедиться, что рассчет работает на новых данных (или не работает :) Но гонять их каждый день на дженкинсе было бы сущей бессмыслицей.


      1. lair
        07.09.2017 11:02

        Но гонять их каждый день на дженкинсе было бы сущей бессмыслицей.

        Почему?


        1. lxsmkv
          07.09.2017 15:25

          Потому что ничего не изменилось, и тесты будут зелеными.
          Другой пример для пояснения, что я имею ввиду:
          зайдите на hmlt5test.com, сделайте тест браузера на поддержку веб стандартов. Имеет смысл тестировать тот же самый браузер несколько раз подряд? Очевидно нет, потому что ничего не изменилось в окружении (как то браузер или операционная система) и результат будет тем же самым что и при первом прогоне.


          1. lair
            07.09.2017 15:27

            Потому что ничего не изменилось, и тесты будут зелеными.

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


  1. safinaskar
    06.09.2017 02:29

    Раз уж я наткнулся на пост на эту тему, можно я вставлю сюда фрагмент из секретной документации с моего прошлого места работы, который я героически стыбзил? Файл был озаглавлен "Текст, прочитав который все сразу начнут писать хорошие программы"


    "Разработка через тестирование" крайне нежелательна. Термин означает отсутствие понимания как работает код, изменение пары строк здесь и там в надежде получить нужный результат. Как только код отрабатывает правильно один раз, задача сразу же считается выполненной. В наших условиях такой способ разработки означает, что многопоточность не учтена, вырожденные условия не учтены, исключения не учтены, сетевое взаимодействие не бралось в расчет, а сам алгоритм работает с ошибками.
    Обратите внимание, что это не противоречит требованию писать сначала тесты, а потом рабочую логику. Разница методологическая: тест нужен не для того чтобы контролировать выполненость задачи, а для того чтобы контролировать, что ваши изменения не сломали старое поведение. Ваш тест (который вы пишете вместе с кодом) нужен НЕ ДЛЯ КОНТРОЛЯ ВЫПОЛНЕННОСТИ ВАШЕЙ ЗАДАЧИ, а для того, чтобы следующий разработчик не смог сломать ваше запрограммированое поведение и не заметить этого.

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


    1. lair
      06.09.2017 11:52

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

      Я так понимаю, это авторское понимание термина?