Pull to refresh

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

Reading time5 min
Views1.8K
Original author: Yahel Yechieli, mawrkus @ github

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


Дисклэмер


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



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:
[конкретная сущность] должна [ожидаемое действие / поведение] при (в случае | если) [название сценария или краткое описание условия]


или в виде кода:


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] — itexpect.


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

Tags:
Hubs:
+12
Comments2

Articles