hwdtech blog
5 вопросов, на которые должен ответить каждый юнит-тест
Поговорим о том, как написать лучшие в мире тесты и как ими пользоваться для получения наиболее ценной информации о коде.
27-04-2020
перевод, JS, JavaScript, front-end
Время чтения: 7 минут
Продолжаем переводить для вас статьи Эрика Эллиотта, редактора таких блогов на Medium, как JavaScript Scene и The Challenge, а также топового автора в разделе «Технологии». Подробнее об авторе можно прочитать внизу страницы, а вот по этим ссылкам - раз и два – вы можете найти предыдущие материалы его авторства, которые мы уже перевели.
Сегодня мы под его чутким руководством отправляемся в удивительный мир юнит-тестов. Автор утверждает, что грамотное тестирование выходит далеко за рамки обычной «проверки на наличие ошибок». Как это работает и почему – читайте в нашем материале, а оригинал статьи можно посмотреть здесь.
Большинство разработчиков не знают, как тестировать
Всякий разработчик знает, что нужно писать юнит-тесты, чтобы предотвратить деплой в продакшн различных дефектов. О чем большинство из них не догадывается, так это об основных компонентах, которые важны для каждого юнит-теста.
«Я не могу сосчитать, сколько раз я видел сбой юнит-теста, после которого обнаруживалось, что абсолютно никому неизвестно, какую функцию пытался протестировать девелопер, не говоря уже о том, что именно пошло не так и почему это имеет значение», - говорит нам автор, - «В одном из моих недавних проектов мы позволили гигантскому количеству юнит-тестов войти в тестовый набор вообще без всякого описания целей. У нас великолепная команда, поэтому я утратил бдительность. Результат? У нас до сих пор тонна юнит-тестов, назначение которых понятно только их авторам».
К счастью, они сделали полный редизайн API и собираются выбросить весь тестовый набор на помойку и начать с нуля. «В противном случае, это был бы пункт №1 в моем списке исправлений», - уверяет Эллиотт.
Не позволяйте подобному случиться с вами!
Зачем беспокоиться о «тестовой дисциплине»?
Ваши тесты – это первая и основная линия обороны от программных дефектов.

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

Ваши тесты так же важны, как и сама реализация. Все, что на самом деле имеет значение, это то, отвечает ли код требованиям. В противном случае не важно, как именно он реализован.
Юнит-тесты сочетают в себе множество функций, которые делают их секретным оружием для достижения успеха в разработке приложения:
1
Помощь в разработке: первоочередное написание тестов дает более четкое представление об идеальном дизайне API.
2
Документация фич (для девелоперов): описания тестов или «test descriptions» закрепляют в коде каждое реализованное функциональное требование.
3
Проверка понимания процесса разработки: понимает ли разработчик проблему достаточно хорошо, чтобы четко выразить в коде все критические требования к компонентам?
4
Обеспечение качества: ручной QA подразумевает ошибки! По своему опыту автор говорит о том, что разработчик не может запомнить все функции, которые необходимо протестировать после внесения изменений в рефакторинг, добавления или удаления фич.
5
Помощь при сontinuous delivery: автоматизированный QA позволяет автоматически предотвращать деплой поврежденных билдов в продакшн.
Важно, что вам не придется изменять юнит-тесты или как-то манипулировать ими для достижения целей. Скорее, в этом кроется суть юнит-тестов - удовлетворять данные потребности. Все эти преимущества являются побочными эффектами грамотно написанного тестового набора с хорошим охватом.

Наука Test Driven Development
Доказательства говорят сами за себя:

- TDD может уменьшить плотность ошибок;

- TDD может поддерживать более разнообразные модульные конструкции, повышая гибкость программного обеспечения (software agility)/ скорость работы команды(team velocity);

- TDD может уменьшить сложность кода;

Исследования говорят нам: существуют значительные эмпирические доказательства того, что TDD работает *.
Первым делом пишите тесты
Исследования, проведенные Microsoft Research, IBM и Springer, проверяли эффективность методологий test-first и test-after и обнаружили, что процесс test-first дает лучшие результаты, нежели более позднее добавление тестов.
Совершенно очевидно: прежде чем начинать реализацию – напишите тест.
Что должно быть в хорошем юнит-тесте?
ОК, значит TDD работает. Первым делом надо писать тесты. Быть более дисциплинированным. Довериться процессу… Все всё поняли. Но как написать хороший юнит тест?
Для изучения этого процесса мы рассмотрим очень простой пример из реального проекта: функция `compose ()` из Stamp Specification.
Будем использовать тестовый фреймворк «Tape», потому что он абсолютно простой и понятный. Кстати, у автора и про него материал есть! Вот он.
Прежде чем мы узнаем ответ на вопрос о том, как написать хороший юнит-тест, мы должны четко понимать, как они используются. (И хотя мы уже говорили об этом в начале нашей статьи, видимо, автор считает, что не лишним будет это повторить – прим. переводчика).

Итак, юнит-тесты…
- …Помогают в разработке, так как пишутся на этапе проектирования, до реализации;

- …Помогают документировать фичи и проверять понимание разработчиков: тест должен содержать четкое описание тестируемой функции:

- …Помогают при QA / Continuous Delivery: тесты должны остановить delivery pipeline в случае сбоя и выдать качественный баг репорт, если это произошло.
Юнит-тест как баг репорт
Если тест провален, то test failure-репорт часто является вашей первой и лучшей подсказкой о том, что именно пошло не так: секрет быстрого обнаружения ошибки в том, чтобы знать, откуда начинать поиски.

