Различные виды тестирования

- Модульные тесты (Unit tests)

Модульное тестирование, или юнит-тестирование (Unit testing) - процесс в программировании, позволяющий проверить на корректность отдельные модули исходного кода программы.

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

- Интеграционные тесты (Integration tests)

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

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

- Функциональные тесты (Functional tests)

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


TDD & BDD

Что это вообще за буквы? И то, и другое - подходы к разработке, когда сначала пишутся тесты, а потом код.

*DD (*что-то* Driven Development) - разработка, основанная на чем-то.

TDD (Test Driven Development) - Разработка на основе тестов.

BDD (Behavior Driven Development) - Разработка на основе поведения.

BDD, на самом деле, является расширением TDD-подхода. Тем не менее, они предназначены для разных целей и для их реализации используются разные инструменты.

- В чем разница

  • TDD хорошо подходит для юнит-тестирования, т.е. для проверки работы отдельных модулей самих по себе. BDD - для интеграционного (т.е. для проверки, как отдельные модули работают друг с другом) и e2e (т.е. для проверки всей системы целиком) тестирования.
  • TDD: тесты сразу реализуются в коде, для BDD чаще всего описываются шаги на языке, понятном всем, а не только разработчикам.
  • TDD: юнит-тесты пишут сами разработчики. BDD требует объедения усилий разных членов команды. Обычно тест-кейсы (шаги) описываются ручным тестировщиком или аналитиком и воплощаются в код тестировщиком-автоматизатором.
  • TDD проверяет работу функций, BDD - пользовательские сценарии.

- А как выглядит на примере

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

Как подойти к этой задаче, используя TDD подход:

  • Пишем тест, в котором проверяем, что функция getCatFood() возвращает нужные значения в разных ситуациях
  • Проверяем, что тесты упали (кода еще нет)
  • Пишем код функции очень просто - так чтобы тесты прошли
  • Проверяем, что тесты прошли
  • На этом шаге можем задуматься о качестве кода. Можем спокойно рефакторить и изменять код как угодно, т.к. у нас есть тесты, которые с уверенностью скажут, что мы где-то ошиблись
  • Повторяем все вышеуказанные шаги еще раз

Как подойти к этой задаче, используя BDD подход:

  • Процесс начинается с того что пользователь открывает форму
  • Нам нужно протестировать числа которые выдает форма
  • Нам нужно ввести 10-20 разных значений
  • Проверка в данном случае это нажатие на Submit кнопку и проверка значения
  • Тест пройдет если результат на форме соответствует "правильным" значениям

Тестируем JavaScript-код с Jest

Jest - это простая и удобная среда тестирования. Она не требует дополнительных настроек, легка в понимании и использовании, имеет неплохую документацию. Кроме того, прекрасно подходит для проектов, в которых используются Node, Angular, Vue, React, Babel, TypeScript. Давайте посмотрим, как всё это выглядит на практике.

- Установка

Чтобы установить Jest, выполняем:

npm install --save-dev jest

Также после установки можно обновить секцию scripts вашего файла package.json:

"scripts" : {
    "test": "jest"
}

Посредством такого простого вызова уже можно запускать тесты, хотя Jest всё равно потребует существование хотя бы одного теста.

Выполнить установку можно и глобально:

npm install jest -global

Далее можно использовать Jest уже из командной строки.

- Первый тест на Jest

Итак, создадим файл first.test.js, а потом напишем первый тест:

// first.test.js
test('My first test', () => {
    expect(Math.max(1, 5, 10)).toBe(10);
});

