Как не выстрелить себе в ногу

    Без использования unit-тестов и TDD очень легко выстрелить себе в ногу. С тестами и TDD сделать это намного сложнее, но если у вас получится, вы останетесь без ноги.

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

    В этой статье я постараюсь объяснить о чем, собственно, разговор. Для чего нужно TDD и как его аккуратно использовать.

    Что такое TDD в двух словах? — это написание разработчиком тестов до реализации функциональности.
    По совету Роя Ошерова разобьем вопрос применимости TDD на два:

    • Зачем писать тесты?
    • Зачем писать тесты до реализации?


    Вы все равно тестируете



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

    Далеко не всем это нужно



    На наших проектах разработчиков, которые не пишут тесты, мы зовем ЧакНоррисами, так как они неимоверно круты и мы им искренне завидуем.

    Почему Чак не пишет тесты?


    Его код идеален

    В коде Чака Норриса никогда не бывает багов. Он никогда не ошибается. Если вы нашли баг в коде Чака — попробуйте поговорить с ним об этом — сразу поймете, что это на самом деле необходимая фича проекта.

    Ему не нужна документация кода и коммуникация на проекте

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

    Идеальная память

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

    Рефакторинг без проблем

    Чак вообще не делает рефакторинг, ведь его код сразу идеален. Так что ситуаций, когда нужно что-то изменить или оптимизировать и постараться ничего не сломать в процессе, у него просто не бывает.

    Почему вы не пишите тестов?


    Не понимаю что это такое и зачем

    Странная отговорка, особенно после того, как вы прочитали почему Чак Норрис не пишет тесты.

    Нет времени

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

    Это невозможно протестировать

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

    Это не моя работа

    У нас есть тестировщики — пусть тестируют. Чем меньше багов вы оставите тестерам, тем меньше вам будет с ними работы в будущем. Работайте эффективнее — работодатели это ценят.

    Мне и так хорошо

    Поздравляю, вы — Чак Норрис.

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

    Так зачем их писать до реализации?




    В этой картинке и фразе Кента Бека "Clean Code That Works" вся суть TDD. Мы хотим, чтобы наш код работал, и хотим, чтобы он был чистым. Вот и все. А с помощью шагов Red->Green->Refactoring, как показывает практика, добиться этого проще.
    На этапе Red мы пишем тест, оцениваем дизайн нашей функции, тут же его исправляем. Убеждаемся, что функционал не реализован и тест не проходит.
    На этапе Green мы достигаем цели — «Code That Works» — наша реализация начинает работать.
    На последнем этапе цикла Refactoring мы реализуем цель — «Clean Code» и он все еще «That Works»… и идем дальше.

    Главная мысль TDD — это вовсе не тесты, это вопрос дизайна. Когда вы пишите тест, вы спрашиваете самого себя, что вы хотите сделать и как это лучше реализовать. Тут же вы свой дизайн кода тестируете, попутно соблюдая принципы Keep It Simple Stupid и You Ain’t Gonna Need It.

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

    TDD – это не серебряная пуля


    К сожалению. Серебряных пуль вообще нет, как написал в своей книге Фредерик Брукс. И TDD не исключение. Вы все еще можете ошибаться, вы можете ошибаться в тестах. Unit-тестами все не ограничится, возможно, вам понадобятся интеграционные и другие виды тестов. TDD точно не хватит, чтобы покрыть весь ваш код. И самое главное — вам нужно будет все еще думать:) И не расслабляться.

    Как начать использовать TDD?


    … описано в статье TDD — это как сноубординг. Вкратце — нужно быть аккуратным. Нужно осознавать что ты делаешь и зачем. И лучший вариант — учиться у того, кто уже использует этот подход. Если такой возможности нет — книги, скринкасты, статьи + письма тем, кто пишет книги, статьи и записывает скринкасты, если что-то непонятно.


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

    Риск остаться без ноги


    Неподготовленный старт

    Если не разобраться в сути вопроса и кинуться с головой в TDD, вы рискуете выкинуть зря кучу своего времени. И не всякое руководство это потерпит. Для начала нужно научиться писать тесты. А потом уже переходить к TDD. Плавно, без резких движений. Мозг сам переключится в определенный момент.

    TDD ради TDD

    TDD — это клево, все о нем пишут, еще больше говорят, давайте и мы будем. В корне неправильно. Очень близко к неподготовленному старту — поймите как это работает, потом уже начинайте.

    Выход за границы применимости

    С помощью TDD нужно тестировать логику проекта. Для всего остального существуют другие виды тестов. Да и не всякая логика поддается такому подходу — отличный пример описывается в статье Почему юнит-тесты не работают в научных приложениях

    Фанатизм

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

    Что почитать для начала


    Test Driven Development: By Example

    Первой книгой должна стать книга Кента Бека — в ней все основы TDD от автора этого подхода. Обязательна к прочтению.

    The Art of Unit Testing: With Examples in .Net

    Книга, которая поможет освоить первый навык — написание тестов. О TDD там буквально пара слов. Примеры на С#, но это не принципиально — полезна будет всем. Хорошую рецензию на книгу можно почитать тут.

    Refactoring: Improving the Design of Existing Code

    Классическая книга Мартина Фаулерао рефакторинге, которая поможет вам сделать ваш код чистым.

    Refactoring To Patterns

    Еще одна книга о рефакторинге. В ней Джошуа Кириевски показывает связь рефакторинга с design patterns. Рекомендую читать сразу после Фаулера.

    Working Effectively with Legacy Code

    Книга Майкла Физерса, в которой подробнейшим образом рассказывается как правильно работать с унаследованным кодом. Интересно, как автор определяет само понятие «унаследованный код» — это код, который не покрыт тестами:) Будет очень полезна для тех, кто работает с подобным кодом.

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

    Статья подготовлена по мотивам моего недавнего доклада на Конференции .NET разработчиков.
    Поделиться публикацией
    Комментарии 38
      +8
      мне понравилось про Чака. И походу я чак ((
        +9
        Раньше я тоже считал себя Чаком, ведь я так всё аккуратно пишу и всегда хорошо тестирую. Но я шокнулся когда написал тесты для своего «идеального» кода. В очередной раз всплыла мысль: б**, как же это вообще работало?
        0
        Думаю, на вопрос «Почему вы не пишете тесты?» чаще всего ответ «лень».
          +4
          Забавно, на самом деле, народу ведь и правда не лень сотни раз перезагружать веб-страницы/создавать временные переменные и логи/копаться в дебаггере и рефлекторе, но вот тесты писать — лень и все тут.
          +2
          «TDD — это клево, все о нем пишут, еще больше говорят, давайте и мы будем. В корне неправильно. Очень близко к неподготовленному старту — поймите как это работает, потом уже начинайте.»

          Голден, такскзть, вордс!
            +2
            Тенденция радует, побольше статей про TDD, Rspec etc, хороших и разных!
              +2
              Я вот смотрю на TDD и радуюсь всяко.
              И хочу применять.
              Проблема в том, что я джаваскриптер. Мои функции — это, в 99% случаев, обработчики событий. Как их покрыть тестами, я не представляю, т.к. тестовая среда сводится к, зачастую, немалому объему HTML-верстки. И проблемы чаще возникают именно там, при взаимодействии с DOM-деревом.

              Как быть?
                0
                  0
                  Фреймворк найти не проблема. Проблема обеспечить тестовую среду.

                  Я еще посмотрю повнимательней, но, кажется, это мало чем отличается от полудесятка других тестировочных фреймворков.
                  0
                  Хороший вопрос, присоединяюсь. Кто пробовал для js писать тесты?
                    +1
                    вот тут говорят, что пишут тесты для js — habrahabr.ru/blogs/tdd/116456/#comment_3778416 надеюсь, расскажут подробнее. тоже интересно как это выглядит на практике, на каких задачах помогает. лично никогда не сталкивался с необходимостью тестирование javascript.
                      0
                      Я сталкивался. Т.е. было достаточно сложное приложение, которое хотелось покрыть тестами (т.к. оно постоянно рефачилось). Но я не знал, как к этому подступиться.
                      0
                      Мы пишем JS тесты, но мы чётко отделяем данные-логику-представление
                    0
                    Вам некогда будет стрелять себе в ногу — вы будете писать тесты!

                    Не знаю, возможно, в идеальных условиях тесты и впрямь хороши, но в действительно сложных приложениях с большим числом зависимостей и assert-то всунуть бывает некуда…

                    Я о том, что порой создать для теста объект, идентичный натуральному, практически невозможно. А если и возможно, то не факт что с другим объектом всё будет так же.
                    • НЛО прилетело и опубликовало эту надпись здесь
                      +8
                      Тесты это здорово, но не всегда удается их нависать.
                      В книгах по ТДД зачастую описываются сферический код в ваукуме типа приема заказов или справочника сотрудников. Не спорю писать тесты под это легко и просто но к суровой реальности это относится не часто. Особенно если для выполнения тестов нужно воссоздать сложное окружение из кучи сторонних компонентов. Тут я просветления не достиг и тестирую только части, не нуждающиеся в сложном окружении. Это кстати влияет и на сам код — приходится некоторые методы разделять чтобы написать тест под них.

                      И, имхо, TDD (именно тдд а не юнит тесты) не всегда возможно. Есть класс задач где тесты не могут быть инициатором разработки и дизайна. Все равно в начале нужно продумать архитектуру а потом наращивать мясо.

                      Вопросы к ТДД гурам:
                      1. Что вы делаете с тестами если поменялась архитектура? Удаляете, переписываете? Вариант ответа: «ТДД дает сразу правильную архитектуру» не канает.
                      2. Как тестировать сложное окружение? Тестировать только элементарные блоки? или писать кучу моков?
                        +3
                        1. Архитектура постепенно меняется также со стороны тестов сперва, затем переходит в код. (конечно, получается иногда много тестов сломать изменением одного)
                        2. Насколько сложное? Используем DI. У блоков редко больше 5 зависимостей. Да, приходится писать stub или mock для каждого. Все равно тесты блока выделяются в отдельную группу для которой можно сделать общую инициализацию инжектируемых зависимостей и подправить в каждом конкретном тесте, если надо.
                          +7
                          Не считаю себя «TDD гурой» :), но попробую ответить.
                          Во-первых, в этой статье опять допустили классическую ошибку по этой теме: смешали TDD и умение писать юнит-тесты. Это 2 абсолютно разных вещи: TDD — это техника записи тестов перед кодом, юнит-тесты — это юнит-тесты :) Поэтому прежде чем практиковать TDD, научитесь писать юнит-тесты и не пытайтесь делать это одновременно — потратите много усилий и вероятно будете разочарованы. Умение писать юнит-тесты — такой же навык, например, как работа с регулярными выражениями. Вначале тяжело, а потом разбираешься и привыкаешь.
                          По юнит-тестам: уже рекомендованный выше Ошроув (маст-рид) и более академический Мессарош (xunit test patterns).
                          По ТДД: обе книги Кента Бека «TDD в примерах» и XP, они обе в сети есть.
                          Еще по архитектуре рекомендую «Чистый код» Р. Мартина («дядя Боб»)
                          Во-вторых, написание тестов для legacy-приложений, т.е. тех где в архитектуру не была заложена тестируемость (и у которых обычно отсутствуют тесты) — одна из самых сложных задач, под силу только test-гуру. Об этом почему-то практически все забывают. Если вы только начинаете с тестами — не беритесь за это в одиночку, лучше пригласите наемного специалиста и выделите на это бюджет (деньги/время).
                          В-третьих, обязательно нужно разделять виды тестов: юнит-тесты (про которые везде пишут), интеграционные тесты и системные тесты (про вторые и третьи намного меньше).
                          Юнит — все знают, тестируется один класс без внешних зависимостей (файловая система, бд, сеть — это заменяется заглушками). Работают везде, очень быстры, покрывают максимум кода.
                          Интеграционные тесты — тестируется взаимодействие нескольких классов вместе, по характеристикам обратны юнит-тестам: обычно внешние системы не подменяются, нужна настройка окружения, выполняются намного дольше, покрывают только нужные куски кода.
                          Системные тесты — тестируют систему сверху донизу, являются выполняемым ТЗ заказчика. Похожие на интеграционные тесты, но затрагивают всю систему, поэтому обычно выполняются еще дольше, сюда также относится тесты UI, внешних сервисов и т.п.
                          Очень важно разделять тесты в разные папки по типам, иначе они перемешаются и сложности настройки и эксплуатации интегр. и системных тестов убьют желание у разработчиков запускать вообще что бы то ни было.
                          1. Тесты зависят не от архитектуры, а от требований заказчика. Если — архитектура поменялась, а требования нет — тесты нужно подстроить под новую архитектуру. В плане ТДД — тесты меняются до изменения кода, поэтому «изменение архитектуры» происходит одновременно с изменениями тестов. Смысл ТДД — тесты двигаются чуть впереди кода и все действие идет маленькими шажками.
                          2. Тестирование элементарных блоков — это юнит, все зависимости подменяются моками. На все их писать необязательно, если метод очень простой (например, обычный геттер/сеттер), то только время убьете. Или у вас есть функция которая выполняет запрос к базе, типа такой:
                          function getClients($id) {
                               return $this->db->query("SELECT * FROM clients WHERE id = $id");
                          }
                          

                          смысла писать на нее юнит-тест нет, а вот интеграционный — стоит.
                          Написание тестов до основного кода очень сильно влияет на конечную архитектуру приложения, она сама начинает делиться на слои, что в итоге дает более удобную и гибкую архитектуру.
                          Основное правило: если вам тяжело писать тест — меняйте архитектуру.
                          Пишите тесты и все у вас получится :)
                            0
                            спасибо за такое подробное дополнение! но unit-тесты и TDD я как раз разделяю в статье, так что «классической ошибки» нет;)
                            это вообще два разных навыка — умение писать тесты и TDD. в статье на этом несколько раз акцентируется внимание.
                            в докладе я более подробно на этом останавливался — статью решил не раздувать.
                              0
                              Ну ясного разделения я не увидел, а главное предупреждения что не нужно пытаться начинать с обеими вещами одновременно. Но это мое мнение, за статью спасибо.
                                0
                                предупреждение тут: Риск остаться без ноги -> Неподготовленный старт
                                согласен с вами полностью в вопросе разделения. рекомендую всем начинать именно с написания тестов после функциональности, а уже потом, освоив первый навык, переходить к TDD.
                                спасибо за спасибо:)

                                  0
                                  >рекомендую всем начинать именно с написания тестов после функциональности, а уже потом, освоив первый навык, переходить к TDD

                                  Довольно большой риск в таком случае не перейти к TDD никогда, по-моему. Потому что писать тесты на код, который не был создан с учётом возможности тестирования зачастую большая проблема. Вплоть до того, что юнитом будет выступать вся программа, а «юнит»-тестом нужно будет создавать окружение (включая БД), эмулировать действия пользователя и перехватывать вывод программы. Надо ли говорить, что написать такой тест даже для одной ветви выполнения совсем не просто, не говоря о том, чтобы покрыть весь код. И если решение использовать тесты и TDD принято только на энтузиазме, то он быстро может кончиться.
                            0
                            Не гуру, но своё ИМХО вставить попробую:
                            1) Архитектуру нужно основательно продумывать заранее, да и как часто её действительно необходимо менять?
                            2) Например, habrahabr.ru/blogs/java/72617/, мок-библиотеки — нет необходимости самостоятельно описывать моки. Помимо этого существуют общие рекомендации по сложности кода, даже выбрать есть из чего. Например, описаны такие общеизвестные метрики как сцепление(CBO) и связность, глубина дерева наследования (DIT)… TDD даёт возможность превышать рекомендованные значения метрик Weightened Methods per Class, Response For Class / Number Of Methods (ajusted RFC) и прочих, без боязни того, что изменения вызовут неожиданный эффект ряби и необходимость в авральном порядке искать ошибки.
                          • НЛО прилетело и опубликовало эту надпись здесь
                              0
                              да, наставник — это здорово. лучший вариант — наставник-коллега. так как на тренингах вам скорее всего перескажут книжки и покажут пару простых примеров. книги, статьи, youtube — и уже есть какая-то база, на которую можно опираться. все зависит от желания.
                              • НЛО прилетело и опубликовало эту надпись здесь
                                0
                                Самостоятельное обучение всему (почти) чему угодно без «фидбэка» всегда (почти) сложнее, чем под чутким руководством опытного наставника (особенно если это действительно опытный наставник, а не «всего лишь» гуру в своей области).
                                +1
                                Статья хорошая, понравилась. Еще бы сравнение с альтернативными вариантами разработки классов и методов не помешало. Кстати, какие вообще реально альтернативы бывают?
                                  0
                                  наглядное сравнение работы программиста с использованием unit-тестов и без них было в докладе (отобразить это в статье достаточно трудно, так что не стал и пытаться) — добавлю ссылку как только будет готово видео.
                                  отдельно какие-то альтернативы я бы выделять не стал. есть набор хороших практик, которые помогают работать более эффективно. применять их или нет — зависит от желания и возможностей разработчика. для многих практик придуманы очень красивые аббревиатуры и абстракции:) хотя на самом деле это просто разумные подходы к решению задач, которые применяются людьми уже много лет. и не только в программировании:)
                                  0
                                  Есть ли автоматические способы тестирования GUI?
                                    0
                                    да. для web, например, есть Selenium.
                                      0
                                      Unit-тесты предназначены для тестирования логики и поведения отдельных классов.
                                      Если обработчик нажатия кнопки на пользовательском интерфейсе содержит какую-то бизнес-логику, то это сигнал пересмотреть архитектуру приложения и, как минимум, разделить, слои (layers).
                                        0
                                        Это не отменяет необходимости тестировать класс-обертку для кнопки, в том числе, что вызывается обработчик при нажатии.
                                          0
                                          Практически отменяет. Если кнопка «обновить» вызывает метод metamodel.Refresh(true) то тестировать надо метод модели, а не кнопку — кнопку (то что если на нее нажать — возникнет событие) уже протестировал разработчик используемого вами библиотеки пользовательского интерфейса.
                                            +1
                                            Если такая библиотека используется. Но даже если используется, это не значит, что кнопку вообще тестировать не надо. Например то, что она существует в принципе, что она видима пользователю и активна, что у неё надпись правильная и т. п.
                                              0
                                              Еще раз. То что при изменении свойства Visible кнопки меняется ее видимость — это тестировали за вас. Правильную в данном конкретном случае надпись кнопки тоже устанавливает модель и именно там и надо писать модульный тест, подставляя вместо кнопки какой-нибудь mock и проверяя в конце теста что надпись действительно изменилась.
                                              Модульные тесты — это максимально изолированные тесты без внешних зависимостей, таких как файловая система, база данных, библиотека пользовательского интерфейса, парсер html который из <button> нарисует на экране кнопку и т.д.
                                                0
                                                Под видимостью я имел в виду, что свойство Visible установлено, что в том же html вообще тэг button есть (парсером будет сам тест), что в этом тэге есть атрибут onclick с нужным значением и т. п. В общем под тестирование UI я имею в виду прежде всего тестирование слоя V в терминах MVC, ну и C к UI тоже относится.
                                      0
                                      Чак Норрис никогда не спит. Он ждёт. :)

                                      Хорошая статья, спасибо.
                                      Imho, было бы неплохо также сравнить TDD с BDD, которое является развитием первого.

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

                                      Самое читаемое