TDD: зачем и как

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

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

Если удобнее прочесть текстом — то добро пожаловать 😃

Плюсы тестирования

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

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

Они обнаруживают регрессии

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

Они становятся хорошим дополнением к документации

Они описывают, как на самом деле работает система. Если в документации мы пишем, почему она должна работать именно так, то тесты говорят как это «именно так».

В отличие от документации тесты не могут устареть

Если тесты устарели, они не пройдут. А если они не проходят, выкатка блокируется (ну, по-хорошему).

Но у тестов, конечно же, есть и издержки…

Издержки тестирования

Они. Требуют. Времени.

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

В каких-то проектах у нас времени нет: бизнес не терпит, горят сроки, надо вчера, разработчики фигачат код без тестов в продакшен. Так происходит не везде, но там где происходит, работать становится тяжело. Копится техдолг и баги, разработчики уходят с проекта.

TDD помогает решить эти проблемы

TDD (Test Driven Development) встраивается в процесс разработки и гарантирует, что к написанному коду будут готовы и тесты. Цикл разработки по TDD состоит из 3 этапов.

Цикл разработки по TDD, сперва пишем тест, затем функциональность, потом — рефакторим код, codedream.me
Цикл разработки по TDD, сперва пишем тест, затем функциональность, потом — рефакторим код, codedream.me

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

Второй этап — зелёная зона. На нём мы пишем функцию, которая проходит этот тест. Цикл короткий, поэтому реализация должна быть максимально простой.

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

Плюсы TDD

TDD как бы «выворачивает» процесс разработки. У такого вывернутого процесса есть несколько плюсов.

Исчезает проблема «дополнительной работы»

Мы пишем тесты сразу, ещё до начала работы над кодом самой сущности. К концу работы тесты уже написаны, нам не приходится прикладывать дополнительных когнитивных усилий, чтобы заставить себя «теперь написать ещё и тесты 🙄»

Писать тесты и рефакторить входит в привычку

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

Рефакторить становится безопаснее

Тесты же уже написаны 🙂

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

Становится видно сущности, которые занимаются разными задачами

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

Проектируем API до реализации, что делает его удобнее

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

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

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

Издержки TDD

Как и у любого инструмента, у TDD есть ограничения и издержки.

Надо привыкнуть к новому процессу разработки

Это не всегда получается с первого раза. Хочется сразу накатать реализацию на 20 строк и потом написать тесты. TDD бьёт по рукам в этот момент и заставляет нас вывернуть мозг наизнанку. Это по началу неудобно, но проходит через какое-то время 😃

Якобы «побуждает думать лишь о Happy Path»

Я считаю, что это слегка притянуто за уши, потому что о happy path можно думать и при разработке без тестов, и когда мы тесты пишем в конце.

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

Требует закладывать время на тесты

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

Тут мне можно задать вопрос:

Что значит с самого начала? А если не я называю срок — как его увеличивать? Как я объясню, почему сроки стали вдруг внезапно выше? Как убедить руководство, что это вообще нужно?

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

  • на что мы можем влиять непосредственно,
  • на что не всегда, опосредованно или не сами,
  • а на что не можем вовсе.

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

Как упростить тестирование

Для того, чтобы тесты занимали меньше времени, и чтобы время на них находилось, они должны быть простыми. А чтобы сделать их проще, есть несколько приёмов.

Чаще использовать чистые функции

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

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

Даже если вы работаете в классическом ООП, можно использовать плюсы чистых функций, если использовать, как её называет Марк Симанн, функциональную архитектуру.

Обращать внимание на зацепление кода

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

Тестировать только свой код

Кажется банальным, но иногда тесты могут проверять, что Math.random возвращает случайное число, или что библиотека работает, как заявлено. Это всё нам не нужно.

Если мы работаем с библиотекой, нам стоит написать к ней адаптер, и тестировать его.

Использовать удобные инструменты

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

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

