В ходе очередного ревью толстого Pull Request'а наткнулся на Unit Test'ы с некорректным именованием тест-кейсов. Обсуждение формулировок в тест-кейсах получилось похожим на разговор Янычара и Легкоступова в к/ф "72 метра" ("если б мне в школе так доходчиво..."). В разговоре прозвучала мысль, что в рускоязычных ресурсах трудно найти толковый гайд именно по текстовым формулировкам. Решил искать самолично на русском (обычно я пользуюсь только англоязычными источниками). На хабре нашел несколько мануалов про юнит-тесты, но все они обходят стороной детали формулировок в тест-кейсах. Под катом моя попытка восполнить данный пробел.
Дисклэмер
Есть шанс, что я плохо искал / слишком по диагонали читал. Вот пример того как тема этой статьи освещена в тех статьях, что попадались мне на глаза.
По просьбе коллег, которым на английском языке руководства читать не комфортно, решил перевести и скомпилировать англоязычные мануалы.
От переводчика
За основу для статьи взял эти два материала:
Еще вынужден заметить, что в некоторых примерах тестов пришлось сделать частичный перевод на русский. Формулировки в блоках "describe" умышленно остаются на английском, т.к. с большой вероятностью будут содержать имя функций, модулей JS или других сущностей в коде, а вот в блоках "it" текст уже переводится для удобства чтения.
Мое личное предпочтение — в коде все должно быть на английском языке.
Именование тестов
Имя теста должно максимально кратко и явным образом описывать свое предназначение. Название и описание теста — это первое, что должно указывать на причину неисправности. Результат теста в консоли должен читаться корректно с точки зрения грамматики. Сторонние разработчики не должны в голове решать ребусы, пытаясь догадаться о чем думал автор теста. Тесты являются частью документации к программе и они также должны быть грамотно написаны.
ПЛОХОЙ пример:
describe('discoveryService => initDiscoveries', () => { it('инициализируем discoveries (дергаем очистку, загрузку данных и т.д.)', () => { // ... }); }); describe('MyGallery', () => { it('init при вызове задает корректные свойства (размер иконки, кол-во иконок)', () => { }); // ... });
Из примеров выше трудно понять какое конкретно действие (действия) совершается и к какому конкретному результату действие должно приводить.
ХОРОШИЙ пример:
describe('discoveryService => initDiscoveries', () => { it('должна очистить данные discoveries', () => { // ... }); it('должна получить новые данные для discoveries', () => { // ... }); }); describe('Экземпляр Gallery', () => { it('должен правильно вычислять размер иконки при вызове инициализации', () => { }); it('должен правильно вычислять количество иконок при вызове инициализации', () => { }); // ... });
Прим. перев. #1: обратите внимание, блок текста в it начинается с прописной, т.к. является продолжением предложения, начавшегося в descibe.
Прим. перев. #2: в примерах выше "discoveryService => initDiscoveries" корректнее все-таки разбить на два блока descibe (один вложен в другой).
Прим. перев. #3: обратите внимание, в примерах про discovery выше нет второй части описания тест-кейса; там подразумевается текст вида "при ее вызове", что не очень хорошо с точки зрения явственности; в простых случаях копипастить "при ее вызове" не особо профитно, ИМХО.
В блок describe обычно помещают описание элементарной работы (Unit of Work, UoW). Формулировка в блоке it должна продолжать паттерн "unit of work — scenario/context — expected behaviour", начавшийся в describe:
[конкретная сущность] должна [ожидаемое действие / поведение] при (в случае | если) [название сценария или краткое описание условия]
или в виде кода:
describe('[unit of work]', () => { it('должна [ожидаемое поведение] когда/если [сценарий/контекст]', () => { }); });
Если несколько групп тестов следуют одному сценарию или укладываются в один контекст, то можно использовать вложенные блоки describe.
describe('[unit of work]', () => { describe('когда/при/если [scenario/context]', () => { it('дожнен/должна [expected behaviour]', () => { }); }); }); describe('Экземпляр Gallery', () => { describe('при инициализации', () => { it('должен корректно вычислять размер иконки', () => { }); it('должен корректно вычислять количество иконок', () => { }); }); // ... });
ОДИН ТЕСТ — ОДНА ПРОБЛЕМА
Каждый тест должен фокусироваться на одном конкретном сценарии в работе приложения. Тест, ответственный за один конкретный аспект, способен выявить конкретную причину неисправности. Чем конкретнее тест, тем меньше шансов, что причин некорректного поведения может оказаться несколько. Старайтесь размещать в одном блоке it лишь один блок expect.
ПЛОХОЙ пример:
describe('isUndefined function', ()=> { it('должна возвращать true or false когда аргумент является undefined', () => { expect(isUndefined(undefined)).toEqual(true); expect(isUndefined(true)).toEqual(false); }); });
Блок it содержит два блока expect. Это означает, что разработчик увидев отрицательный результат выполнения данного теста не сможет точно определить, что конкретно в его коде некорректно и как это исправить.
ХОРОШИЙ пример:
describe('isUndefined function', ()=> { it('должна вернуть true, если аргумент является undefined', () => { expect(isUndefined(undefined)).toEqual(true); }); it('должна вернуть false если аргумент имеет значение логического типа', () => { expect(isUndefined(true)).toEqual(false); }); });
Каждый тест в примере выше оценивает одну конкретную проблему. Кроме того, в описании теста четко описано в каком случае он будет пройден. В обоих кейсах в консоли разработчик прочитает в виде списка какие результаты при каких действиях / условиях ожидаются от тестируемой пользовательской функциональности.
Тестируем поведение
Смотрите на картину, не разглядывайте мазки. Тестируйте пользовательский сценарий / поведение, а не детали реализации. Тогда изменение деталей реализации не повлияют на результаты тестирования. Отрицательный результат теста должен говорить о том, корректно или нет ведет себя программа с точки зрения пользователя. Тест не должен контролировать / ограничивать детали реализации.
ПЛОХОЙ пример:
it('должна добавить данные discovery в кэш', () => { discoveriesCache.addDiscovery('57463', 'John'); expect(discoveriesCache._discoveries[0].id).toBe('57463'); expect(discoveriesCache._discoveries[0].name).toBe('John'); });
Что здесь плохо? Во-первых, два блока expect, но не это главное. Во-вторых, тестируется не поведение, а детали реализации. Детали реализации поменяются (переименованы приватные поля) — тест станет не валидным и его нужно будет переписывать.
ХОРОШИЙ пример:
it('должна добавить данные discovery в кэш', () => { discoveriesCache.addDiscovery('57463', 'John'); expect(discoveriesCache.isDiscoveryExist('57463', 'John')).toBe(true); });
В этом примере тестируется публичное API, которое должно быть максимально стабильным.
ЗАКЛЮЧЕНИЕ ОТ ПЕРЕВОДЧИКА
"Онегин был педант..." У меня складывается впечатление, что большинство разработчиков уделяют точности и удобочитаемости названий тестов недостаточно много внимания. Часто наблюдаю довольно длительные обсуждения вида "А что же делает этот код" или "А зачем этот код". Это касается как основного кода в JS (неясные, нечеткие названия модулей, сервисов, функций и переменных), так и тестов (размытые кейсы, тестирование деталей реализации, нечеткие описания). Все это ведет к тому, что код делает не совсем то, что ожидается.
В одном из своих интервью Дэвид Хайнмейер Хэнссон (David Heinemeier Hansson, создатель фреймворка Rails) сказал что-то вроде следующего:
"Юнит тесты показывают лишь то, что ваша программа ожидаемым образом делает го%: о".
Он имел в виду то, что тестировать надо поведение, а не юниты кода. И текстовые формулировки должны иметь поведенческий паттерн. Т.е. "Сущность А должна вести себя так-то при таких-то условиях". В такую складную формулировку должна превращаться цепочка вида describe [- describe] — it — expect.
Спасибо за внимание!

