Как мы тестируем тесты. Утилита Suppressor

Практики курса по тестированию сильно отличаются от практик других курсов. Обычно вам нужно написать реализацию какой-то функции под существующий набор тестов. Здесь же всё наоборот. Вам нужно написать тесты для готового кода, и система должна проверить правильность выполнения написанных тестов.

Рассмотрим работу практики на конкретном примере. Представьте себе функцию last(), которая возвращает последний элемент в массиве:

numbers = [2, 2];
console.log(last(numbers)); // 2

Можно ли однозначно утверждать, что она работает верно, основываясь на примере выше? Нет. Вполне возможно, что эта функция реализована так:

const last = () => 2; // всегда возвращает 2

В таком случае понадобится еще один вызов, чтобы проверить ее работу:

const numbers = [5];
console.log(last(numbers)); // => 5

Достаточно ли теперь? И снова нет. Ее код может быть таким:

const last = (items) => items[0]; // возвращает первый элемент вместо последнего

При такой реализации и первый вызов, и второй вернут правильные данные, хотя функция реализована неверно. А вот правильная реализация функции:

const last = (items) => items[items.length - 1];

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

Существует ровно один хороший способ, проверить, что вы написали тесты правильно — вызвать эти тесты с разными реализациями функции. Если реализация правильная, тесты должны успешно пройти, если нет — упасть с ошибкой. Такой подход позволяет проверять эффективность тестов, а не то, как они написаны.

Как технически работает проверка тестов?

В коде урока создается модуль JS, содержащий несколько разных реализаций того кода, который вам нужно протестировать. Вот как будет выглядеть этот файл в случае тестирования функции last().

// обычно это файл functions.js

// Разные реализации функции last. Имена функций не важны.
// Только одна реализация верная (right1), остальные нет (fail1, fail2)

const functions = {
    right1: (items) => items[items.length - 1],
    fail1: () => 2,
    fail2: (items) => items[0],
};

// process.env содержит переменные окружения, среди них есть FUNCTION_VERSION, о ней ниже
// Смысл этой функции в том, чтобы вернуть нужную реализацию тестируемой функции из списка выше

const f = (name = process.env.FUNCTION_VERSION) => functions[name] || functions['right1'];
export default f;

В файле с тестами происходит импорт этого модуля:

import getFunction from './functions.js';

// Теперь функция называется как надо
const last = getFunction();

// Тут располагаются тесты.

Теперь самое интересное. В каждом упражнении есть файл Makefile, открыв который можно увидеть, как запускаются тесты:

test:
    suppressor pass 'node ./tests/collection.test.js'
    FUNCTION_VERSION=fail1 suppressor fail 'node ./tests/collection.test.js'
    FUNCTION_VERSION=fail2 suppressor fail 'node ./tests/collection.test.js'

В этом файле происходит запуск тестов для каждой из реализаций тестируемой функции из файла functions.js.

Наша проверочная система использует для запуска тестов специальную библиотеку Suppressor, которая может понять, успешно ли завершилось выполнение тестов. У неё есть два режима работы:

  • Suppressor проверяет, что тесты выполнились успешно. В такой проверке в ваши тесты подставляется правильная реализация функции, которая тестируется (без ошибок).
    # pass – проверяет, что тесты успешно выполнились
    # FUNCTION_VERSION – переменная окружения, которая содержит имя функции,
    # которое надо подставить для текущего запуска тестов
    
    FUNCTION_VERSION=right1 suppressor pass 'node ./tests/collection.test.js'
    
    # В таком случае getFunction вернет функцию (items) => items[items.length - 1]
    Аргумент pass говорит о том, что Suppressor ожидает успешное завершение тестов. Следующий аргумент — команда, которую надо запустить. В случае успеха будет выведено сообщение:
    Expected tests to pass, received tests passed
    Если тесты вместо успеха завершились с ошибкой, библиотека выведет следующее сообщение:
    Expected tests to pass, but error occurred. See output above.
    Это значит, что для текущего запуска тестов использовалась рабочая (правильная) версия кода, для которой тесты должны завершиться успешно. Если тесты провалились, значит они написаны неправильно.В Makefile в строке запуска может не быть переменной окружения FUNCTION_VERSION:
    suppressor pass 'node ./tests/collection.test.js'
    В таком случае Suppressor будет использовать правильную реализацию функции — right1.
  • Suppressor проверяет, что тесты упали с ошибкой. В этом запуске тесты выполняются с неправильной реализацией тестируемой функции. Правильно написанные тесты должны найти ошибку и сигнализировать об этом, то есть упасть.
    FUNCTION_VERSION=fail1 suppressor fail 'node ./tests/collection.test.js'
    # В таком случае getFunction вернет функцию (items) => 2
    Аргумент fail говорит о том, что Suppressor ожидает, что выполняемая команда завершится с ошибкой. Это будет значить, что тесты написаны верно, и библиотека выведет сообщение:
    Expected tests to fail, received tests failed
    Если команда завершилась успешно, библиотека выведет следующее сообщение:
    Expected tests to fail, but they passed. See output above.
    Это значит, что для текущего запуска тестов использовалась неверная (нерабочая) версия кода, для которой тесты должны были упасть (сигнализировать об ошибке). Если тесты завершились успешно, значит они написаны неправильно.