Потратить время на удобную инфраструктуру

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

Как TDD помогает искать пахнущий код

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

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

Тестов слишком много по сравнению с остальными

Скорее всего, модуль делает несколько разных задач вместо одной.

Стоит посмотреть, какие задачи он решает, можно ли вынести часть из них в отдельные функции, методы, классы, модули. Короче, модуль вероятно нарушает SRP, а его интерфейс — ISP.

Описание тестов ссылается на разные сущности или аспекты приложения

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

Ожидание оформлено невнятно

Это сеет сомнения в том, как функция вообще должна работать. Если тест не может чётко и внятно ответить на вопрос «Что и при каких условиях должно произойти», то код в модуле непонятный.

Подготовка теста слишком сложная

Например, надо создавать слишком много мóков, стабов, добавлять какую-то экзотическую инфраструктуру, запрашивать дополнительные данные. Скорее всего, в модуле утекли абстракции, или мы где-то напортачили с зависимостями.

Тест проверяет детали реализации

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

Тесты должны проверять выход функции при заданных условиях. Передаём аргументы, проверяем результат. Если мы проверяем как именно этот результат получен, то либо надо сделать ревью публичного API (редко) и метод разделить на несколько, либо перестать проверять реализацию (чаще всего).

Нужно мóкать библиотеку

Беспорядочно вызывать библиотеки не стоит, нам нужны адаптеры для них. Об этом писал Майкл Физерс в «Эффективной работе с легаси». Именно адаптеры мы должны проверять.

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

Будь у нас собственный хук-адаптер, который бы вызывал useSelector, то нам бы пришлось переписать тесты только к нему. Об этом стоит почитать у Мартина в «Чистой архитектуре», в разделе про слой адаптеров.

Тест всегда зелёный

Никогда не падающий тест бесполезен 😃

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

Как помочь лиду увидеть пользу

Окей, вот мы наводим порядок в кодовой базе. Что мы можем сделать ещё?

Мы можем сходить к лиду и поговорить о TDD и тестировании. Скорее всего, лиды и сами понимают, зачем это всё. Но если «вдруг чего-то-там-где-то-там», то стоит посмотреть за измеряемые параметры, которые можно улучшить с тестами.

Параметры именно измеряемые, потому что разговоры из разряда «А вот было бы круто» — это не круто. Мы хотим подкреплять точку зрения фактами: желательно, такими, которые показывают, как именно работа влияет на здоровье проекта.

Из таких параметров можно перечислить:

  • Бас-фактор. Если о проекте знает всего лишь один человек, это плохо. Чем больше человек знает о том, как всё работает, тем выше вероятность что проект выживет и будет развиваться.
  • Количество регрессий. Если после каждого релиза в алёрт-монитор сыпется куча ошибок, это повод задуматься, что не так. Вероятно, мы не учитываем сложность системы при написании кода.
  • Документированность. Тесты — хорошее дополнение к документации, но если её нет вовсе, стоит начать с тестов.
  • Время на рефакторинг. Чем больше кода покрыто тестами, тем меньше времени будет уходить на рефакторинг и перепроверку работы после него. Рефакторить будет приятнее, потому не потребуется сидеть и полчаса тыкать кнопочки руками после каждого изменения.

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

Если на исследования времени выбить не получается, и вообще всё плохо, то можно попробовать попартизанить: покрыть тестами какой-то участок кода и сделать замеры самостоятельно. После — прийти и выложить результаты кхм… исследования на стол и обсудить их.

Как помочь бизнесу увидеть пользу

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

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

Важно: всё это работает, если проект — не прототип. У прототипов другая судьба: проверить гипотезу и умереть. Мы же говорим о проекте, который собирается жить долго.

На примере

Давайте разработаем ма-а-а-а-аленькое приложение по TDD. Пусть это будет функция, которая делит одно число на другое. (Для более сложных примеров советую прочесть книжку TTT-TDD или посмотреть мой доклад о TDD в React.)