Теперь запустим тесты посредством npm run test или командой jest (при глобальной установке). После запуска вы увидите отчёт о прохождении тестов.

 <b>PASS</b>  ./first.test.js
  ✓ My first test (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        0.618 s, estimated 1 s

Что же, разберём код теста. У нас есть функция test, используемая для создания нового теста. Она принимает 3 аргумента (в примере у нас 2 аргумента). Первый - это строка с названием теста (jest отображает его в отчёте). Второй - это функция, содержащая логику теста. При необходимости используют и третий аргумент - таймаут. Он необязателен и задаётся в миллисекундах. Значение по умолчанию - 5 сек.

Следует добавить, что вместо test() мы можем применять it() - разницы нету, а it() просто является алиасам на функцию test().

Идём дальше. Внутри функции теста мы поначалу вызываем функцию expect(), которой передаём значение для проверки. В нашем случае речь идёт о результате вызова Math.max(1, 5, 10). Здесь expect() возвратит объект-"обёртку", у которой есть методы для сопоставления полученного значения и ожидаемого значения. Один из этих методов мы, как раз, и использовали - это метод toBe.

- Разберем основные из этих методов:

toBe() - подойдёт, если нужно сравнить примитивные значения либо проверить, является ли переданное значение ссылкой на тот объект, который указан как ожидаемое значение. Значения сравниваются посредством Object.is();

toEqual() - если нужно сравнить структуру более сложных типов. Метод выполнит сравнение всех полей переданного объекта с ожидаемым. Он проверит каждый элемент массива, сделав это рекурсивно по всей вложенности:

test('toEqual with objects', () => {
    expect({ foo: 'foo', subObject: { baz: 'baz' } })
        .toEqual({ foo: 'foo', subObject: { baz: 'baz' } });  // ок
    expect({ foo: 'foo', subObject: { num: 0 } })
        .toEqual({ foo: 'foo', subObject: { baz: 'baz' } });  // ошибка
});

test('toEqual with arrays', () => {
    expect([11, 19, 5]).toEqual([11, 19, 5]);  // ок
    expect([11, 19, 5]).toEqual([11, 19]);  // ошибка
});

toContain() - проверит, содержит ли массив либо итерируемый объект значение (применяется оператор ===):

const arr = ['apple', 'orange', 'banana'];
expect(arr).toContain('banana');
expect(new Set(arr)).toContain('banana');
expect('apple, orange, banana').toContain('banana');

toContainEqual() - содержит ли массив элемент с ожидаемой структурой:

expect([{a: 1}, {b: 2}]).toContainEqual({a: 1});

toHaveLength() - соответствует ли свойство length у объекта ожидаемому:

expect([1, 2, 3, 4]).toHaveLength(4);
expect('foo').toHaveLength(3);
expect({ length: 1 }).toHaveLength(1);

toBeNull() - проверка на равенство с null;

toBeUndefined() - проверка на равенство с undefined;

toBeDefined() - противоположность toBeUndefined. Проверка, является ли значение !== undefined;

toBeTruthy() - проверка, соответствует ли значение true в булевом контексте;

toBeFalsy() - противоположность toBeTruthy(). Проверка, соответствует ли значение в булевом контексте false;

toBeGreaterThan() - проверка, больше ли числовое значение, чем ожидаемое;

toBeGreaterThanOrEqual() - проверка, что ожидаемое значение больше или равно ожидаемому;

toBeLessThan() и toBeLessThanOrEqual() - противоположности toBeGreaterThan() и toBeGreaterThanOrEqual();

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

const num = 0.1 + 0.2;  // 0.30000000000000004
expect(num).toBeCloseTo(0.3);
expect(Math.PI).toBeCloseTo(3.14, 2);

toMatch() - проверка, соответствует ли строка регулярному выражению:

expect('Banana').toMatch(/Ba/);

toThrow() - подходит, если нужно проверить исключение. Метод может проверить сам факт ошибки либо выполнить проверку на выброс исключения определённого класса, а также по сообщению ошибки либо по соответствию сообщения регулярному выражению:

function funcWithError() {
    throw new Error('some error');
}
   
expect(funcWithError).toThrow();
expect(funcWithError).toThrow(Error);
expect(funcWithError).toThrow('some error');
expect(funcWithError).toThrow(/some/);

not - свойство, позволяющее выполнить проверку на неравенство. Оно предоставляет объект, имеющий все вышеперечисленные методы, однако работать они будут наоборот:

expect(true).not.toBe(false);
expect({ foo: 'bar' }).not.toEqual({});

function funcWithoutError() {}
expect(funcWithoutError).not.toThrow();

Больше информации.