[Перевод] Об удобочитаемом именовании тестов в JS и поведенческом паттерне

В ходе очередного ревью толстого Pull Request’а наткнулся на Unit Test’ы с некорректным именованием тест-кейсов. Обсуждение формулировок в тест-кейсах получилось похожим на разговор Янычара и Легкоступова в к/ф »72 метра» («если б мне в школе так доходчиво…»). В разговоре прозвучала мысль, что в рускоязычных ресурсах трудно найти толковый гайд именно по текстовым формулировкам. Решил искать самолично на русском (обычно я пользуюсь только англоязычными источниками). На хабре нашел несколько мануалов про юнит-тесты, но все они обходят стороной детали формулировок в тест-кейсах. Под катом моя попытка восполнить данный пробел.


Дисклэмер

Есть шанс, что я плохо искал / слишком по диагонали читал. Вот пример того как тема этой статьи освещена в тех статьях, что попадались мне на глаза.

xj85eux2nk1lxg3fjcdxhbo-0_y.png
TDD для начинающих

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


От переводчика

За основу для статьи взял эти два материала:

Еще вынужден заметить, что в некоторых примерах тестов пришлось сделать частичный перевод на русский. Формулировки в блоках «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('[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.

Спасибо за внимание!

© Habrahabr.ru