Я в примере буду использовать Jest, если вы не знакомы с его API, то у него есть отличная документация.

Спроектируем тесты и функциональность.

  • Мы хотим, чтобы функция в принципе делила одно число на другое;
  • чтобы она округляла нецелый результат до 2-го знака после запятой по умолчанию;
  • чтобы мы могли указать в настройках значение для точности (например, 3–4 знака после запятой);
  • чтобы функция не позволяла нам делить на ноль.

Первый тест

Так как мы работаем по TDD, нам первым делом надо написать тест.

Мы хотим, чтобы когда функция вызвана, она вернула результат деления первого числа на второе. Я люблю в describe описывать условие при котором вызывается функция, а в it — что именно должно произойти, поэтому так и запишем:

//index.test.js

// Условие:
describe("when called", () => {

  // Ожидание:
  it("should return the result of division `a` over `b`", () => {

Внутри мы пишем код собственно теста. Хорошие тесты простые, независимые и лаконичные, но кроме этого они подчиняются 3-актной структуре “Arrange-Act-Assert”:

  • На первом шаге мы готовим (arrange) всё необходимое для теста — аргументы, ожидаемый результат.
  • На втором — вызываем (act) тестируемую функцию.
  • На третьем — сравниваем (assert) ожидаемый результат с настоящим.

Сперва подготовим ожидаемый результат, затем вызовем функцию и сравним.

// index.test.js

it('should return the result of division `a` over `b`', () => {
	const expected = 5;
	const result = divide(10, 2);
	expect(result).toEqual(expected);
});

Сейчас тест упал:

● when called › should return the result of division `a` over `b`

  TypeError: (0 , _.divide) is not a function

    15 |   it("should return the result of division `a` over `b`", () => {
    16 |     const expected = 5;
  > 17 |     const result = divide(10, 2);
       |                    ^
    18 |     expect(result).toEqual(expected);
    19 |   });

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

Красная зона

Добавим функцию и экспортируем её из модуля:

// index.js

export function divide(a, b) {
	return null;
}

Проверим тест теперь.

● when called › should return the result of division `a` over `b`

  expect(received).toEqual(expected) // deep equality

  Expected: 5
  Received: undefined

    17 |     const expected = 5;
    18 |     const result = divide(10, 2);
  > 19 |     expect(result).toEqual(expected);
       |                    ^
    20 |   });
    21 | });

Теперь тест падает по правильной причине. Мы проверяем это, чтобы убедиться, что тест тестирует именно наше предположение, а не что-нибудь другое.

Движемся к зелёной зоне

Можем приступить к реализации. По канонам TDD реализация должна быть максимально простой, даже тупо простой. Это нужно, чтобы цикл разработки занимал не больше 10–15 минут.

Самое простое, что мы можем сделать, — просто вернуть 5:

// index.js

export function divide(a, b) {
	return 5;
}

Тест проходит:

 PASS  ./index.test.js
  when called
    ✓ should return the result of division `a` over `b` (2 ms)

…Но очевидно, то просто возвращать 5 — это не особо полезно 😃
Мы отрефакторим функцию, чтобы она работала в более общем виде.

Синяя зона и рефакторинг

Обновим функцию:

// index.js

export function divide(a, b) {
	return b / a;
}

Тест упал!

● when called › should return the result of division `a` over `b`
  expect(received).toEqual(expected) // deep equality

  Expected: 5
  Received: 0.2

    17 |     const result = divide(10, 2);
    18 |     const expected = 5;
  > 19 |     expect(result).toEqual(expected);

При рефакторинге мы кое-что поломали: разделили аргументы в неправильном порядке.

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

// index.js

export function divide(a, b) {
	return a / b;
}
PASS  ./index.test.js
  when called
    ✓ should return the result of division `a` over `b` (1 ms)

Отлично, тест прошёл, можем двигаться дальше.