Этот процесс становится намного проще, когда у вас есть действительно четкий баг репорт.
Отчет о проваленном тесте и нужно воспринимать как высококачественный баг репорт.
На какие вопросы он должен отвечать:
1. Что вы вообще тестировали?

2. Что оно должно было делать?

3. Что оно делало: каким были выходные данные (актуальное поведение)?

4. Каким было ожидаемое поведение (ожидаемые выходные данные)?



Начните с ответа на вопрос «Что вы тестируете?»
- Какой компонент вы тестируете?

- Что эта фича должна делать? Какие специфические требования к поведению вы тестируете?

Например, наша функция `compose ()` принимает любое количество stamps и создает новый stamp.

Чтобы протестировать ее, мы будем двигаться в обратном направлении от конечной цели любого теста - проверки конкретного требования к поведению. Давайте ответим на вопрос: «Для успешного прохождения теста, какое специфическое поведение должен продемонстрировать код?».
Что должна делать функция?
Я предпочитаю начинать с написания строки. Ни к чему не привязанной. Не передающейся ни в одну функцию. Просто четкий акцент на конкретном требовании, которое компонент должен удовлетворять. В данном случае мы начнем с того, что функция `compose ()` должна возвращать функцию.

Простое требование, выполнение которого легко проверить:

'compose() should return a function.'

Сейчас мы пропустим некоторые вещи и конкретизируем остальную часть теста. Эта строка служит нам в качестве цели. Мы обозначили ее заранее, и она поможет нам фокусироваться на нужной информации.
Какой аспект компонента мы тестируем?
То, что вы подразумеваете под «аспектом компонента», будет варьироваться от теста к тесту, в зависимости от детализации, требуемой для обеспечения адекватного покрытия.

В данном случае мы собираемся протестировать возвращаемый тип функции `compose ()`, чтобы убедиться, что она возвращает правильные данные, а не «undefined» или вообще ничего – потому, что ломается при запуске.

Давайте переведем заданный в заголовке вопрос в тестовый код. Ответ мы получим в описании теста. На этом шаге мы также вызовем нашу функцию и передадим функцию callback, которую запустит тестировщик при старте тестов:

test('<What component aspect are we testing?>', assert => {

});

В этом случае мы тестируем выходные данные функции `compose ()`:

test('Compose function output type.', assert => {

});


И, конечно, нам все еще нужно наше первое описание. Оно входит в функцию callback.

test('Compose function output type.', assert => {

'compose() should return a function.'

});

Какими будут выходные данные (ожидаемые и фактические)?
`equal ()` - мое любимое утверждение. Если бы единственное доступное утверждение в каждом тестовом наборе было равно «equal ()», почти каждый набор тестов в мире был бы лучшим. Почему?

Потому что `equal ()` по своей природе отвечает на два самых важных вопроса, на которые должен ответить каждый юнит-тест (но большинство не отвечают):
- Каковы фактические выходные данные?

- Каковы ожидаемые выходные данные?

Если вы закончите тестирование, не ответив на эти два вопроса, у вас не будет настоящего юнит-теста. У вас будет нечто сырое и неряшливое. И даже более того, если вы планируете запомнить только одну мысль из этой статьи, пусть это будет следующее:
«Equal» - это ваше новое утверждение по умолчанию. Это основа любого хорошего тестового набора.
А все эти необычные библиотеки утверждений с сотнями различных причудливых вариантов только разрушают качество ваших тестов.
Челлендж
Хотите стать намного лучше в деле написания юнит-тестов? В течение следующей недели попробуйте написать каждое отдельное утверждение, используя «equal ()» или «deepEqual ()», или их эквиваленты в выбранной вами библиотеке утверждений. Не беспокойтесь о том, как это повлияет на качество вашего тестового набора. «Мои деньги говорят, что это упражнение невероятно его повысит!», - пишет Эллиотт.

Как это выглядит в коде:

const actual = '<what is the actual output?>';

const expected = '<what is the expected output?>';

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

const actual = '<how is the test reproduced?>';

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

const actual = typeof compose();

const expected = 'function';


Вы можете создать утверждение, не присваивая специально переменным значения «фактические» и «ожидаемые», но автор недавно начал делать это в каждом тесте и обнаружил, что это делает его тесты более простыми для прочтения.
Посмотрите, как это уточняет утверждение:

assert.equal(actual, expected,

'compose() should return a function.');


Оно отделяет «как» от «что» в теле теста.


- Хотите знать, как мы получили результаты? Посмотрите на значения переменных.

- Хотите знать, для чего мы проводим тестирование? Посмотрите на описание утверждения.

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

Давайте посмотрим на все это в контексте:

В следующий раз, когда вы напишите тест, не забудьте ответить на все вопросы:

- Что вы тестируете?

- Что оно должно делать?

- Каков фактический результат?

- Каков ожидаемый результат?

- Как можно воспроизвести тест?


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

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

Еще немного об авторе:

Эрик Эллиотт является автором книг «Programming JavaScript Applications» (O'Reilly) и «Learn JavaScript Universal App Development with Node, ES6, & React». Он принимал участие в разработке программного обеспечения для Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC, а также ведущих исполнителей, включая Usher, Frank Ocean, Metallica и многих других.

Раз в месяц мы делаем рассылку с анонсом новых кейсов и статей, опубликованных на сайте.
Подпишитесь на обновления.
Гарантируем - никакого спама. Нажимая на кнопку, вы даете согласие на обработку персональных данных и соглашаетесь c политикой в отношении обработки персональных данных.