Тестируйте шаг за шагом, а не одну гору за раз

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

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

Одни и те же команды снова и снова приходили к одному и тому же решению, поэтому мне интересно, почему такие команды не пишут достаточно тестов. «Достаточно тестов» может быть плохим выбором слов - качество ваших тестов так же важно, как и количество. Но в любом случае: основываясь на моих собственных анекдотических выводах, большинство из нас думает, что мы должны писать больше / лучше тестов.

Так почему бы и нет? Когда я думаю об этом логически, напрашивается очевидный вывод (очевидный для фанатиков TDD). Постфактум сложно писать тесты.

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

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

Зачем нужен рефакторинг? Что ж, эти 200 строк непросто проверить. Чтобы сделать их тестируемыми, вам нужно немного реорганизовать. Рефакторинг может легко сломать ваш код, но, по крайней мере, у вас есть подстраховка, состоящая из тестов, чтобы… о, подождите. Видите проблему?

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

Что такое TDD и почему вам это нужно?

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

Как мне протестировать то, что я еще не написал?

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

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

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

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

  1. Учитывая, что продукт A есть в наличии (количество ›0)
    Когда я добавляю продукт A в свою корзину
    Тогда в моей корзине должен быть 1 продукт A
  2. Учитывая, что продукт A есть в наличии (количество = 0)
    Когда я добавляю продукт A в свою корзину,
    тогда должна появиться ошибка, сообщающая мне, что товара нет в наличии.
  3. Учитывая, что Продукт A есть в наличии (количество = 1)
    Когда я добавляю 2 x продукта A в свою корзину
    Тогда должна появиться ошибка, сообщающая мне, что на складе недостаточно

Предполагая, что уже есть тесты для добавления товара в вашу корзину (без проверки наличия), тогда тест 1 будет легко написать, поэтому мы должны начать с него. Запишите остальные тестовые примеры и просто сосредоточьтесь на проблеме, стоящей перед вами, а именно «как мы проверяем уровень запасов?»

Ваш первый тест может выглядеть только так:

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

Я пока точно не знаю, какие методы будут в службе, но теперь я могу обновить класс Basket, чтобы он принял StockService в своем конструкторе. После этого тест должен «пройти» (т.е. скомпилироваться), после чего я могу перейти к своему первому утверждению:

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

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

Это одна из вещей, которые, я думаю, люди забывают объяснить о разработке через тестирование. Речь идет не только о написании более качественных тестов, но и о естественном разбиении больших проблем на мелкие кусочки. В результате тестирование часто может показаться гораздо менее сложным, чем если бы у вас было 200 строк кода для тестирования. Обычно вы просто пытаетесь найти минимальный объем теста, который приведет к сбою тестового прогона, а затем минимальный объем кода, чтобы он прошел.

Затем, если нужно, вы проводите рефакторинг. На этот раз у вас есть подстраховка в виде только что написанного теста. Этот процесс часто называют красным, зеленым, рефакторингом.

  • Красный - это когда тест не прошел.
  • Зеленый - когда тест проходит.
  • Рефакторинг - это когда вы смотрите на то, как вы прошли тест, и решаете, что есть лучший способ. Вместо того, чтобы говорить «мы исправим это позже», вы пытаетесь исправить это сейчас, пока это свежо в вашей памяти.

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

Так что прекратите писать тесты для кода, который вы написали, и начните писать тесты для кода, который вы еще не написали.