Расширяем функциональность

Проверим, как функция работает с дробями.

Напомню, мы хотим, чтобы функция по умолчанию при дробном результате округляла его до 2-го знака после запятой. Так и напишем, сформируем ожидание:

// index.test.js

describe('when the result is not an integer', () => {
	it('should round it to 2 decimal places', () => {
		expect(divide(10, 3)).toEqual(3.33);
	});
});

Тест падает, проверяем причину.

● when the result is not an integer › should round it to 2 decimal places

  expect(received).toEqual(expected) // deep equality

  Expected: 3.33
  Received: 3.3333333333333335

    24 | describe("when the result is not an integer", () => {
    25 |   it("should round it to 2 decimal places", () => {
  > 26 |     expect(divide(10, 3)).toEqual(3.33);
       |                           ^
    27 |   });

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

Пишем работу с дробями

Округляем результат до 2 знаков после запятой:

// index.js

export function divide(a, b) {
	return (a / b).toFixed(2);
}

Тест не починился, да ещё и прошлый упал!

FAIL  ./index.test.js
  when called
    ✕ should return the result of division `a` over `b` (2 ms)
  when the result is not an integer
    ✕ should round it to 2 decimal places

Дело в том, что toFixed возвращает строку, а мы проверяем на строгое соответствие.

TDD полезен ещё и тем, что выявляет странности стороннего API, когда мы начинаем с ним работаем. Давайте исправим реализацию:

// index.js

export function divide(a, b) {
	return Number((a / b).toFixed(2));
}
PASS  ./index.test.js
  when called
    ✓ should return the result of division `a` over `b` (1 ms)
  when the result is not an integer
    ✓ should round it to 2 decimal places (1 ms)

Отлично, тест проходит, мы в зелёной зоне, можем приступить к рефакторингу.

Генерируем тесты

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

// index.test.js

describe('when the result is not an integer', () => {
	it('should round it to 2 decimal places', () => {
		// Проверяем лишь один случай:
		expect(divide(10, 3)).toEqual(3.33);
	});
});

При этом писать однотипные тесты, которые отличаются только аргументами — не хочется. К счастью, мы можем нагенерировать тестов автоматически!

Создадим массив, в котором каждый элемент будет объектом с тестовым случаем. Там будут аргументы и ожидаемый результат:

// index.test.js

describe('when the result is not an integer', () => {
	const testCases = [
		{ a: 10, b: 3, result: 3.33 },
		{ a: 10, b: 6, result: 1.67 },
		{ a: 10, b: 7, result: 1.43 }
	];
});

А теперь проитерируемся по каждому и проверим, что тест работает:

// index.test.js
describe('when the result is not an integer', () => {
	const testCases = [
		{ a: 10, b: 3, result: 3.33 },
		{ a: 10, b: 6, result: 1.67 },
		{ a: 10, b: 7, result: 1.43 }
	];

	it.each(testCases)('should round it to 2 decimal places', ({ a, b, result }) => {
		expect(divide(a, b)).toEqual(result);
	});
});

В консоли:

PASS  ./index.test.js
  when called
    ✓ should return the result of division `a` over `b` (4 ms)
  when the result is not an integer
    ✓ should round it to 2 decimal places
    ✓ should round it to 2 decimal places
    ✓ should round it to 2 decimal places (1 ms)
    ✓ should round it to 2 decimal places

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

Создаём настройки

Теперь перейдём к указанию настроек.

// index.test.js

describe("when specified a precision", () => {
  it("should round the result up to the decimal place specified in the settings", () => {

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

// index.test.js

// expect(divide(10, 3, 5)).toEqual(3.33333); Непонятно, что за 5
// Так лучше:
expect(divide(10, 3, { precision: 5 })).toEqual(3.33333);

TDD удобен тем, что функцию мы сразу же используем — то есть сразу видим API со стороны потребителя. Если бы мы писали сперва код, то возможно, передали бы последним объектом просто число.

Напишем функцию:

// index.js

export function divide(a, b, settings = {}) {
	const { precision } = settings;
	return Number((a / b).toFixed(precision));
}

Какие-то тесты попадали.

  when the result is not an integer
    ✕ should round it to 2 decimal places
    ✕ should round it to 2 decimal places (1 ms)
    ✕ should round it to 2 decimal places
    ✕ should round it to 2 decimal places (1 ms)

Дело в том, что мы не учитываем объект с настройками в старых вызовах — мы сломали обратную совместимость.

// index.js

export function divide(a, b, settings = {}) {
	// Вернём округление до 2-го знака по умолчанию:
	const { precision = 2 } = settings;
	return Number((a / b).toFixed(precision));
}

Все тесты проходят:

PASS  ./index.test.js
  when called
    ✓ should return the result of division `a` over `b` (2 ms)
  when the result is not an integer
    ✓ should round it to 2 decimal places (1 ms)
    ✓ should round it to 2 decimal places
    ✓ should round it to 2 decimal places (1 ms)
    ✓ should round it to 2 decimal places
  when specified a precision
    ✓ should round the result up to the decimal place specified in the settings

Запрещаем делить на 0

Последнее, что нам хочется от этой функции, чтобы она кричала на нас, когда мы пытаемся делить на 0.

// index.test.js

describe("when tried to divide by 0", () => {
  it("should throw an error", () => {

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

// index.test.js

describe('when tried to divide by 0', () => {
	it('should throw an error', () => {
		const attempt = () => divide(10, 0);
		const error = new Error("Maths doesn't work like that...");
		expect(attempt).toThrow(error);
	});
});

Проверим причину падения теста:

● when tried to divide by 0 › should throw an error
  expect(received).toThrow(expected)

  Expected message: "Maths doesn't work like that..."
  Received function did not throw

    58 |     const attempt = () => divide(10, 0);
    59 |     const error = new Error("Maths doesn't work like that...");
  > 60 |     expect(attempt).toThrow(error);
       |                     ^

Причина верная, реализуем:

// index.js

export function divide(a, b, settings = {}) {
	if (b === 0) throw new Error('Nope!');
	const { precision = 2 } = settings;
	return Number((a / b).toFixed(precision));
}

Проверим, прошёл ли тест:

● when tried to divide by 0 › should throw an error
  expect(received).toThrow(expected)

  Expected message: "Maths doesn't work like that..."
  Received message: "Nope!"

    32 | export function divide(a, b, settings = {}) {
  > 33 |   if (b === 0) throw new Error("Nope!");
       |                      ^

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

Поправим:

// index.js

export function divide(a, b, settings = {}) {
	if (b === 0) throw new Error("Maths doesn't work like that...");
	const { precision = 2 } = settings;
	return Number((a / b).toFixed(precision));
}

Теперь тест проходит.

Итого

Давайте посмотрим, чего мы смогли добиться с помощью TDD:

  • функция отрабатывает по всем требованиям, которые мы составили вначале;
  • у нас к ней появился типа как бы «автоматически» набор тестов;
  • рефакторить код — безопасно, потому что тесты закрывают функциональность, которая уже написана;
  • API мы проектируем наперёд, поэтому оно становится проще и удобнее;
  • странности стороннего API мы тоже ловим на ранних этапах.

Самое главное, что все эти плюшки появляются как бы «сами», просто потому что процесс разработки требует от нас сперва написать тесты.

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

Список литературы

Как обычно, я собрал список исследований, книжек и статей, которые мне кажутся самыми полезными. Кроме них я также оставил ссылку на большой воркшоп по TDD в React-приложениях. Он довольно длинный, около 5 часов, но на нём я подробно показываю, как именно можно использовать TDD при разработке приложений на React.

Книги и статьи

Исследования и терминология

Мои статьи и доклады по теме