Перейти к содержанию

🚀 Где задеплоить проект?

Партнёрская ссылка. Дата-центры в РФ, оплата картой.

Запуск тестов

Стабильность: 2 – Стабильная

АПИ является удовлетворительным. Совместимость с NPM имеет высший приоритет и не будет нарушена кроме случаев явной необходимости.

Модуль node:test предназначен для написания и запуска JavaScript-тестов. Подключение:

1
import test from 'node:test';
1
const test = require('node:test');

Модуль доступен только в схеме node:.

Тест из модуля test — это одна функция, которая обрабатывается одним из трёх способов:

  1. Синхронная функция: провал при выброшенном исключении, успех иначе.
  2. Функция, возвращающая Promise: провал при отклонении промиса, успех при выполнении.
  3. Функция с колбэком: провал, если первый аргумент колбэка истинный; успех, если первый аргумент ложный. Если функция и принимает колбэк, и возвращает Promise, тест считается проваленным.

Ниже пример использования модуля test.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
test('synchronous passing test', (t) => {
    // Этот тест проходит: исключение не выбрасывается.
    assert.strictEqual(1, 1);
});

test('synchronous failing test', (t) => {
    // Этот тест падает: выбрасывается исключение.
    assert.strictEqual(1, 2);
});

test('asynchronous passing test', async (t) => {
    // Этот тест проходит: промис из async-функции выполнен, а не отклонён.
    assert.strictEqual(1, 1);
});

test('asynchronous failing test', async (t) => {
    // Этот тест падает: промис из async-функции отклонён.
    assert.strictEqual(1, 2);
});

test('failing test using Promises', (t) => {
    // Можно вернуть промис напрямую.
    return new Promise((resolve, reject) => {
        setImmediate(() => {
            reject(
                new Error(
                    'this will cause the test to fail'
                )
            );
        });
    });
});

test('callback passing test', (t, done) => {
    // done() — колбэк; когда сработает setImmediate(), он вызовет done() без аргументов.
    setImmediate(done);
});

test('callback failing test', (t, done) => {
    // Когда сработает setImmediate(), done() вызывается с Error — тест падает.
    setImmediate(() => {
        done(new Error('callback failure'));
    });
});

Если какой-либо тест провален, код выхода процесса устанавливается в 1.

Подтесты

Метод test() контекста теста позволяет создавать подтесты и выстраивать иерархию вложенных тестов внутри более крупного. Поведение совпадает с функцией test() верхнего уровня. Ниже — пример верхнего теста с двумя подтестами.

1
2
3
4
5
6
7
8
9
test('top level test', async (t) => {
    await t.test('subtest 1', (t) => {
        assert.strictEqual(1, 1);
    });

    await t.test('subtest 2', (t) => {
        assert.strictEqual(2, 2);
    });
});

Примечание: хуки beforeEach и afterEach вызываются между запусками подтестов.

Здесь await гарантирует завершение обоих подтестов: родительский тест сам по себе не ждёт подтесты (в отличие от тестов внутри suite). Незавершённые подтесты при завершении родителя отменяются и считаются провалом; провал подтеста ведёт к провалу родителя.

Повторный запуск проваленных тестов

Раннер может сохранять состояние прогона в файл и затем перезапускать только проваленные тесты без полного прогона. Укажите путь к файлу состояния ключом --test-rerun-failures; если файла нет, он будет создан.

Файл состояния — JSON с массивом попыток запуска. Каждая попытка — объект, сопоставляющий успешно прошедшие тесты с номером попытки, на которой они впервые прошли. Ключ — путь к файлу теста с номером строки и столбца определения теста. Если один и тот же тест в одной позиции запускается несколько раз (например в цикле), к ключу добавляется счётчик. Изменение порядка выполнения или места теста может сбить соответствие; --test-rerun-failures имеет смысл при детерминированном порядке тестов.

Пример файла состояния:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
[
    {
        "test.js:10:5": {
            "passed_on_attempt": 0,
            "name": "test 1"
        }
    },
    {
        "test.js:10:5": {
            "passed_on_attempt": 0,
            "name": "test 1"
        },
        "test.js:20:5": {
            "passed_on_attempt": 1,
            "name": "test 2"
        }
    }
]

В примере две попытки и два теста в test.js: первый прошёл с первой попытки, второй — со второй.

С опцией --test-rerun-failures выполняются только тесты, которые ещё ни разу не прошли успешно.

1
node --test-rerun-failures /path/to/state/file

Псевдонимы describe() и it()

Наборы и тесты можно оформлять через describe() и it(): describe() это псевдоним suite(), it() — псевдоним test().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
describe('A thing', () => {
    it('should work', () => {
        assert.strictEqual(1, 1);
    });

    it('should be ok', () => {
        assert.strictEqual(2, 2);
    });

    describe('a nested thing', () => {
        it('should work', () => {
            assert.strictEqual(3, 3);
        });
    });
});

describe и it импортируются из модуля node:test.

1
import { describe, it } from 'node:test';
1
const { describe, it } = require('node:test');

Пропуск тестов

Отдельные тесты можно пропустить, передав опцию skip в вызов test, либо вызвав метод контекста skip(), как в примере ниже.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// Используется опция skip без сообщения.
test('skip option', { skip: true }, (t) => {
    // Этот код не выполняется.
});

// Используется опция skip с сообщением.
test(
    'skip option with message',
    { skip: 'this is skipped' },
    (t) => {
        // Этот код не выполняется.
    }
);

test('skip() method', (t) => {
    // При необходимости верните управление здесь, если в тесте есть ещё логика.
    t.skip();
});

test('skip() method with message', (t) => {
    // При необходимости верните управление здесь, если в тесте есть ещё логика.
    t.skip('this is skipped');
});

Тесты TODO

Отдельные тесты можно пометить как нестабильные или незавершённые, передав опцию todo в вызов test или вызвав метод контекста todo(), как в примере ниже. Такие тесты означают отложенную реализацию или известную ошибку, которую нужно исправить. Тесты TODO выполняются, но не считаются провалом и не влияют на код выхода процесса. Если тест помечен и как TODO, и как пропущенный, опция TODO игнорируется.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// Используется опция todo без сообщения.
test('todo option', { todo: true }, (t) => {
    // Код выполняется, но не считается провалом.
    throw new Error('this does not fail the test');
});

// Используется опция todo с сообщением.
test(
    'todo option with message',
    { todo: 'this is a todo test' },
    (t) => {
        // Код выполняется.
    }
);

test('todo() method', (t) => {
    t.todo();
});

test('todo() method with message', (t) => {
    t.todo(
        'this is a todo test and is not treated as a failure'
    );
    throw new Error('this does not fail the test');
});

Ожидание падения тестов

Инвертирует отчёт об успехе/провале для отдельного теста или набора: помеченный тест считается пройденным только если выбрасывает исключение; если исключения нет — провал.

В примерах ниже doTheThing() не возвращает true, но тесты помечены expectFailure, поэтому проходят.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
it.expectFailure('should do the thing', () => {
    assert.strictEqual(doTheThing(), true);
});

it('should do the thing', { expectFailure: true }, () => {
    assert.strictEqual(doTheThing(), true);
});

it(
    'should do the thing',
    { expectFailure: 'feature not implemented' },
    () => {
        assert.strictEqual(doTheThing(), true);
    }
);

Если expectFailure имеет тип RegExp | Function | Object | Error, тест проходит только при выбросе совпадающего значения. См. assert.throws про обработку типов.

Следующие тесты падают несмотря на expectFailure, потому что ошибка не совпадает с ожидаемой.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
it(
    'fails because regex does not match',
    {
        expectFailure: /expected message/,
    },
    () => {
        throw new Error('different message');
    }
);

it(
    'fails because object matcher does not match',
    {
        expectFailure: { code: 'ERR_EXPECTED' },
    },
    () => {
        const err = new Error('boom');
        err.code = 'ERR_ACTUAL';
        throw err;
    }
);

Чтобы задать и причину, и шаблон ошибки для expectFailure, используйте { label, match }.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
it(
    'should fail with specific error and reason',
    {
        expectFailure: {
            label: 'reason for failure',
            match: /error message/,
        },
    },
    () => {
        assert.strictEqual(doTheThing(), true);
    }
);

skip и/или todo несовместимы с expectFailure; при совместном указании приоритет у skip и todo (skip сильнее остальных, todo сильнее expectFailure).

Эти тесты будут пропущены (не выполнятся):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
it.expectFailure(
    'should do the thing',
    { skip: true },
    () => {
        assert.strictEqual(doTheThing(), true);
    }
);

it.skip(
    'should do the thing',
    { expectFailure: true },
    () => {
        assert.strictEqual(doTheThing(), true);
    }
);

Эти тесты будут помечены как «todo» (ошибки подавляются):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
it.expectFailure(
    'should do the thing',
    { todo: true },
    () => {
        assert.strictEqual(doTheThing(), true);
    }
);

it.todo(
    'should do the thing',
    { expectFailure: true },
    () => {
        assert.strictEqual(doTheThing(), true);
    }
);

Тесты only

Если Node.js запущен с --test-only или отключена изоляция тестов, можно выполнить только выбранные тесты, передав им опцию only. У теста с only выполняются и все подтесты. Если only задан у набора, выполняются все тесты набора, кроме случая, когда у потомков тоже есть only — тогда только они.

При подтестах внутри test()/it() нужно пометить only всех предков, чтобы запустить только часть подтестов.

Метод контекста runOnly() даёт то же на уровне подтестов. Невыполненные тесты не попадают в вывод раннера.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// Предположим, что Node.js запущен с опцией командной строки --test-only.
// Задана опция 'only' набора, поэтому эти тесты выполняются.
test('this test is run', { only: true }, async (t) => {
    // В этом тесте по умолчанию выполняются все подтесты.
    await t.test('running subtest');

    // Контекст можно обновить, чтобы подтесты запускались с опцией 'only'.
    t.runOnly(true);
    await t.test('this subtest is now skipped');
    await t.test('this subtest is run', { only: true });

    // Снова переключаем контекст, чтобы выполнялись все тесты.
    t.runOnly(false);
    await t.test('this subtest is now run');

    // Явно не запускаем эти тесты.
    await t.test('skipped subtest 3', { only: false });
    await t.test('skipped subtest 4', { skip: true });
});

// Опция 'only' не задана — этот тест пропускается.
test('this test is not run', () => {
    // Этот код не выполняется.
    throw new Error('fail');
});

describe('a suite', () => {
    // Задана опция 'only' — этот тест выполняется.
    it('this test is run', { only: true }, () => {
        // Этот код выполняется.
    });

    it('this test is not run', () => {
        // Этот код не выполняется.
        throw new Error('fail');
    });
});

describe.only('a suite', () => {
    // Задана опция 'only' — этот тест выполняется.
    it('this test is run', () => {
        // Этот код выполняется.
    });

    it('this test is run', () => {
        // Этот код выполняется.
    });
});

Фильтрация тестов по имени

Опция командной строки --test-name-pattern позволяет запускать только те тесты, имя которых соответствует заданному шаблону; опция --test-skip-pattern — пропускать тесты, имя которых соответствует шаблону. Шаблоны имён интерпретируются как регулярные выражения JavaScript. Опции --test-name-pattern и --test-skip-pattern можно указывать несколько раз, в том числе для вложенных тестов. Для каждого выполняемого теста также выполняются соответствующие хуки, например beforeEach(). Тесты, которые не выполняются, не попадают в вывод раннера.

Для приведённого ниже файла тестов запуск Node.js с --test-name-pattern="test [1-3]" приведёт к выполнению test 1, test 2 и test 3. Если test 1 не совпал с шаблоном имени, его подтесты не выполнятся, даже если сами подходят под шаблон. Тот же набор тестов можно выполнить, передав --test-name-pattern несколько раз (например --test-name-pattern="test 1", --test-name-pattern="test 2" и т. д.).

1
2
3
4
5
6
7
8
9
test('test 1', async (t) => {
    await t.test('test 2');
    await t.test('test 3');
});

test('Test 4', async (t) => {
    await t.test('Test 5');
    await t.test('test 6');
});

Шаблоны имён можно задавать в виде литералов регулярных выражений — тогда доступны флаги RegExp. В предыдущем примере запуск Node.js с --test-name-pattern="/test [4-5]/i" (или --test-skip-pattern="/test [4-5]/i") даст совпадение с Test 4 и Test 5, так как шаблон без учёта регистра.

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

1
2
3
4
5
6
7
describe('test 1', (t) => {
    it('some test');
});

describe('test 2', (t) => {
    it('some test');
});

Запуск Node.js с --test-name-pattern="test 1 some test" сопоставит только some test внутри test 1.

Шаблоны имён не меняют набор файлов, которые выполняет раннер.

Если заданы и --test-name-pattern, и --test-skip-pattern, тест должен удовлетворять обоим условиям, чтобы быть выполненным.

Посторонняя асинхронная активность

Как только функция теста завершает работу, результаты сообщаются как можно скорее, сохраняя порядок тестов. При этом тест может породить асинхронную активность, которая переживает сам тест. Раннер обрабатывает такую активность, но не откладывает отчёт о результатах ради неё.

В примере ниже тест завершается, когда два вызова setImmediate() ещё не выполнены. Первый setImmediate() пытается создать новый подтест. Поскольку родительский тест уже завершился и вывел результаты, новый подтест сразу помечается как проваленный и позже попадает в TestsStream.

Второй setImmediate() создаёт событие uncaughtException. События uncaughtException и unhandledRejection, исходящие от уже завершённого теста, модуль test помечает как провал и передаёт в TestsStream как диагностические предупреждения верхнего уровня.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
test('a test that creates asynchronous activity', (t) => {
    setImmediate(() => {
        t.test('subtest that is created too late', (t) => {
            throw new Error('error1');
        });
    });

    setImmediate(() => {
        throw new Error('error2');
    });

    // Тест завершается после этой строки.
});

Режим наблюдения (watch)

Стабильность: 1 — экспериментально

Раннер тестов Node.js поддерживает режим наблюдения при передаче флага --watch:

1
node --test --watch

В режиме watch раннер отслеживает изменения в тестовых файлах и зависимостях. При обнаружении изменений перезапускаются затронутые тесты. Работа продолжается, пока процесс не будет завершён.

Глобальная подготовка и завершение

Стабильность: 1.0 — ранняя разработка

Можно указать модуль, который выполняется до всех тестов и задаёт глобальное состояние или фикстуры. Удобно для подготовки ресурсов или общего состояния, нужного нескольким тестам.

Модуль может экспортировать:

  • функцию globalSetup — выполняется один раз перед стартом всех тестов;
  • функцию globalTeardown — выполняется один раз после завершения всех тестов.

Путь к модулю задаётся флагом --test-global-setup при запуске тестов из командной строки.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
// файл setup-module.js
async function globalSetup() {
  // Общие ресурсы, состояние или окружение
  console.log('Global setup executed');
  // Серверы, файлы, БД и т.д.
}

async function globalTeardown() {
  // Очистка ресурсов, состояния или окружения
  console.log('Global teardown executed');
  // Остановка серверов, удаление файлов, отключение от БД и т.д.
}

module.exports = { globalSetup, globalTeardown };
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// файл setup-module.mjs
export async function globalSetup() {
  // Общие ресурсы, состояние или окружение
  console.log('Global setup executed');
  // Серверы, файлы, БД и т.д.
}

export async function globalTeardown() {
  // Очистка ресурсов, состояния или окружения
  console.log('Global teardown executed');
  // Остановка серверов, удаление файлов, отключение от БД и т.д.
}

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

Запуск тестов из командной строки

Раннер тестов Node.js вызывается из командной строки с флагом --test:

1
node --test

По умолчанию Node.js выполняет все файлы, подходящие под шаблоны:

  • **/*.test.{cjs,mjs,js}
  • **/*-test.{cjs,mjs,js}
  • **/*_test.{cjs,mjs,js}
  • **/test-*.{cjs,mjs,js}
  • **/test.{cjs,mjs,js}
  • **/test/**/*.{cjs,mjs,js}

Если не указан --no-strip-types, дополнительно учитываются:

  • **/*.test.{cts,mts,ts}
  • **/*-test.{cts,mts,ts}
  • **/*_test.{cts,mts,ts}
  • **/test-*.{cts,mts,ts}
  • **/test.{cts,mts,ts}
  • **/test/**/*.{cts,mts,ts}

Вместо этого в конце команды Node.js можно передать один или несколько шаблонов glob, как показано ниже. Поведение glob соответствует glob(7). Шаблоны glob в командной строке лучше заключать в двойные кавычки, чтобы оболочка не раскрывала их — так выше переносимость между системами.

1
node --test "**/*.test.js" "**/*.spec.js"

Случайный порядок выполнения тестов

Стабильность: 1.0 — ранняя разработка

Раннер может перемешивать порядок выполнения, чтобы выявлять тесты, зависящие от порядка. В этом режиме перемешиваются и найденные файлы тестов, и очередь тестов внутри файла. Включение: --test-randomize.

1
node --test --test-randomize

При включённой рандомизации раннер выводит диагностическое сообщение с seed прогона:

1
Randomized test order seed: 12345

--test-random-seed=<число> воспроизводит тот же порядок. Указание --test-random-seed также включает рандомизацию, поэтому при заданном seed флаг --test-randomize необязателен:

1
node --test --test-random-seed=12345

В большинстве файлов рандомизация работает автоматически. Важное исключение — последовательное await подтестов: каждый следующий стартует после завершения предыдущего, и раннер сохраняет порядок объявления вместо перемешивания.

Пример: выполняется строго по очереди и не рандомизируется.

1
2
3
4
5
6
7
8
import test from 'node:test';

test('math', async (t) => {
  for (const name of ['adds', 'subtracts', 'multiplies']) {
    // Последовательный await подтестов сохраняет порядок объявления.
    await t.test(name, async () => {});
  }
});
1
2
3
4
5
6
7
8
const test = require('node:test');

test('math', async (t) => {
  for (const name of ['adds', 'subtracts', 'multiplies']) {
    // Последовательный await подтестов сохраняет порядок объявления.
    await t.test(name, async () => {});
  }
});

API в стиле suite (describe()/it() или suite()/test()) по-прежнему допускают рандомизацию: соседние тесты ставятся в очередь вместе.

Пример: этот вариант остаётся подходящим для рандомизации.

1
2
3
4
5
6
7
import { describe, it } from 'node:test';

describe('math', () => {
  it('adds', () => {});
  it('subtracts', () => {});
  it('multiplies', () => {});
});
1
2
3
4
5
6
7
const { describe, it } = require('node:test');

describe('math', () => {
  it('adds', () => {});
  it('subtracts', () => {});
  it('multiplies', () => {});
});

--test-randomize и --test-random-seed несовместимы с режимом --watch.

Подходящие файлы выполняются как тестовые. Подробнее — в разделе модель выполнения раннера тестов.

Модель выполнения раннера тестов

При включённой изоляции тестов на уровне процесса каждый подходящий файл теста выполняется в отдельном дочернем процессе. Максимальное число одновременных дочерних процессов задаёт --test-concurrency. Если дочерний процесс завершился с кодом 0, тест считается пройденным; иначе — провалом. Файлы должны быть исполняемы Node.js, но не обязаны использовать внутри модуль node:test.

Каждый файл выполняется как обычный скрипт: если в нём через node:test объявлены тесты, они выполняются в одном потоке приложения, независимо от опции concurrency у test().

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

Наследование опций дочерним процессом

В режиме изоляции процессов (по умолчанию) дочерние процессы наследуют опции Node.js от родителя, в том числе из файлов конфигурации. Часть флагов отфильтрована для корректной работы раннера:

  • --test — запрещён, чтобы избежать рекурсивного запуска тестов
  • --experimental-test-coverage — управляет раннер
  • --watch — режим watch на уровне родителя
  • --experimental-default-config-file — загрузка конфигурации на уровне родителя
  • --test-reporter — отчётность на уровне родителя
  • --test-reporter-destination — назначения вывода задаёт родитель
  • --experimental-config-file — пути к конфигу на уровне родителя
  • --test-randomize — рандомизацию задаёт родитель и передаёт дочерним процессам
  • --test-random-seed — seed рандомизации задаёт родитель и передаёт дочерним процессам

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

Сбор покрытия кода

Стабильность: 1 — экспериментально

При запуске Node.js с флагом --experimental-test-coverage собирается покрытие кода; после завершения всех тестов выводится статистика. Если переменная окружения NODE_V8_COVERAGE задаёт каталог покрытия, файлы V8 записываются туда. Модули ядра Node.js и файлы в node_modules/ по умолчанию не входят в отчёт; их можно явно включить флагом --test-coverage-include. По умолчанию все подходящие тестовые файлы исключаются из отчёта о покрытии; исключения можно переопределить --test-coverage-exclude. При включённом покрытии отчёт передаётся репортёрам тестов через событие 'test:coverage'.

Покрытие для набора строк можно отключить такими комментариями:

1
2
3
4
5
6
7
/* node:coverage disable */
if (anAlwaysFalseCondition) {
    // Код в этой ветке не выполнится, но строки не учитываются в покрытии.
    // Все строки после 'disable' игнорируются до парного комментария 'enable'.
    console.log('this is never executed');
}
/* node:coverage enable */

Покрытие можно отключить на заданное число строк; затем оно снова включается автоматически. Если число строк не указано, игнорируется одна строка.

1
2
3
4
5
6
7
8
9
/* node:coverage ignore next */
if (anAlwaysFalseCondition) {
    console.log('this is never executed');
}

/* node:coverage ignore next 3 */
if (anAlwaysFalseCondition) {
    console.log('this is never executed');
}

Репортёры покрытия

Репортёры tap и spec выводят сводку по покрытию. Также есть репортёр lcov, создающий файл lcov для детального отчёта.

1
node --test --experimental-test-coverage --test-reporter=lcov --test-reporter-destination=lcov.info
  • Этот репортёр не выводит результаты самих тестов.
  • Его лучше использовать вместе с другим репортёром.

Подмены (mocking)

Модуль node:test поддерживает подмены через объект верхнего уровня mock. В примере создаётся шпион для функции сложения двух чисел; затем проверяется, что функция вызывалась ожидаемо.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import assert from 'node:assert';
import { mock, test } from 'node:test';

test('spies on a function', () => {
  const sum = mock.fn((a, b) => {
    return a + b;
  });

  assert.strictEqual(sum.mock.callCount(), 0);
  assert.strictEqual(sum(3, 4), 7);
  assert.strictEqual(sum.mock.callCount(), 1);

  const call = sum.mock.calls[0];
  assert.deepStrictEqual(call.arguments, [3, 4]);
  assert.strictEqual(call.result, 7);
  assert.strictEqual(call.error, undefined);

  // Сброс глобально отслеживаемых подмен.
  mock.reset();
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
'use strict';
const assert = require('node:assert');
const { mock, test } = require('node:test');

test('spies on a function', () => {
  const sum = mock.fn((a, b) => {
    return a + b;
  });

  assert.strictEqual(sum.mock.callCount(), 0);
  assert.strictEqual(sum(3, 4), 7);
  assert.strictEqual(sum.mock.callCount(), 1);

  const call = sum.mock.calls[0];
  assert.deepStrictEqual(call.arguments, [3, 4]);
  assert.strictEqual(call.result, 7);
  assert.strictEqual(call.error, undefined);

  // Сброс глобально отслеживаемых подмен.
  mock.reset();
});

Та же функциональность доступна на объекте TestContext каждого теста. В примере ниже шпион создаётся для метода объекта через API контекста. Плюс подмен через контекст: раннер сам восстанавливает подменённое поведение после завершения теста.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
test('spies on an object method', (t) => {
    const number = {
        value: 5,
        add(a) {
            return this.value + a;
        },
    };

    t.mock.method(number, 'add');
    assert.strictEqual(number.add.mock.callCount(), 0);
    assert.strictEqual(number.add(3), 8);
    assert.strictEqual(number.add.mock.callCount(), 1);

    const call = number.add.mock.calls[0];

    assert.deepStrictEqual(call.arguments, [3]);
    assert.strictEqual(call.result, 8);
    assert.strictEqual(call.target, undefined);
    assert.strictEqual(call.this, number);
});

Таймеры

Подмена таймеров — распространённый приём: имитация и управление setInterval и setTimeout без реального ожидания интервалов.

Полный список методов и возможностей — в классе MockTimers.

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

Ниже показана подмена setTimeout. Вызов .enable({ apis: ['setTimeout'] }); подменяет setTimeout в модулях node:timers и node:timers/promises, а также глобальный setTimeout в Node.js.

Примечание: деструктуризация вроде import [setTimeout](timers.md#settimeoutcallback-delay-args) from 'node:timers' в этой API пока не поддерживается.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
import assert from 'node:assert';
import { mock, test } from 'node:test';

test('mocks setTimeout to be executed synchronously without having to actually wait for it', () => {
  const fn = mock.fn();

  // При необходимости выберите, что подменять
  mock.timers.enable({ apis: ['setTimeout'] });
  setTimeout(fn, 9999);
  assert.strictEqual(fn.mock.callCount(), 0);

  // Сдвиг времени
  mock.timers.tick(9999);
  assert.strictEqual(fn.mock.callCount(), 1);

  // Сброс глобально отслеживаемых подмен
  mock.timers.reset();

  // Вызов сброса экземпляра mock также сбрасывает таймеры
  mock.reset();
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
const assert = require('node:assert');
const { mock, test } = require('node:test');

test('mocks setTimeout to be executed synchronously without having to actually wait for it', () => {
  const fn = mock.fn();

  // При необходимости выберите, что подменять
  mock.timers.enable({ apis: ['setTimeout'] });
  setTimeout(fn, 9999);
  assert.strictEqual(fn.mock.callCount(), 0);

  // Сдвиг времени
  mock.timers.tick(9999);
  assert.strictEqual(fn.mock.callCount(), 1);

  // Сброс глобально отслеживаемых подмен
  mock.timers.reset();

  // Вызов сброса экземпляра mock также сбрасывает таймеры
  mock.reset();
});

Та же функциональность доступна в свойстве mock у TestContext. При подмене через контекст раннер после теста автоматически восстанавливает подменённые таймеры.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
import assert from 'node:assert';
import { test } from 'node:test';

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
  const fn = context.mock.fn();

  // При необходимости выберите, что подменять
  context.mock.timers.enable({ apis: ['setTimeout'] });
  setTimeout(fn, 9999);
  assert.strictEqual(fn.mock.callCount(), 0);

  // Сдвиг времени
  context.mock.timers.tick(9999);
  assert.strictEqual(fn.mock.callCount(), 1);
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const assert = require('node:assert');
const { test } = require('node:test');

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
  const fn = context.mock.fn();

  // При необходимости выберите, что подменять
  context.mock.timers.enable({ apis: ['setTimeout'] });
  setTimeout(fn, 9999);
  assert.strictEqual(fn.mock.callCount(), 0);

  // Сдвиг времени
  context.mock.timers.tick(9999);
  assert.strictEqual(fn.mock.callCount(), 1);
});

Дата

API подмены таймеров позволяет подменять и объект Date — удобно для тестов, зависящих от времени, и для симуляции Date.now() и т. п.

Реализация даты тоже входит в MockTimers — см. там полный список методов.

Примечание: при совместной подмене Date и таймеров они связаны: сдвиг времени двигает и подменённую дату — имитируется один общий внутренний таймер.

Ниже — подмена Date и текущее значение Date.now().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import assert from 'node:assert';
import { test } from 'node:test';

test('mocks the Date object', (context) => {
  // При необходимости выберите, что подменять
  context.mock.timers.enable({ apis: ['Date'] });
  // Если не задано, начальная дата — 0 в эпохе UNIX
  assert.strictEqual(Date.now(), 0);

  // Сдвиг времени также двигает дату
  context.mock.timers.tick(9999);
  assert.strictEqual(Date.now(), 9999);
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const assert = require('node:assert');
const { test } = require('node:test');

test('mocks the Date object', (context) => {
  // При необходимости выберите, что подменять
  context.mock.timers.enable({ apis: ['Date'] });
  // Если не задано, начальная дата — 0 в эпохе UNIX
  assert.strictEqual(Date.now(), 0);

  // Сдвиг времени также двигает дату
  context.mock.timers.tick(9999);
  assert.strictEqual(Date.now(), 9999);
});

Если начальная эпоха не задана, дата берётся от 0 в Unix-эпохе (1 января 1970, 00:00:00 UTC). Начальное время можно задать свойством now в вызове .enable() — оно станет стартовым для подменённого Date (положительное целое или другой объект Date).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
import assert from 'node:assert';
import { test } from 'node:test';

test('mocks the Date object with initial time', (context) => {
  // При необходимости выберите, что подменять
  context.mock.timers.enable({ apis: ['Date'], now: 100 });
  assert.strictEqual(Date.now(), 100);

  // Сдвиг времени также двигает дату
  context.mock.timers.tick(200);
  assert.strictEqual(Date.now(), 300);
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const assert = require('node:assert');
const { test } = require('node:test');

test('mocks the Date object with initial time', (context) => {
  // При необходимости выберите, что подменять
  context.mock.timers.enable({ apis: ['Date'], now: 100 });
  assert.strictEqual(Date.now(), 100);

  // Сдвиг времени также двигает дату
  context.mock.timers.tick(200);
  assert.strictEqual(Date.now(), 300);
});

Метод .setTime() вручную переносит подменённую дату; принимается только неотрицательное целое.

Примечание: метод не выполняет подменённые таймеры, которые оказались «в прошлом» относительно нового времени.

В примере ниже задаётся новое время для подменённой даты.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import assert from 'node:assert';
import { test } from 'node:test';

test('sets the time of a date object', (context) => {
  // При необходимости выберите, что подменять
  context.mock.timers.enable({ apis: ['Date'], now: 100 });
  assert.strictEqual(Date.now(), 100);

  // Сдвиг времени также двигает дату
  context.mock.timers.setTime(1000);
  context.mock.timers.tick(200);
  assert.strictEqual(Date.now(), 1200);
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
const assert = require('node:assert');
const { test } = require('node:test');

test('sets the time of a date object', (context) => {
  // При необходимости выберите, что подменять
  context.mock.timers.enable({ apis: ['Date'], now: 100 });
  assert.strictEqual(Date.now(), 100);

  // Сдвиг времени также двигает дату
  context.mock.timers.setTime(1000);
  context.mock.timers.tick(200);
  assert.strictEqual(Date.now(), 1200);
});

Таймеры, запланированные «в прошлом», при вызове setTime() не срабатывают. Чтобы выполнить их, дальше двигайте время через .tick().

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import assert from 'node:assert';
import { test } from 'node:test';

test('setTime does not execute timers', (context) => {
  // При необходимости выберите, что подменять
  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
  const fn = context.mock.fn();
  setTimeout(fn, 1000);

  context.mock.timers.setTime(800);
  // Таймер не сработал — время ещё не достигнуто
  assert.strictEqual(fn.mock.callCount(), 0);
  assert.strictEqual(Date.now(), 800);

  context.mock.timers.setTime(1200);
  // Таймер всё ещё не сработал
  assert.strictEqual(fn.mock.callCount(), 0);
  // Сдвиг времени для срабатывания таймера
  context.mock.timers.tick(0);
  assert.strictEqual(fn.mock.callCount(), 1);
  assert.strictEqual(Date.now(), 1200);
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const assert = require('node:assert');
const { test } = require('node:test');

test('runs timers as setTime passes ticks', (context) => {
  // При необходимости выберите, что подменять
  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
  const fn = context.mock.fn();
  setTimeout(fn, 1000);

  context.mock.timers.setTime(800);
  // Таймер не сработал — время ещё не достигнуто
  assert.strictEqual(fn.mock.callCount(), 0);
  assert.strictEqual(Date.now(), 800);

  context.mock.timers.setTime(1200);
  // Таймер сработал — время достигнуто
  assert.strictEqual(fn.mock.callCount(), 1);
  assert.strictEqual(Date.now(), 1200);
});

.runAll() выполняет все таймеры в очереди и сдвигает подменённую дату до времени последнего выполненного таймера, как если бы время прошло.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import assert from 'node:assert';
import { test } from 'node:test';

test('runs timers as setTime passes ticks', (context) => {
  // При необходимости выберите, что подменять
  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
  const fn = context.mock.fn();
  setTimeout(fn, 1000);
  setTimeout(fn, 2000);
  setTimeout(fn, 3000);

  context.mock.timers.runAll();
  // Все таймеры сработали — время достигнуто
  assert.strictEqual(fn.mock.callCount(), 3);
  assert.strictEqual(Date.now(), 3000);
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const assert = require('node:assert');
const { test } = require('node:test');

test('runs timers as setTime passes ticks', (context) => {
  // При необходимости выберите, что подменять
  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
  const fn = context.mock.fn();
  setTimeout(fn, 1000);
  setTimeout(fn, 2000);
  setTimeout(fn, 3000);

  context.mock.timers.runAll();
  // Все таймеры сработали — время достигнуто
  assert.strictEqual(fn.mock.callCount(), 3);
  assert.strictEqual(Date.now(), 3000);
});

Снимки (snapshot testing)

Добавлено в: v22.3.0

Тесты-снимки сериализуют произвольные значения в строки и сравнивают их с эталоном. Эталонные значения называются снимками и хранятся в файле снимков; раннер им управляет, но формат остаётся читаемым для отладки. Файлы снимков обычно коммитят вместе с тестами.

Файлы снимков создаются при запуске Node.js с флагом --test-update-snapshots. На каждый тестовый файл — отдельный файл снимка. По умолчанию имя совпадает с тестом и расширением .snapshot; поведение настраивается через snapshot.setResolveSnapshotPath(). Каждое утверждение снимка соответствует экспорту в файле.

Пример ниже при первом запуске провалится: файла снимка ещё нет.

1
2
3
4
5
6
7
// файл test.js
suite('suite of snapshot tests', () => {
    test('snapshot test', (t) => {
        t.assert.snapshot({ value1: 1, value2: 2 });
        t.assert.snapshot(5);
    });
});

Сгенерируйте файл снимка, запустив тест с --test-update-snapshots. Тест должен пройти, рядом появится test.js.snapshot. Ниже — пример содержимого: каждый снимок помечен полным именем теста и счётчиком, если в одном тесте несколько снимков.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
exports[`suite of snapshot tests > snapshot test 1`] = `
{
  "value1": 1,
  "value2": 2
}
`;

exports[`suite of snapshot tests > snapshot test 2`] = `
5
`;

После создания файла снимка запустите тесты без --test-update-snapshots — они должны проходить.

Репортёры тестов

В модуле node:test можно передать флаги --test-reporter, чтобы раннер использовал нужный репортёр.

Встроенные репортёры:

  • spec Выводит результаты в удобочитаемом виде. Репортёр по умолчанию.

  • tap Формат TAP.

  • dot Компактный вывод: успешный тест — ., провал — X.

  • junit jUnit XML.

  • lcov Покрытие кода; используется с --experimental-test-coverage.

Точный формат вывода может меняться между версиями Node.js и не гарантирован для программного разбора. Для программного доступа к результатам подписывайтесь на события TestsStream.

Репортёры импортируются из node:test/reporters:

1
import { tap, spec, dot, junit, lcov } from 'node:test/reporters';
1
const { tap, spec, dot, junit, lcov } = require('node:test/reporters');

Пользовательские репортёры

--test-reporter может указывать путь к своему репортёру. Это модуль, экспортирующий значение, допустимое для stream.compose. Репортёр преобразует события TestsStream.

Пример репортёра на базе stream.Transform:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import { Transform } from 'node:stream';

const customReporter = new Transform({
  writableObjectMode: true,
  transform(event, encoding, callback) {
    switch (event.type) {
      case 'test:dequeue':
        callback(null, `test ${event.data.name} dequeued`);
        break;
      case 'test:enqueue':
        callback(null, `test ${event.data.name} enqueued`);
        break;
      case 'test:watch:drained':
        callback(null, 'test watch queue drained');
        break;
      case 'test:watch:restarted':
        callback(null, 'test watch restarted due to file change');
        break;
      case 'test:start':
        callback(null, `test ${event.data.name} started`);
        break;
      case 'test:pass':
        callback(null, `test ${event.data.name} passed`);
        break;
      case 'test:fail':
        callback(null, `test ${event.data.name} failed`);
        break;
      case 'test:plan':
        callback(null, 'test plan');
        break;
      case 'test:diagnostic':
      case 'test:stderr':
      case 'test:stdout':
        callback(null, event.data.message);
        break;
      case 'test:coverage': {
        const { totalLineCount } = event.data.summary.totals;
        callback(null, `total line count: ${totalLineCount}\n`);
        break;
      }
    }
  },
});

export default customReporter;
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const { Transform } = require('node:stream');

const customReporter = new Transform({
  writableObjectMode: true,
  transform(event, encoding, callback) {
    switch (event.type) {
      case 'test:dequeue':
        callback(null, `test ${event.data.name} dequeued`);
        break;
      case 'test:enqueue':
        callback(null, `test ${event.data.name} enqueued`);
        break;
      case 'test:watch:drained':
        callback(null, 'test watch queue drained');
        break;
      case 'test:watch:restarted':
        callback(null, 'test watch restarted due to file change');
        break;
      case 'test:start':
        callback(null, `test ${event.data.name} started`);
        break;
      case 'test:pass':
        callback(null, `test ${event.data.name} passed`);
        break;
      case 'test:fail':
        callback(null, `test ${event.data.name} failed`);
        break;
      case 'test:plan':
        callback(null, 'test plan');
        break;
      case 'test:diagnostic':
      case 'test:stderr':
      case 'test:stdout':
        callback(null, event.data.message);
        break;
      case 'test:coverage': {
        const { totalLineCount } = event.data.summary.totals;
        callback(null, `total line count: ${totalLineCount}\n`);
        break;
      }
    }
  },
});

module.exports = customReporter;

Пример репортёра на генераторе:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
export default async function * customReporter(source) {
  for await (const event of source) {
    switch (event.type) {
      case 'test:dequeue':
        yield `test ${event.data.name} dequeued\n`;
        break;
      case 'test:enqueue':
        yield `test ${event.data.name} enqueued\n`;
        break;
      case 'test:watch:drained':
        yield 'test watch queue drained\n';
        break;
      case 'test:watch:restarted':
        yield 'test watch restarted due to file change\n';
        break;
      case 'test:start':
        yield `test ${event.data.name} started\n`;
        break;
      case 'test:pass':
        yield `test ${event.data.name} passed\n`;
        break;
      case 'test:fail':
        yield `test ${event.data.name} failed\n`;
        break;
      case 'test:plan':
        yield 'test plan\n';
        break;
      case 'test:diagnostic':
      case 'test:stderr':
      case 'test:stdout':
        yield `${event.data.message}\n`;
        break;
      case 'test:coverage': {
        const { totalLineCount } = event.data.summary.totals;
        yield `total line count: ${totalLineCount}\n`;
        break;
      }
    }
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
module.exports = async function * customReporter(source) {
  for await (const event of source) {
    switch (event.type) {
      case 'test:dequeue':
        yield `test ${event.data.name} dequeued\n`;
        break;
      case 'test:enqueue':
        yield `test ${event.data.name} enqueued\n`;
        break;
      case 'test:watch:drained':
        yield 'test watch queue drained\n';
        break;
      case 'test:watch:restarted':
        yield 'test watch restarted due to file change\n';
        break;
      case 'test:start':
        yield `test ${event.data.name} started\n`;
        break;
      case 'test:pass':
        yield `test ${event.data.name} passed\n`;
        break;
      case 'test:fail':
        yield `test ${event.data.name} failed\n`;
        break;
      case 'test:plan':
        yield 'test plan\n';
        break;
      case 'test:diagnostic':
      case 'test:stderr':
      case 'test:stdout':
        yield `${event.data.message}\n`;
        break;
      case 'test:coverage': {
        const { totalLineCount } = event.data.summary.totals;
        yield `total line count: ${totalLineCount}\n`;
        break;
      }
    }
  }
};

Значение для --test-reporter — строка в том же виде, что аргумент динамического import(), или значение, как у --import.

Несколько репортёров

Флаг --test-reporter можно указать несколько раз, чтобы получить вывод в нескольких форматах. Тогда для каждого репортёра нужно задать назначение через --test-reporter-destination: stdout, stderr или путь к файлу. Пары «репортёр — назначение» сопоставляются по порядку следования.

В примере spec пишет в stdout, а dot — в file.txt:

1
node --test-reporter=spec --test-reporter=dot --test-reporter-destination=stdout --test-reporter-destination=file.txt

Если указан один репортёр, назначение по умолчанию — stdout, пока не задано явно.

run([options])

  • options <Object> Параметры конфигурации запуска тестов. Поддерживаются следующие свойства:
    • concurrency <number> | <boolean> Если указано число, параллельно выполняется столько процессов с тестами; каждый процесс соответствует одному тестовому файлу. Если true, параллельно запускается os.availableParallelism() - 1 тестовых файлов. Если false, за раз выполняется только один тестовый файл. По умолчанию: false.
    • cwd <string> Текущий рабочий каталог для раннера тестов. Используется как база для разрешения путей к файлам, как при запуске тестов из командной строки из этого каталога. По умолчанию: process.cwd().
    • files <Array> Массив путей к запускаемым файлам. По умолчанию: как при запуске тестов из командной строки.
    • forceExit <boolean> Завершать процесс после того, как все известные тесты выполнены, даже если цикл событий иначе оставался бы активным. По умолчанию: false.
    • globPatterns <Array> Массив glob-шаблонов для подбора тестовых файлов. Нельзя использовать вместе с files. По умолчанию: как при запуске тестов из командной строки.
    • inspectPort <number> | <Function> Порт инспектора для дочернего процесса с тестами. Может быть числом или функцией без аргументов, возвращающей число. Если передано пустое значение, у каждого процесса свой порт, увеличивающийся от process.debugPort родителя. Опция игнорируется, если isolation равно 'none' — дочерние процессы не создаются. По умолчанию: undefined.
    • isolation <string> Тип изоляции тестов. При значении 'process' каждый тестовый файл выполняется в отдельном дочернем процессе. При 'none' все тестовые файлы выполняются в текущем процессе. По умолчанию: 'process'.
    • only <boolean> Если истинно, контекст выполняет только тесты с установленной опцией only.
    • setup <Function> Функция, принимающая экземпляр TestsStream и позволяющая зарегистрировать обработчики до запуска тестов. По умолчанию: undefined.
    • execArgv <Array> Массив флагов CLI для передачи исполняемому файлу node при порождении дочерних процессов. Не действует при isolation равном 'none'. По умолчанию: []
    • argv <Array> Массив флагов CLI для передачи каждому тестовому файлу при порождении дочерних процессов. Не действует при isolation равном 'none'. По умолчанию: [].
    • signal <AbortSignal> Позволяет прервать выполняющийся прогон тестов.
    • testNamePatterns <string> | <RegExp> | <Array> Строка, RegExp или массив RegExp — выполнять только тесты, имя которых совпадает с шаблоном. Шаблоны имён интерпретируются как регулярные выражения JavaScript. Для каждого выполняемого теста вызываются и соответствующие хуки, например beforeEach(). По умолчанию: undefined.
    • testSkipPatterns <string> | <RegExp> | <Array> Строка, RegExp или массив RegExp — исключать тесты, имя которых совпадает с шаблоном. Шаблоны имён интерпретируются как регулярные выражения JavaScript. Для каждого выполняемого теста вызываются и соответствующие хуки, например beforeEach(). По умолчанию: undefined.
    • timeout <number> Через столько миллисекунд выполнение тестов считается проваленным. Если не задано, подтесты наследуют значение от родителя. По умолчанию: Infinity.
    • watch <boolean> Включить режим watch или нет. По умолчанию: false.
    • shard <Object> Запуск части тестов (шард). По умолчанию: undefined.
      • index <number> — положительное целое от 1 до <total>; индекс выполняемого шарда. Параметр обязателен.
      • total <number> — положительное целое: на сколько шардов делятся тестовые файлы. Параметр обязателен.
    • randomize <boolean> Перемешивать порядок тестовых файлов и очередь тестов. Несовместимо с watch: true. По умолчанию: false.
    • randomSeed <number> Зерно для перемешивания порядка. Если задано, прогоны можно повторять с тем же порядком, а указание опции также включает рандомизацию. Значение — целое от 0 до 4294967295. По умолчанию: undefined.
    • rerunFailuresFilePath <string> Путь к файлу, в котором раннер сохраняет состояние тестов, чтобы при следующем запуске выполнить только проваленные тесты; подробнее см. Повторный запуск проваленных тестов. По умолчанию: undefined.
    • coverage <boolean> Включить сбор покрытия кода. По умолчанию: false.
    • coverageExcludeGlobs <string> | <Array> Исключить файлы из покрытия по glob-шаблону (абсолютные и относительные пути). Учитывается только при coverage: true. Если заданы и coverageExcludeGlobs, и coverageIncludeGlobs, в отчёт попадают файлы, удовлетворяющие обоим условиям. По умолчанию: undefined.
    • coverageIncludeGlobs <string> | <Array> Явно включить файлы в покрытие по glob-шаблону (абсолютные и относительные пути). Учитывается только при coverage: true. Если заданы и coverageExcludeGlobs, и coverageIncludeGlobs, в отчёт попадают файлы, удовлетворяющие обоим условиям. По умолчанию: undefined.
    • lineCoverage <number> Минимальный процент покрытых строк. Если покрытие ниже порога, процесс завершится с кодом 1. По умолчанию: 0.
    • branchCoverage <number> Минимальный процент покрытых ветвлений. Если покрытие ниже порога, процесс завершится с кодом 1. По умолчанию: 0.
    • functionCoverage <number> Минимальный процент покрытых функций. Если покрытие ниже порога, процесс завершится с кодом 1. По умолчанию: 0.
    • env <Object> Переменные окружения для процесса с тестами. Несовместимо с isolation='none'. Переменные заменяют значения основного процесса и не сливаются с process.env. По умолчанию: process.env.
  • Возвращает: <TestsStream>

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { tap } from 'node:test/reporters';
import { run } from 'node:test';
import process from 'node:process';
import path from 'node:path';

run({ files: [path.resolve('./tests/test.js')] })
 .on('test:fail', () => {
   process.exitCode = 1;
 })
 .compose(tap)
 .pipe(process.stdout);
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const { tap } = require('node:test/reporters');
const { run } = require('node:test');
const path = require('node:path');

run({ files: [path.resolve('./tests/test.js')] })
 .on('test:fail', () => {
   process.exitCode = 1;
 })
 .compose(tap)
 .pipe(process.stdout);

suite([name][, options][, fn])

  • name <string> Имя набора, отображаемое в отчёте о тестах. По умолчанию: свойство name у fn или '<anonymous>', если у fn нет имени.
  • options <Object> Необязательные параметры набора. Поддерживаются те же опции, что у test([name][, options][, fn]).
  • fn <Function> | <AsyncFunction> Функция набора с вложенными тестами и наборами. Первый аргумент — объект SuiteContext. По умолчанию: пустая функция.
  • Возвращает: <Promise> Немедленно выполняется с undefined.

Функция suite() импортируется из модуля node:test.

suite.skip([name][, options][, fn])

Сокращение для пропуска набора. То же, что suite([name], { skip: true }[, fn]).

suite.todo([name][, options][, fn])

Сокращение для пометки набора как TODO. То же, что suite([name], { todo: true }[, fn]).

suite.only([name][, options][, fn])

Сокращение для пометки набора как only. То же, что suite([name], { only: true }[, fn]).

test([name][, options][, fn])

  • name <string> Имя теста, отображаемое в отчёте. По умолчанию: свойство name у fn или '<anonymous>', если у fn нет имени.
  • options <Object> Параметры теста. Поддерживаются следующие свойства:
    • concurrency <number> | <boolean> Если указано число, столько тестов выполняется асинхронно (по-прежнему в одном потоке цикла событий). Если true, все запланированные асинхронные тесты идут параллельно в рамках потока. Если false — по одному тесту за раз. Если не задано, подтесты наследуют значение от родителя. По умолчанию: false.
    • expectFailure <boolean> | <string> | <RegExp> | <Function> | <Object> | <Error> Если истинно, от теста ожидается провал. Непустая строка показывается в отчёте как причина ожидаемого провала. Если переданы напрямую RegExp | Function | Object | Error (без обёртки { match: … }), тест считается пройденным только при совпадении выброшенной ошибки по правилам assert.throws. Чтобы задать и причину, и проверку, передайте объект с полями label (строка) и match (RegExp, Function, Object или Error). По умолчанию: false.
    • only <boolean> Если истинно и контекст настроен на выполнение только тестов с only, этот тест выполняется; иначе пропускается. По умолчанию: false.
    • signal <AbortSignal> Позволяет прервать выполняющийся тест.
    • skip <boolean> | <string> Если истинно, тест пропускается. Строка показывается в отчёте как причина пропуска. По умолчанию: false.
    • todo <boolean> | <string> Если истинно, тест помечается как TODO. Строка показывается в отчёте как причина статуса TODO. По умолчанию: false.
    • timeout <number> Через столько миллисекунд тест считается проваленным. Если не задано, подтесты наследуют значение от родителя. По умолчанию: Infinity.
    • plan <number> Ожидаемое число утверждений и подтестов в тесте. Если фактическое число не совпадает с планом, тест проваливается. По умолчанию: undefined.
  • fn <Function> | <AsyncFunction> Тестируемая функция. Первый аргумент — объект TestContext. При колбэк-стиле второй аргумент — колбэк. По умолчанию: пустая функция.
  • Возвращает: <Promise> Выполняется с undefined после завершения теста или сразу, если тест выполняется внутри набора.

test() — это значение, импортируемое из модуля test. Каждый вызов регистрирует тест в TestsStream.

Объект TestContext, передаваемый в fn, используется для действий в контексте текущего теста: пропуск, диагностика, подтесты и т. д.

test() возвращает Promise, который выполняется по завершении теста; внутри набора он может выполниться сразу. Для тестов верхнего уровня возвращаемое значение часто можно игнорировать. Для подтестов await нужен, чтобы родитель не завершился раньше и не отменил подтест (см. пример).

1
2
3
4
5
6
7
8
9
test('top level test', async (t) => {
    // setTimeout() в подтесте может пережить родителя, если убрать await на следующей строке:
    // после завершения родителя незавершённые подтесты отменяются.
    await t.test('longer running subtest', async (t) => {
        return new Promise((resolve, reject) => {
            setTimeout(resolve, 1000);
        });
    });
});

Опция timeout завершает тест с провалом по истечении timeout мс, но не является надёжным способом отмены: выполняющийся тест может блокировать поток и помешать срабатыванию отмены.

test.skip([name][, options][, fn])

Сокращение для пропуска теста — то же, что test([name], { skip: true }[, fn]).

test.todo([name][, options][, fn])

Сокращение для пометки теста как TODO — то же, что test([name], { todo: true }[, fn]).

test.only([name][, options][, fn])

Сокращение для пометки теста как only — то же, что test([name], { only: true }[, fn]).

describe([name][, options][, fn])

Псевдоним suite().

Функция describe() импортируется из модуля node:test.

describe.skip([name][, options][, fn])

Сокращение для пропуска набора — то же, что describe([name], { skip: true }[, fn]).

describe.todo([name][, options][, fn])

Сокращение для пометки набора как TODO — то же, что describe([name], { todo: true }[, fn]).

describe.only([name][, options][, fn])

Сокращение для пометки набора как only — то же, что describe([name], { only: true }[, fn]).

it([name][, options][, fn])

Псевдоним test().

Функция it() импортируется из модуля node:test.

it.skip([name][, options][, fn])

Сокращение для пропуска теста — то же, что it([name], { skip: true }[, fn]).

it.todo([name][, options][, fn])

Сокращение для пометки теста как TODO — то же, что it([name], { todo: true }[, fn]).

it.only([name][, options][, fn])

Сокращение для пометки теста как only — то же, что it([name], { only: true }[, fn]).

before([fn][, options])

  • fn <Function> | <AsyncFunction> Функция хука. При колбэк-стиле второй аргумент — колбэк. По умолчанию: пустая функция.
  • options <Object> Параметры хука. Поддерживаются следующие свойства:
    • signal <AbortSignal> Позволяет прервать выполняющийся хук.
    • timeout <number> Через столько миллисекунд хук считается проваленным. Если не задано, подтесты наследуют значение от родителя. По умолчанию: Infinity.

Создаёт хук, выполняемый перед набором тестов.

1
2
3
4
5
6
describe('tests', async () => {
    before(() => console.log('about to run some test'));
    it('is a subtest', () => {
        // здесь проверки
    });
});

after([fn][, options])

  • fn <Function> | <AsyncFunction> Функция хука. При колбэк-стиле второй аргумент — колбэк. По умолчанию: пустая функция.
  • options <Object> Параметры хука. Поддерживаются следующие свойства:
    • signal <AbortSignal> Позволяет прервать выполняющийся хук.
    • timeout <number> Через столько миллисекунд хук считается проваленным. Если не задано, подтесты наследуют значение от родителя. По умолчанию: Infinity.

Создаёт хук, выполняемый после набора тестов.

1
2
3
4
5
6
describe('tests', async () => {
    after(() => console.log('finished running tests'));
    it('is a subtest', () => {
        // здесь проверки
    });
});

Примечание: хук after выполняется гарантированно, даже если тесты в наборе провалились.

beforeEach([fn][, options])

  • fn <Function> | <AsyncFunction> Функция хука. При колбэк-стиле второй аргумент — колбэк. По умолчанию: пустая функция.
  • options <Object> Параметры хука. Поддерживаются следующие свойства:
    • signal <AbortSignal> Позволяет прервать выполняющийся хук.
    • timeout <number> Через столько миллисекунд хук считается проваленным. Если не задано, подтесты наследуют значение от родителя. По умолчанию: Infinity.

Создаёт хук, выполняемый перед каждым тестом в текущем наборе.

1
2
3
4
5
6
describe('tests', async () => {
    beforeEach(() => console.log('about to run a test'));
    it('is a subtest', () => {
        // здесь проверки
    });
});

afterEach([fn][, options])

  • fn <Function> | <AsyncFunction> Функция хука. При колбэк-стиле второй аргумент — колбэк. По умолчанию: пустая функция.
  • options <Object> Параметры хука. Поддерживаются следующие свойства:
    • signal <AbortSignal> Позволяет прервать выполняющийся хук.
    • timeout <number> Через столько миллисекунд хук считается проваленным. Если не задано, подтесты наследуют значение от родителя. По умолчанию: Infinity.

Создаёт хук, выполняемый после каждого теста в текущем наборе. Хук afterEach() выполняется даже при провале теста.

1
2
3
4
5
6
describe('tests', async () => {
    afterEach(() => console.log('finished running a test'));
    it('is a subtest', () => {
        // здесь проверки
    });
});

assert

Объект, методы которого настраивают доступные утверждения для объектов TestContext в текущем процессе. По умолчанию доступны методы из node:assert и функции снимков.

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

assert.register(name, fn)

Определяет новую функцию утверждения с заданным именем. Если утверждение с таким именем уже есть, оно перезаписывается.

snapshot

Объект для настройки параметров снимков по умолчанию в текущем процессе. Общая конфигурация для всех файлов — через модуль с --require или --import.

snapshot.setDefaultSnapshotSerializers(serializers)

  • serializers <Array> Массив синхронных функций — сериализаторы по умолчанию для тестов со снимками.

Задаёт механизм сериализации по умолчанию для раннера. По умолчанию раннер вызывает JSON.stringify(value, null, 2) для переданного значения. У JSON.stringify() есть ограничения на циклические структуры и типы данных. Для более надёжной сериализации используйте эту функцию.

snapshot.setResolveSnapshotPath(fn)

  • fn <Function> Функция вычисления пути к файлу снимка. Единственный аргумент — путь к тестовому файлу. Если тест не привязан к файлу (например в REPL), аргумент undefined. fn() должна вернуть строку с путём к файлу снимка.

Задаёт расположение файла снимка. По умолчанию имя файла снимка совпадает с точкой входа с расширением .snapshot.

Class: MockFunctionContext

Класс MockFunctionContext позволяет просматривать и менять поведение подмен, созданных через API MockTracker.

ctx.calls

Геттер возвращает копию внутреннего массива вызовов подмены. Каждый элемент — объект со свойствами:

  • arguments <Array> Аргументы вызова подменённой функции.
  • error <any> Если подменённая функция выбросила исключение, здесь значение исключения. По умолчанию: undefined.
  • result <any> Возвращённое подменённой функцией значение.
  • stack <Error> Объект Error, по стеку можно определить место вызова подмены.
  • target <Function> | undefined Если подмена — конструктор, здесь класс создаваемого экземпляра; иначе undefined.
  • this <any> Значение this при вызове подмены.

ctx.callCount()

  • Возвращает: <integer> Число вызовов этой подмены.

Эффективнее, чем ctx.calls.length: ctx.calls — геттер, создающий копию внутреннего массива.

ctx.mockImplementation(implementation)

Меняет поведение существующей подмены.

Пример: создаётся подмена через t.mock.fn(), вызывается функция, затем подменяется реализация.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
test('changes a mock behavior', (t) => {
    let cnt = 0;

    function addOne() {
        cnt++;
        return cnt;
    }

    function addTwo() {
        cnt += 2;
        return cnt;
    }

    const fn = t.mock.fn(addOne);

    assert.strictEqual(fn(), 1);
    fn.mock.mockImplementation(addTwo);
    assert.strictEqual(fn(), 3);
    assert.strictEqual(fn(), 5);
});

ctx.mockImplementationOnce(implementation[, onCall])

  • implementation <Function> | <AsyncFunction> Реализация для вызова с номером onCall.
  • onCall <integer> Номер вызова, на котором применить implementation. Если такой вызов уже произошёл, выбрасывается исключение. По умолчанию: номер следующего вызова.

Меняет поведение подмены только для одного вызова. После вызова с номером onCall подмена возвращается к поведению, которое было бы без mockImplementationOnce().

Пример: подмена через t.mock.fn(), смена реализации на один следующий вызов, затем прежнее поведение.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
test('changes a mock behavior once', (t) => {
    let cnt = 0;

    function addOne() {
        cnt++;
        return cnt;
    }

    function addTwo() {
        cnt += 2;
        return cnt;
    }

    const fn = t.mock.fn(addOne);

    assert.strictEqual(fn(), 1);
    fn.mock.mockImplementationOnce(addTwo);
    assert.strictEqual(fn(), 3);
    assert.strictEqual(fn(), 4);
});

ctx.resetCalls()

Сбрасывает историю вызовов подменённой функции.

ctx.restore()

Восстанавливает исходную реализацию подменённой функции. Подмену по-прежнему можно вызывать.

Class: MockModuleContext

Стабильность: 1.0 — ранняя разработка

Класс MockModuleContext управляет поведением подмен модулей, созданных через API MockTracker.

ctx.restore()

Восстанавливает исходную реализацию подменённого модуля.

Class: MockPropertyContext

Класс MockPropertyContext позволяет просматривать и менять поведение подмен свойств, созданных через API MockTracker.

ctx.accesses

Геттер возвращает копию внутреннего массива обращений (чтение/запись) к подменённому свойству. Каждый элемент — объект со свойствами:

  • type <string> 'get' или 'set' — тип обращения.
  • value <any> Прочитанное ('get') или записанное ('set') значение.
  • stack <Error> Объект Error для определения места обращения к подмене.

ctx.accessCount()

  • Возвращает: <integer> Число обращений к свойству (чтение и запись).

Эффективнее, чем ctx.accesses.length: ctx.accesses — геттер с копией внутреннего массива.

ctx.mockImplementation(value)

  • value <any> Новое значение подменённого свойства.

Меняет значение, возвращаемое геттером подменённого свойства.

ctx.mockImplementationOnce(value[, onAccess])

  • value <any> Значение для обращения с номером onAccess.
  • onAccess <integer> Номер обращения, на котором применить value. Если такое обращение уже было, выбрасывается исключение. По умолчанию: номер следующего обращения.

Меняет поведение подмены только для одного обращения. После обращения с номером onAccess подмена возвращается к поведению без mockImplementationOnce().

Пример: подмена через t.mock.property(), смена значения на одно следующее обращение, затем прежнее поведение.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
test('changes a mock behavior once', (t) => {
    const obj = { foo: 1 };

    const prop = t.mock.property(obj, 'foo', 5);

    assert.strictEqual(obj.foo, 5);
    prop.mock.mockImplementationOnce(25);
    assert.strictEqual(obj.foo, 25);
    assert.strictEqual(obj.foo, 5);
});

Ограничение

Для согласованности с остальным API подмен эта функция считает и чтение, и запись свойства обращениями. Если запись происходит на том же индексе обращения, значение «на один раз» расходуется на операцию set, и подменённое свойство принимает это значение. Это может давать неожиданный результат, если «на один раз» предполагалось только для чтения.

ctx.resetAccesses()

Сбрасывает историю обращений к подменённому свойству.

ctx.restore()

Восстанавливает исходное поведение подменённого свойства. Подмену по-прежнему можно использовать.

Class: MockTracker

Класс MockTracker управляет подменами. Модуль раннера экспортирует mock верхнего уровня — экземпляр MockTracker. У каждого теста свой экземпляр в свойстве контекста mock.

mock.fn([original[, implementation]][, options])

  • original <Function> | <AsyncFunction> Необязательная исходная функция для подмены. По умолчанию: пустая функция.
  • implementation <Function> | <AsyncFunction> Необязательная реализация для original. Удобно для подмен с заданным числом вызовов с одним поведением и последующим восстановлением original. По умолчанию: функция из original.
  • options <Object> Необязательные параметры подмены функции. Поддерживаются свойства:
    • times <integer> Сколько раз подмена использует поведение implementation. После times вызовов автоматически восстанавливается поведение original. Должно быть целым больше нуля. По умолчанию: Infinity.
  • Возвращает: <Proxy> Подменённая функция с особым свойством mock — экземпляром MockFunctionContext для проверки и смены поведения.

Создаёт подмену функции.

Пример: счётчик увеличивается на 1 за вызов; опция times задаёт два первых вызова с прибавлением 2.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
test('mocks a counting function', (t) => {
    let cnt = 0;

    function addOne() {
        cnt++;
        return cnt;
    }

    function addTwo() {
        cnt += 2;
        return cnt;
    }

    const fn = t.mock.fn(addOne, addTwo, { times: 2 });

    assert.strictEqual(fn(), 2);
    assert.strictEqual(fn(), 4);
    assert.strictEqual(fn(), 5);
    assert.strictEqual(fn(), 6);
});

mock.getter(object, methodName[, implementation][, options])

Синтаксический сахар для MockTracker.method с options.getter равным true.

mock.method(object, methodName[, implementation][, options])

  • object <Object> Объект, метод которого подменяется.
  • methodName <string> | <symbol> Имя метода на object. Если object[methodName] не функция, выбрасывается ошибка.
  • implementation <Function> | <AsyncFunction> Необязательная реализация для object[methodName]. По умолчанию: исходный метод object[methodName].
  • options <Object> Необязательные параметры подмены метода. Поддерживаются свойства:
    • getter <boolean> Если true, object[methodName] трактуется как геттер. Несовместимо с опцией setter. По умолчанию: false.
    • setter <boolean> Если true, object[methodName] трактуется как сеттер. Несовместимо с опцией getter. По умолчанию: false.
    • times <integer> Сколько раз подмена использует поведение implementation. После times вызовов восстанавливается исходное поведение. Должно быть целым больше нуля. По умолчанию: Infinity.
  • Возвращает: <Proxy> Подменённый метод с особым свойством mock — экземпляром MockFunctionContext.

Создаёт подмену существующего метода объекта. Ниже — пример подмены метода.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
test('spies on an object method', (t) => {
    const number = {
        value: 5,
        subtract(a) {
            return this.value - a;
        },
    };

    t.mock.method(number, 'subtract');
    assert.strictEqual(number.subtract.mock.callCount(), 0);
    assert.strictEqual(number.subtract(3), 2);
    assert.strictEqual(number.subtract.mock.callCount(), 1);

    const call = number.subtract.mock.calls[0];

    assert.deepStrictEqual(call.arguments, [3]);
    assert.strictEqual(call.result, 2);
    assert.strictEqual(call.error, undefined);
    assert.strictEqual(call.target, undefined);
    assert.strictEqual(call.this, number);
});

mock.module(specifier[, options])

Стабильность: 1.0 — ранняя разработка

  • specifier <string> | <URL> Идентификатор подменяемого модуля.
  • options <Object> Необязательные параметры подмены модуля. Поддерживаются свойства:
    • cache <boolean> Если false, каждый вызов require() или import() создаёт новую подмену. Если true, последующие вызовы возвращают ту же подмену, модуль попадает в кэш CommonJS. По умолчанию: false.
    • exports <Object> Необязательные экспорты подмены. Свойство default, если задано, — экспорт по умолчанию; остальные собственные перечислимые свойства — именованные экспорты. Нельзя использовать вместе с defaultExport или namedExports.
      • Для CommonJS или встроенного модуля exports.default задаёт значение module.exports.
      • Если для CommonJS или встроенной подмены нет exports.default, module.exports — пустой объект.
      • Если есть именованные экспорты и экспорт по умолчанию не объект, при использовании как CommonJS или встроенного модуля подмена выбросит исключение.
    • defaultExport <any> Необязательный экспорт по умолчанию. Если не задан, у ESM-подмены нет default-экспорта. Для CommonJS или встроенного модуля значение идёт в module.exports. Если не задано, для CJS и встроенных подмен module.exports — пустой объект. Нельзя использовать вместе с options.exports. Устарело, будет удалено; предпочтительно options.exports.default.
    • namedExports <Object> Необязательный объект ключей и значений для именованных экспортов. Для CommonJS или встроенного модуля значения копируются в module.exports. Если есть именованные экспорты и экспорт по умолчанию не объект, при использовании как CJS или встроенного модуля подмена выбросит исключение. Нельзя использовать вместе с options.exports. Устарело; предпочтительно options.exports.
  • Возвращает: <MockModuleContext> Объект для управления подменой.

Подменяет экспорты модулей ECMAScript, CommonJS, JSON и встроенных модулей Node.js. Ссылки на оригинальный модуль, созданные до подмены, не меняются. Для подмены модулей Node.js нужно запускать с флагом --experimental-test-module-mocks.

Примечание: хуки настройки модулей, зарегистрированные через синхронный API, влияют на разрешение specifier в mock.module. Хуки асинхронного API сейчас игнорируются (загрузчик раннера синхронный, многократные цепочки загрузки в Node не поддерживаются).

Ниже — пример подмены модуля.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
test('mocks a builtin module in both module systems', async (t) => {
    // Подмена 'node:readline': именованный экспорт 'foo', которого нет в оригинале.
    const mock = t.mock.module('node:readline', {
        exports: { foo: () => 42 },
    });

    let esmImpl = await import('node:readline');
    let cjsImpl = require('node:readline');

    // cursorTo() — экспорт оригинального 'node:readline'.
    assert.strictEqual(esmImpl.cursorTo, undefined);
    assert.strictEqual(cjsImpl.cursorTo, undefined);
    assert.strictEqual(esmImpl.fn(), 42);
    assert.strictEqual(cjsImpl.fn(), 42);

    mock.restore();

    // Подмена снята — снова загружается встроенный модуль.
    esmImpl = await import('node:readline');
    cjsImpl = require('node:readline');

    assert.strictEqual(typeof esmImpl.cursorTo, 'function');
    assert.strictEqual(typeof cjsImpl.cursorTo, 'function');
    assert.strictEqual(esmImpl.fn, undefined);
    assert.strictEqual(cjsImpl.fn, undefined);
});

mock.property(object, propertyName[, value])

  • object <Object> Объект, свойство которого подменяется.
  • propertyName <string> | <symbol> Имя свойства на object.
  • value <any> Необязательное значение подмены для object[propertyName]. По умолчанию: исходное значение свойства.
  • Возвращает: <Proxy> Прокси к объекту с подменённым свойством; у него есть свойство mock — экземпляр MockPropertyContext для проверки и смены поведения.

Создаёт подмену значения свойства: можно отслеживать и управлять чтением и записью и восстановить исходное значение.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
test('mocks a property value', (t) => {
    const obj = { foo: 42 };
    const prop = t.mock.property(obj, 'foo', 100);

    assert.strictEqual(obj.foo, 100);
    assert.strictEqual(prop.mock.accessCount(), 1);
    assert.strictEqual(prop.mock.accesses[0].type, 'get');
    assert.strictEqual(prop.mock.accesses[0].value, 100);

    obj.foo = 200;
    assert.strictEqual(prop.mock.accessCount(), 2);
    assert.strictEqual(prop.mock.accesses[1].type, 'set');
    assert.strictEqual(prop.mock.accesses[1].value, 200);

    prop.mock.restore();
    assert.strictEqual(obj.foo, 42);
});

mock.reset()

Восстанавливает исходное поведение всех подмен, созданных этим MockTracker, и отвязывает их от экземпляра MockTracker. Отвязанные подмены по-прежнему вызываемы, но этот MockTracker больше не может их сбрасывать или иначе управлять ими.

После каждого теста эта функция вызывается для MockTracker контекста. При активном использовании глобального MockTracker рекомендуется вызывать её вручную.

mock.restoreAll()

Восстанавливает исходное поведение всех подмен этого MockTracker. В отличие от mock.reset(), mock.restoreAll() не отвязывает подмены от MockTracker.

mock.setter(object, methodName[, implementation][, options])

Синтаксический сахар для MockTracker.method с options.setter равным true.

Class: MockTimers

Подмена таймеров — распространённый приём тестирования: имитировать и управлять setInterval и setTimeout без реального ожидания.

MockTimers также может подменять объект Date.

У MockTracker есть экспорт верхнего уровня timers — экземпляр MockTimers.

timers.enable([enableOptions])

Включает подмену указанных таймеров.

  • enableOptions <Object> Необязательные параметры включения подмены таймеров. Поддерживаются свойства:
    • apis <Array> Необязательный массив подменяемых API. Допустимые значения: 'setInterval', 'setTimeout', 'setImmediate' и 'Date'. По умолчанию: ['setInterval', 'setTimeout', 'setImmediate', 'Date']. Если массив не передан, по умолчанию подменяются все связанные с временем API ('setInterval', 'clearInterval', 'setTimeout', 'clearTimeout', 'setImmediate', 'clearImmediate' и 'Date').
    • now <number> | <Date> Необязательное начальное время в миллисекундах или объект Date для Date.now(). По умолчанию: 0.

Примечание: при подмене конкретного таймера неявно подменяется и соответствующая функция очистки.

Примечание: подмена Date влияет на подменённые таймеры — общие внутренние часы.

Пример без задания начального времени:

1
2
import { mock } from 'node:test';
mock.timers.enable({ apis: ['setInterval'] });
1
2
const { mock } = require('node:test');
mock.timers.enable({ apis: ['setInterval'] });

В примере подменяется setInterval и неявно clearInterval. Подменяются только setInterval и clearInterval из node:timers, node:timers/promises и globalThis.

Пример с заданным начальным временем

1
2
import { mock } from 'node:test';
mock.timers.enable({ apis: ['Date'], now: 1000 });
1
2
const { mock } = require('node:test');
mock.timers.enable({ apis: ['Date'], now: 1000 });

Пример с объектом Date как начальным временем

1
2
import { mock } from 'node:test';
mock.timers.enable({ apis: ['Date'], now: new Date() });
1
2
const { mock } = require('node:test');
mock.timers.enable({ apis: ['Date'], now: new Date() });

Если вызвать mock.timers.enable() без параметров:

Подменяются все таймеры ('setInterval', 'clearInterval', 'setTimeout', 'clearTimeout', 'setImmediate' и 'clearImmediate'). Функции setInterval, clearInterval, setTimeout, clearTimeout, setImmediate и clearImmediate из node:timers, node:timers/promises и globalThis, а также глобальный объект Date.

timers.reset()

Восстанавливает исходное поведение всех подмен, созданных этим экземпляром MockTimers, и отвязывает их от MockTracker.

Примечание: после каждого теста эта функция вызывается для MockTracker контекста.

1
2
import { mock } from 'node:test';
mock.timers.reset();
1
2
const { mock } = require('node:test');
mock.timers.reset();

timers[Symbol.dispose]()

Вызывает timers.reset().

timers.tick([milliseconds])

Сдвигает время для всех подменённых таймеров.

  • milliseconds <number> На сколько миллисекунд сдвинуть таймеры. По умолчанию: 1.

Примечание: в отличие от setTimeout в Node.js здесь допустимы только положительные числа. Отрицательные задержки в Node поддерживаются лишь для совместимости с вебом.

Ниже подменяется setTimeout, затем .tick сдвигает время и срабатывают ожидающие таймеры.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import assert from 'node:assert';
import { test } from 'node:test';

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
  const fn = context.mock.fn();

  context.mock.timers.enable({ apis: ['setTimeout'] });

  setTimeout(fn, 9999);

  assert.strictEqual(fn.mock.callCount(), 0);

  // Сдвиг времени
  context.mock.timers.tick(9999);

  assert.strictEqual(fn.mock.callCount(), 1);
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
const assert = require('node:assert');
const { test } = require('node:test');

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
  const fn = context.mock.fn();
  context.mock.timers.enable({ apis: ['setTimeout'] });

  setTimeout(fn, 9999);
  assert.strictEqual(fn.mock.callCount(), 0);

  // Сдвиг времени
  context.mock.timers.tick(9999);

  assert.strictEqual(fn.mock.callCount(), 1);
});

Функцию .tick можно вызывать несколько раз подряд.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
import assert from 'node:assert';
import { test } from 'node:test';

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
  const fn = context.mock.fn();
  context.mock.timers.enable({ apis: ['setTimeout'] });
  const nineSecs = 9000;
  setTimeout(fn, nineSecs);

  const threeSeconds = 3000;
  context.mock.timers.tick(threeSeconds);
  context.mock.timers.tick(threeSeconds);
  context.mock.timers.tick(threeSeconds);

  assert.strictEqual(fn.mock.callCount(), 1);
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const assert = require('node:assert');
const { test } = require('node:test');

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
  const fn = context.mock.fn();
  context.mock.timers.enable({ apis: ['setTimeout'] });
  const nineSecs = 9000;
  setTimeout(fn, nineSecs);

  const threeSeconds = 3000;
  context.mock.timers.tick(threeSeconds);
  context.mock.timers.tick(threeSeconds);
  context.mock.timers.tick(threeSeconds);

  assert.strictEqual(fn.mock.callCount(), 1);
});

Сдвиг времени через .tick также двигает любой объект Date, созданный после включения подмены (если подменялся и Date).

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import assert from 'node:assert';
import { test } from 'node:test';

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
  const fn = context.mock.fn();

  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
  setTimeout(fn, 9999);

  assert.strictEqual(fn.mock.callCount(), 0);
  assert.strictEqual(Date.now(), 0);

  // Сдвиг времени
  context.mock.timers.tick(9999);
  assert.strictEqual(fn.mock.callCount(), 1);
  assert.strictEqual(Date.now(), 9999);
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
const assert = require('node:assert');
const { test } = require('node:test');

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
  const fn = context.mock.fn();
  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });

  setTimeout(fn, 9999);
  assert.strictEqual(fn.mock.callCount(), 0);
  assert.strictEqual(Date.now(), 0);

  // Сдвиг времени
  context.mock.timers.tick(9999);
  assert.strictEqual(fn.mock.callCount(), 1);
  assert.strictEqual(Date.now(), 9999);
});

Функции очистки

Как уже сказано, все функции очистки таймеров (clearTimeout, clearInterval и clearImmediate) подменяются неявно. Пример с setTimeout:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
import assert from 'node:assert';
import { test } from 'node:test';

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
  const fn = context.mock.fn();

  // При необходимости выберите, что подменять
  context.mock.timers.enable({ apis: ['setTimeout'] });
  const id = setTimeout(fn, 9999);

  // clearTimeout тоже подменён
  clearTimeout(id);
  context.mock.timers.tick(9999);

  // setTimeout отменён — мок-функция не вызовется
  assert.strictEqual(fn.mock.callCount(), 0);
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
const assert = require('node:assert');
const { test } = require('node:test');

test('mocks setTimeout to be executed synchronously without having to actually wait for it', (context) => {
  const fn = context.mock.fn();

  // При необходимости выберите, что подменять
  context.mock.timers.enable({ apis: ['setTimeout'] });
  const id = setTimeout(fn, 9999);

  // clearTimeout тоже подменён
  clearTimeout(id);
  context.mock.timers.tick(9999);

  // setTimeout отменён — мок-функция не вызовется
  assert.strictEqual(fn.mock.callCount(), 0);
});

Модули таймеров Node.js

После включения подмены таймеров затрагиваются node:timers, node:timers/promises и глобальные таймеры Node.js:

Примечание: деструктуризация вроде import [setTimeout](timers.md#settimeoutcallback-delay-args) from 'node:timers' пока не поддерживается этой API.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import assert from 'node:assert';
import { test } from 'node:test';
import nodeTimers from 'node:timers';
import nodeTimersPromises from 'node:timers/promises';

test('mocks setTimeout to be executed synchronously without having to actually wait for it', async (context) => {
  const globalTimeoutObjectSpy = context.mock.fn();
  const nodeTimerSpy = context.mock.fn();
  const nodeTimerPromiseSpy = context.mock.fn();

  // При необходимости выберите, что подменять
  context.mock.timers.enable({ apis: ['setTimeout'] });
  setTimeout(globalTimeoutObjectSpy, 9999);
  nodeTimers.setTimeout(nodeTimerSpy, 9999);

  const promise = nodeTimersPromises.setTimeout(9999).then(nodeTimerPromiseSpy);

  // Сдвиг времени
  context.mock.timers.tick(9999);
  assert.strictEqual(globalTimeoutObjectSpy.mock.callCount(), 1);
  assert.strictEqual(nodeTimerSpy.mock.callCount(), 1);
  await promise;
  assert.strictEqual(nodeTimerPromiseSpy.mock.callCount(), 1);
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const assert = require('node:assert');
const { test } = require('node:test');
const nodeTimers = require('node:timers');
const nodeTimersPromises = require('node:timers/promises');

test('mocks setTimeout to be executed synchronously without having to actually wait for it', async (context) => {
  const globalTimeoutObjectSpy = context.mock.fn();
  const nodeTimerSpy = context.mock.fn();
  const nodeTimerPromiseSpy = context.mock.fn();

  // При необходимости выберите, что подменять
  context.mock.timers.enable({ apis: ['setTimeout'] });
  setTimeout(globalTimeoutObjectSpy, 9999);
  nodeTimers.setTimeout(nodeTimerSpy, 9999);

  const promise = nodeTimersPromises.setTimeout(9999).then(nodeTimerPromiseSpy);

  // Сдвиг времени
  context.mock.timers.tick(9999);
  assert.strictEqual(globalTimeoutObjectSpy.mock.callCount(), 1);
  assert.strictEqual(nodeTimerSpy.mock.callCount(), 1);
  await promise;
  assert.strictEqual(nodeTimerPromiseSpy.mock.callCount(), 1);
});

В Node.js setInterval из node:timers/promises — это AsyncGenerator; он тоже поддерживается этой API:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import assert from 'node:assert';
import { test } from 'node:test';
import nodeTimersPromises from 'node:timers/promises';
test('should tick five times testing a real use case', async (context) => {
  context.mock.timers.enable({ apis: ['setInterval'] });

  const expectedIterations = 3;
  const interval = 1000;
  const startedAt = Date.now();
  async function run() {
    const times = [];
    for await (const time of nodeTimersPromises.setInterval(interval, startedAt)) {
      times.push(time);
      if (times.length === expectedIterations) break;
    }
    return times;
  }

  const r = run();
  context.mock.timers.tick(interval);
  context.mock.timers.tick(interval);
  context.mock.timers.tick(interval);

  const timeResults = await r;
  assert.strictEqual(timeResults.length, expectedIterations);
  for (let it = 1; it < expectedIterations; it++) {
    assert.strictEqual(timeResults[it - 1], startedAt + (interval * it));
  }
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const assert = require('node:assert');
const { test } = require('node:test');
const nodeTimersPromises = require('node:timers/promises');
test('should tick five times testing a real use case', async (context) => {
  context.mock.timers.enable({ apis: ['setInterval'] });

  const expectedIterations = 3;
  const interval = 1000;
  const startedAt = Date.now();
  async function run() {
    const times = [];
    for await (const time of nodeTimersPromises.setInterval(interval, startedAt)) {
      times.push(time);
      if (times.length === expectedIterations) break;
    }
    return times;
  }

  const r = run();
  context.mock.timers.tick(interval);
  context.mock.timers.tick(interval);
  context.mock.timers.tick(interval);

  const timeResults = await r;
  assert.strictEqual(timeResults.length, expectedIterations);
  for (let it = 1; it < expectedIterations; it++) {
    assert.strictEqual(timeResults[it - 1], startedAt + (interval * it));
  }
});

timers.runAll()

Немедленно выполняет все ожидающие подменённые таймеры. Если подменён и объект Date, время Date сдвигается до момента самого позднего таймера.

Ниже все ожидающие таймеры срабатывают сразу, без задержки.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import assert from 'node:assert';
import { test } from 'node:test';

test('runAll functions following the given order', (context) => {
  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
  const results = [];
  setTimeout(() => results.push(1), 9999);

  // При одинаковой задержке у обоих таймеров порядок выполнения гарантирован
  setTimeout(() => results.push(3), 8888);
  setTimeout(() => results.push(2), 8888);

  assert.deepStrictEqual(results, []);

  context.mock.timers.runAll();
  assert.deepStrictEqual(results, [3, 2, 1]);
  // Date сдвигается до времени самого позднего таймера
  assert.strictEqual(Date.now(), 9999);
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
const assert = require('node:assert');
const { test } = require('node:test');

test('runAll functions following the given order', (context) => {
  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
  const results = [];
  setTimeout(() => results.push(1), 9999);

  // При одинаковой задержке у обоих таймеров порядок выполнения гарантирован
  setTimeout(() => results.push(3), 8888);
  setTimeout(() => results.push(2), 8888);

  assert.deepStrictEqual(results, []);

  context.mock.timers.runAll();
  assert.deepStrictEqual(results, [3, 2, 1]);
  // Date сдвигается до времени самого позднего таймера
  assert.strictEqual(Date.now(), 9999);
});

Примечание: runAll() предназначена для срабатывания таймеров в режиме подмены. На реальные системные часы и таймеры вне подмены не влияет.

timers.setTime(milliseconds)

Задаёт текущую метку Unix как опорную для всех подменённых объектов Date.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import assert from 'node:assert';
import { test } from 'node:test';

test('runAll functions following the given order', (context) => {
  const now = Date.now();
  const setTime = 1000;
  // Date.now ещё не подменён
  assert.deepStrictEqual(Date.now(), now);

  context.mock.timers.enable({ apis: ['Date'] });
  context.mock.timers.setTime(setTime);
  // Date.now теперь 1000
  assert.strictEqual(Date.now(), setTime);
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const assert = require('node:assert');
const { test } = require('node:test');

test('setTime replaces current time', (context) => {
  const now = Date.now();
  const setTime = 1000;
  // Date.now ещё не подменён
  assert.deepStrictEqual(Date.now(), now);

  context.mock.timers.enable({ apis: ['Date'] });
  context.mock.timers.setTime(setTime);
  // Date.now теперь 1000
  assert.strictEqual(Date.now(), setTime);
});

Дата и таймеры вместе

Объекты даты и таймеров связаны. Если вызвать setTime() для подменённого Date, уже запланированные setTimeout и setInterval не срабатывают от этого.

Метод tick при этом сдвигает подменённый объект Date.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import assert from 'node:assert';
import { test } from 'node:test';

test('runAll functions following the given order', (context) => {
  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
  const results = [];
  setTimeout(() => results.push(1), 9999);

  assert.deepStrictEqual(results, []);
  context.mock.timers.setTime(12000);
  assert.deepStrictEqual(results, []);
  // Дата сдвинута, таймеры при этом не срабатывают
  assert.strictEqual(Date.now(), 12000);
});
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
const assert = require('node:assert');
const { test } = require('node:test');

test('runAll functions following the given order', (context) => {
  context.mock.timers.enable({ apis: ['setTimeout', 'Date'] });
  const results = [];
  setTimeout(() => results.push(1), 9999);

  assert.deepStrictEqual(results, []);
  context.mock.timers.setTime(12000);
  assert.deepStrictEqual(results, []);
  // Дата сдвинута, таймеры при этом не срабатывают
  assert.strictEqual(Date.now(), 12000);
});

Class: TestsStream

Успешный вызов run() возвращает новый TestsStream — поток событий о ходе выполнения тестов. TestsStream генерирует события в порядке, связанном с объявлением тестов.

Часть событий гарантированно следует порядку объявления тестов, другие — порядку фактического выполнения.

Event: 'test:coverage'

  • data <Object>
    • summary <Object> Объект с отчётом о покрытии.
      • files <Array> Массив отчётов по файлам. Каждый отчёт — объект со схемой:
        • path <string> Абсолютный путь к файлу.
        • totalLineCount <number> Всего строк.
        • totalBranchCount <number> Всего ветвлений.
        • totalFunctionCount <number> Всего функций.
        • coveredLineCount <number> Покрыто строк.
        • coveredBranchCount <number> Покрыто ветвлений.
        • coveredFunctionCount <number> Покрыто функций.
        • coveredLinePercent <number> Процент покрытых строк.
        • coveredBranchPercent <number> Процент покрытых ветвлений.
        • coveredFunctionPercent <number> Процент покрытых функций.
        • functions <Array> Массив записей о покрытии функций.
          • name <string> Имя функции.
          • line <number> Строка объявления функции.
          • count <number> Сколько раз функция вызывалась.
        • branches <Array> Массив записей о ветвлениях.
          • line <number> Строка объявления ветвления.
          • count <number> Сколько раз выполнялась ветвь.
        • lines <Array> Массив строк: номер строки и число покрытий.
          • line <number> Номер строки.
          • count <number> Сколько раз строка была исполнена.
      • thresholds <Object> Пороги по типам покрытия.
        • function <number> Порог по функциям.
        • branch <number> Порог по ветвлениям.
        • line <number> Порог по строкам.
      • totals <Object> Сводка по всем файлам.
        • totalLineCount <number> Всего строк.
        • totalBranchCount <number> Всего ветвлений.
        • totalFunctionCount <number> Всего функций.
        • coveredLineCount <number> Покрыто строк.
        • coveredBranchCount <number> Покрыто ветвлений.
        • coveredFunctionCount <number> Покрыто функций.
        • coveredLinePercent <number> Процент покрытых строк.
        • coveredBranchPercent <number> Процент покрытых ветвлений.
        • coveredFunctionPercent <number> Процент покрытых функций.
      • workingDirectory <string> Рабочий каталог на начало сбора покрытия. Удобно для относительных путей, если тесты меняли cwd процесса Node.js.
    • nesting <number> Уровень вложенности теста.

Генерируется при включённом покрытии после завершения всех тестов.

Event: 'test:complete'

  • data <Object>
    • column <number> | undefined Номер столбца объявления теста или undefined, если тест запущен из REPL.
    • details <Object> Дополнительные метаданные выполнения.
      • passed <boolean> Прошёл ли тест.
      • duration_ms <number> Длительность теста в миллисекундах.
      • error <Error> | undefined Обёртка над ошибкой теста, если тест не прошёл.
        • cause <Error> Исходная ошибка теста.
      • type <string> | undefined Тип теста: признак того, что это набор.
    • file <string> | undefined Путь к тестовому файлу; undefined, если тест запущен из REPL.
    • line <number> | undefined Номер строки объявления теста или undefined, если тест запущен из REPL.
    • name <string> Имя теста.
    • nesting <number> Уровень вложенности теста.
    • testNumber <number> Порядковый номер теста.
    • todo <string> | <boolean> | undefined Присутствует, если вызван context.todo
    • skip <string> | <boolean> | undefined Присутствует, если вызван context.skip

Генерируется по завершении выполнения теста. Порядок событий не совпадает с порядком объявления тестов. Соответствующие события в порядке объявления — 'test:pass' и 'test:fail'.

Event: 'test:dequeue'

  • data <Object>
    • column <number> | undefined Номер столбца объявления теста или undefined, если тест запущен из REPL.
    • file <string> | undefined Путь к тестовому файлу; undefined, если тест запущен из REPL.
    • line <number> | undefined Номер строки объявления теста или undefined, если тест запущен из REPL.
    • name <string> Имя теста.
    • nesting <number> Уровень вложенности теста.
    • type <string> Тип теста: 'suite' или 'test'.

Генерируется при снятии теста с очереди, непосредственно перед выполнением. Порядок в общем случае не совпадает с порядком объявления тестов. Соответствующее событие в порядке объявления — 'test:start'.

Event: 'test:diagnostic'

  • data <Object>
    • column <number> | undefined Номер столбца объявления теста или undefined, если тест запущен из REPL.
    • file <string> | undefined Путь к тестовому файлу; undefined, если тест запущен из REPL.
    • line <number> | undefined Номер строки объявления теста или undefined, если тест запущен из REPL.
    • message <string> Текст диагностики.
    • nesting <number> Уровень вложенности теста.
    • level <string> Уровень важности сообщения. Допустимые значения:
      • 'info': информационные сообщения.
      • 'warn': предупреждения.
      • 'error': ошибки.

Генерируется при вызове context.diagnostic. Порядок событий совпадает с порядком объявления тестов.

Event: 'test:enqueue'

  • data <Object>
    • column <number> | undefined Номер столбца объявления теста или undefined, если тест запущен из REPL.
    • file <string> | undefined Путь к тестовому файлу; undefined, если тест запущен из REPL.
    • line <number> | undefined Номер строки объявления теста или undefined, если тест запущен из REPL.
    • name <string> Имя теста.
    • nesting <number> Уровень вложенности теста.
    • type <string> Тип теста: 'suite' или 'test'.

Генерируется при постановке теста в очередь на выполнение.

Event: 'test:fail'

  • data <Object>
    • column <number> | undefined Номер столбца объявления теста или undefined, если тест запущен из REPL.
    • details <Object> Дополнительные метаданные выполнения.
      • duration_ms <number> Длительность теста в миллисекундах.
      • error <Error> Обёртка над ошибкой, выброшенной тестом.
        • cause <Error> Исходная ошибка теста.
      • type <string> | undefined Тип теста: признак набора.
      • attempt <number> | undefined Номер попытки прогона, только при --test-rerun-failures.
    • file <string> | undefined Путь к тестовому файлу; undefined, если тест запущен из REPL.
    • line <number> | undefined Номер строки объявления теста или undefined, если тест запущен из REPL.
    • name <string> Имя теста.
    • nesting <number> Уровень вложенности теста.
    • testNumber <number> Порядковый номер теста.
    • todo <string> | <boolean> | undefined При вызове context.todo
    • skip <string> | <boolean> | undefined При вызове context.skip

Генерируется при провале теста. Порядок совпадает с порядком объявления тестов. Соответствующее событие в порядке выполнения — 'test:complete'.

Event: 'test:interrupted'

  • data <Object>
    • tests <Array> Массив объектов с данными о прерванных тестах.
      • column <number> | undefined Номер столбца объявления теста или undefined, если тест из REPL.
      • file <string> | undefined Путь к тестовому файлу; undefined, если тест из REPL.
      • line <number> | undefined Номер строки объявления теста или undefined, если тест из REPL.
      • name <string> Имя теста.
      • nesting <number> Уровень вложенности теста.

Генерируется при прерывании раннера сигналом SIGINT (например Ctrl+C). В событии — сведения о тестах, которые выполнялись в момент прерывания.

При изоляции процессов (по умолчанию) имя теста — путь к файлу: родительский раннер знает только тесты уровня файла. При --test-isolation=none показывается фактическое имя теста.

Event: 'test:pass'

  • data <Object>
    • column <number> | undefined Номер столбца объявления теста или undefined, если тест запущен из REPL.
    • details <Object> Дополнительные метаданные выполнения.
      • duration_ms <number> Длительность теста в миллисекундах.
      • type <string> | undefined Тип теста: признак набора.
      • attempt <number> | undefined Номер попытки прогона, только при --test-rerun-failures.
      • passed_on_attempt <number> | undefined Номер попытки, на которой тест прошёл, только при --test-rerun-failures.
    • file <string> | undefined Путь к тестовому файлу; undefined, если тест запущен из REPL.
    • line <number> | undefined Номер строки объявления теста или undefined, если тест запущен из REPL.
    • name <string> Имя теста.
    • nesting <number> Уровень вложенности теста.
    • testNumber <number> Порядковый номер теста.
    • todo <string> | <boolean> | undefined При вызове context.todo
    • skip <string> | <boolean> | undefined При вызове context.skip

Генерируется при успешном прохождении теста. Порядок совпадает с порядком объявления тестов. Соответствующее событие в порядке выполнения — 'test:complete'.

Event: 'test:plan'

  • data <Object>
    • column <number> | undefined Номер столбца объявления теста или undefined, если тест запущен из REPL.
    • file <string> | undefined Путь к тестовому файлу; undefined, если тест запущен из REPL.
    • line <number> | undefined Номер строки объявления теста или undefined, если тест запущен из REPL.
    • nesting <number> Уровень вложенности теста.
    • count <number> Число выполненных подтестов.

Генерируется после завершения всех подтестов данного теста. Порядок совпадает с порядком объявления тестов.

Event: 'test:start'

  • data <Object>
    • column <number> | undefined Номер столбца объявления теста или undefined, если тест запущен из REPL.
    • file <string> | undefined Путь к тестовому файлу; undefined, если тест запущен из REPL.
    • line <number> | undefined Номер строки объявления теста или undefined, если тест запущен из REPL.
    • name <string> Имя теста.
    • nesting <number> Уровень вложенности теста.

Генерируется, когда тест начинает отчитываться о себе и подтестах. Порядок совпадает с порядком объявления тестов. Соответствующее событие в порядке выполнения — 'test:dequeue'.

Event: 'test:stderr'

  • data <Object>
    • file <string> Путь к тестовому файлу.
    • message <string> Сообщение, записанное в stderr.

Генерируется при записи выполняющегося теста в stderr. Только если передан флаг --test. Порядок в общем случае не совпадает с порядком объявления тестов.

Event: 'test:stdout'

  • data <Object>
    • file <string> Путь к тестовому файлу.
    • message <string> Сообщение, записанное в stdout.

Генерируется при записи выполняющегося теста в stdout. Только если передан флаг --test. Порядок в общем случае не совпадает с порядком объявления тестов.

Event: 'test:summary'

  • data <Object>
    • counts <Object> Счётчики результатов прогона.
      • cancelled <number> Число отменённых тестов.
      • failed <number> Число проваленных тестов.
      • passed <number> Число успешных тестов.
      • skipped <number> Число пропущенных тестов.
      • suites <number> Число выполненных наборов.
      • tests <number> Число выполненных тестов без наборов.
      • todo <number> Число тестов в статусе TODO.
      • topLevel <number> Число тестов и наборов верхнего уровня.
    • duration_ms <number> Длительность прогона в миллисекундах.
    • file <string> | undefined Путь к тестовому файлу, с которого получена сводка. Если сводка объединяет несколько файлов — undefined.
    • success <boolean> Считается ли прогон успешным. При любой ошибке (провал теста, не выполнен порог покрытия и т. п.) — false.

Генерируется по завершении прогона. Содержит метрики для оценки успеха или провала. При изоляции тестов на уровне процесса событие 'test:summary' приходит для каждого файла и отдельно итоговая сводка.

Event: 'test:watch:drained'

Генерируется, когда в режиме watch в очереди больше нет тестов.

Event: 'test:watch:restarted'

Генерируется при перезапуске одного или нескольких тестов из‑за изменения файла в режиме watch.

getTestContext()

Возвращает TestContext или SuiteContext для текущего теста или набора, либо undefined вне теста/набора. Позволяет получить контекст из тела теста/набора или из асинхронных операций внутри них.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { getTestContext } from 'node:test';

test('example test', async () => {
  const ctx = getTestContext();
  console.log(`Running test: ${ctx.name}`);
});

describe('example suite', () => {
  const ctx = getTestContext();
  console.log(`Running suite: ${ctx.name}`);
});

Из теста возвращает TestContext. Из набора — SuiteContext.

Вне теста или набора (например на верхнем уровне модуля или в колбэке setTimeout после завершения) — undefined.

Внутри хука (before, beforeEach, after, afterEach) возвращается контекст теста или набора, к которому привязан хук.

Class: TestContext

В каждую функцию теста передаётся экземпляр TestContext для работы с раннером. Конструктор TestContext в API не экспортируется.

context.before([fn][, options])

  • fn <Function> | <AsyncFunction> Функция хука. Первый аргумент — TestContext. При колбэк-стиле второй аргумент — колбэк. По умолчанию: пустая функция.
  • options <Object> Параметры хука:
    • signal <AbortSignal> Позволяет прервать выполняющийся хук.
    • timeout <number> Через столько миллисекунд хук считается проваленным. Если не задано, подтесты наследуют значение от родителя. По умолчанию: Infinity.

Создаёт хук, выполняемый перед подтестом текущего теста.

context.beforeEach([fn][, options])

  • fn <Function> | <AsyncFunction> Функция хука. Первый аргумент — TestContext. При колбэк-стиле второй аргумент — колбэк. По умолчанию: пустая функция.
  • options <Object> Параметры хука:
    • signal <AbortSignal> Позволяет прервать выполняющийся хук.
    • timeout <number> Через столько миллисекунд хук считается проваленным. Если не задано, подтесты наследуют значение от родителя. По умолчанию: Infinity.

Создаёт хук перед каждым подтестом текущего теста.

1
2
3
4
5
6
7
8
test('top level test', async (t) => {
    t.beforeEach((t) =>
        t.diagnostic(`about to run ${t.name}`)
    );
    await t.test('This is a subtest', (t) => {
        // здесь ваши проверки
    });
});

context.after([fn][, options])

  • fn <Function> | <AsyncFunction> Функция хука. Первый аргумент — TestContext. При колбэк-стиле второй аргумент — колбэк. По умолчанию: пустая функция.
  • options <Object> Параметры хука:
    • signal <AbortSignal> Позволяет прервать выполняющийся хук.
    • timeout <number> Через столько миллисекунд хук считается проваленным. Если не задано, подтесты наследуют значение от родителя. По умолчанию: Infinity.

Создаёт хук после завершения текущего теста.

1
2
3
4
5
6
test('top level test', async (t) => {
    t.after((t) =>
        t.diagnostic(`finished running ${t.name}`)
    );
    // здесь ваши проверки
});

context.afterEach([fn][, options])

  • fn <Function> | <AsyncFunction> Функция хука. Первый аргумент — TestContext. При колбэк-стиле второй аргумент — колбэк. По умолчанию: пустая функция.
  • options <Object> Параметры хука:
    • signal <AbortSignal> Позволяет прервать выполняющийся хук.
    • timeout <number> Через столько миллисекунд хук считается проваленным. Если не задано, подтесты наследуют значение от родителя. По умолчанию: Infinity.

Создаёт хук после каждого подтеста текущего теста.

1
2
3
4
5
6
7
8
test('top level test', async (t) => {
    t.afterEach((t) =>
        t.diagnostic(`finished running ${t.name}`)
    );
    await t.test('This is a subtest', (t) => {
        // здесь ваши проверки
    });
});

context.assert

Объект с методами утверждения, привязанными к context. Здесь доступны функции верхнего уровня модуля node:assert для построения планов тестов.

1
2
3
4
test('test', (t) => {
    t.plan(1);
    t.assert.strictEqual(true, true);
});

context.assert.fileSnapshot(value, path[, options])

  • value <any> Значение для сериализации в строку. Если Node.js запущен с --test-update-snapshots, сериализованное значение записывается в path. Иначе оно сравнивается с содержимым существующего файла снимка.
  • path <string> Файл, куда записывается сериализованное value.
  • options <Object> Необязательные параметры. Поддерживаются следующие свойства:
    • serializers <Array> Массив синхронных функций сериализации value в строку. В первую функцию передаётся только value; результат каждой передаётся следующей. После всех сериализаторов значение приводится к строке. По умолчанию: если сериализаторы не заданы, используются сериализаторы раннера по умолчанию.

Сериализует value и записывает в файл path.

1
2
3
4
5
6
test('snapshot test with default serialization', (t) => {
    t.assert.fileSnapshot(
        { value1: 1, value2: 2 },
        './snapshots/snapshot.json'
    );
});

Отличия от context.assert.snapshot():

  • Путь к файлу снимка задаётся явно.
  • В одном файле снимка — одно значение.
  • Раннер не выполняет дополнительного экранирования.

Это упрощает подсветку синтаксиса и подобные сценарии.

context.assert.snapshot(value[, options])

  • value <any> Значение для сериализации. При --test-update-snapshots сериализованное значение записывается в файл снимка. Иначе сравнивается с соответствующим значением в существующем файле.
  • options <Object> Необязательные параметры. Поддерживаются следующие свойства:
    • serializers <Array> Массив синхронных сериализаторов (как в fileSnapshot). По умолчанию: сериализаторы раннера по умолчанию.

Утверждения для тестов со снимками.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
test('snapshot test with default serialization', (t) => {
    t.assert.snapshot({ value1: 1, value2: 2 });
});

test('snapshot test with custom serialization', (t) => {
    t.assert.snapshot(
        { value3: 3, value4: 4 },
        {
            serializers: [(value) => JSON.stringify(value)],
        }
    );
});

context.diagnostic(message)

  • message <string> Текст диагностики.

Пишет диагностику в вывод; данные появляются в конце результатов теста. Возвращаемого значения нет.

1
2
3
test('top level test', (t) => {
    t.diagnostic('A diagnostic message');
});

context.filePath

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

context.fullName

Имя теста и всех предков через >.

context.name

Имя теста.

context.passed

  • Тип: <boolean> До выполнения теста — false, например в хуке beforeEach.

Указывает, успешен ли тест.

context.error

Причина провала теста/кейса; исходная ошибка в context.error.cause.

context.attempt

Номер попытки теста (с нуля: первая — 0, вторая — 1 и т. д.). Удобно с опцией --test-rerun-failures, чтобы знать текущую попытку.

context.workerId

Уникальный идентификатор воркера, выполняющего текущий тестовый файл. Берётся из NODE_TEST_WORKER_ID. При --test-isolation=process (по умолчанию) каждый файл в отдельном дочернем процессе с ID от 1 до N (N — число параллельных воркеров). При --test-isolation=none все тесты в одном процессе, идентификатор всегда 1. Вне контекста теста — undefined.

Свойство удобно для распределения ресурсов (соединения с БД, порты и т. д.) между параллельными файлами:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
import { test } from 'node:test';
import { process } from 'node:process';

test('database operations', async (t) => {
  // ID воркера в контексте
  console.log(`Running in worker ${t.workerId}`);

  // или в переменной окружения (доступна при импорте)
  const workerId = process.env.NODE_TEST_WORKER_ID;
  // по workerId можно разводить ресурсы между воркерами
});

context.plan(count[,options])

  • count <number> Ожидаемое число утверждений и подтестов.
  • options <Object> Дополнительные параметры плана.
    • wait <boolean> | <number> Поведение ожидания плана:
      • true — ждать неограниченно, пока не выполнятся все утверждения и подтесты.
      • false — проверка сразу после завершения функции теста, без ожидания отложенных утверждений и подтестов. Утверждения и подтесты, завершившиеся позже, в план не входят.
      • число — максимальное время ожидания в миллисекундах до тайм-аута при ожидании ожидаемых утверждений и подтестов. При тайм-ауте тест проваливается. По умолчанию: false.

Задаёт ожидаемое число утверждений и подтестов в тесте. При несовпадении с фактом тест проваливается.

Примечание: чтобы утверждения учитывались планом, используйте t.assert, а не прямой импорт assert.

1
2
3
4
5
test('top level test', (t) => {
    t.plan(2);
    t.assert.ok('some relevant assertion here');
    t.test('subtest', () => {});
});

В асинхронном коде plan помогает убедиться, что выполнено нужное число утверждений:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
test('planning with streams', (t, done) => {
    function* generate() {
        yield 'a';
        yield 'b';
        yield 'c';
    }
    const expected = ['a', 'b', 'c'];
    t.plan(expected.length);
    const stream = Readable.from(generate());
    stream.on('data', (chunk) => {
        t.assert.strictEqual(chunk, expected.shift());
    });

    stream.on('end', () => {
        done();
    });
});

С опцией wait можно задать, как долго тест ждёт ожидаемые утверждения. Например, ограничение по времени позволяет дождаться асинхронных утверждений в заданном окне:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
test('plan with wait: 2000 waits for async assertions', (t) => {
    t.plan(1, { wait: 2000 }); // ждём до 2 с, пока утверждение завершится

    const asyncActivity = () => {
        setTimeout(() => {
            t.assert.ok(
                true,
                'Async assertion completed within the wait time'
            );
        }, 1000); // срабатывает через 1 с — укладываемся в окно ожидания
    };

    asyncActivity(); // тест пройдёт: утверждение успевает в срок
});

Примечание: отсчёт тайм-аута wait начинается только после завершения функции теста.

context.runOnly(shouldRunOnlyTests)

  • shouldRunOnlyTests <boolean> Выполнять ли только тесты с only.

Если shouldRunOnlyTests истинно, контекст выполняет только тесты с опцией only; иначе — все. Без флага --test-only в командной строке вызов не действует.

1
2
3
4
5
6
7
8
test('top level test', (t) => {
    // Можно включить режим only для подтестов в этом контексте.
    t.runOnly(true);
    return Promise.all([
        t.test('this subtest is now skipped'),
        t.test('this subtest is run', { only: true }),
    ]);
});

context.signal

Позволяет прервать подзадачи теста при его отмене.

1
2
3
test('top level test', async (t) => {
    await fetch('some/uri', { signal: t.signal });
});

context.skip([message])

  • message <string> Необязательное сообщение о пропуске.

Помечает тест в выводе как пропущенный. Если передана message, она попадает в отчёт. skip() не прерывает выполнение функции теста. Возвращаемого значения нет.

1
2
3
4
test('top level test', (t) => {
    // При дополнительной логике в тесте здесь тоже нужен return.
    t.skip('this is skipped');
});

context.todo([message])

  • message <string> Необязательное сообщение TODO.

Добавляет в вывод директиву TODO. При переданной message она включается в отчёт. todo() не прерывает выполнение функции теста. Возвращаемого значения нет.

1
2
3
4
test('top level test', (t) => {
    // Тест помечен как TODO
    t.todo('this is a todo');
});

context.test([name][, options][, fn])

  • name <string> Имя подтеста в отчёте. По умолчанию: свойство name у fn или '<anonymous>', если у fn нет имени.
  • options <Object> Параметры подтеста. Поддерживаются свойства:
    • concurrency <number> | <boolean> | null Если число — столько подтестов выполняется асинхронно (в одном потоке цикла событий). Если true — все подтесты параллельно. Если false — по одному за раз. Если не задано, подтесты наследуют от родителя. По умолчанию: null.
    • only <boolean> Если истинно и контекст настроен на only, подтест выполняется; иначе пропускается. По умолчанию: false.
    • signal <AbortSignal> Прерывание выполняющегося подтеста.
    • skip <boolean> | <string> Пропуск подтеста; строка — причина в отчёте. По умолчанию: false.
    • todo <boolean> | <string> Статус TODO; строка — пояснение в отчёте. По умолчанию: false.
    • timeout <number> Тайм-аут подтеста в миллисекундах. Если не задано, подтесты наследуют от родителя. По умолчанию: Infinity.
    • plan <number> Ожидаемое число утверждений и подтестов. При несовпадении с планом подтест проваливается. По умолчанию: undefined.
  • fn <Function> | <AsyncFunction> Функция подтеста; первый аргумент — TestContext; при колбэках второй — колбэк. По умолчанию: пустая функция.
  • Возвращает: <Promise> Выполняется с undefined по завершении подтеста.

Создаёт подтесты внутри текущего теста; поведение как у test() верхнего уровня.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
test('top level test', async (t) => {
    await t.test(
        'This is a subtest',
        {
            only: false,
            skip: false,
            concurrency: 1,
            todo: false,
            plan: 1,
        },
        (t) => {
            t.assert.ok('some relevant assertion here');
        }
    );
});

context.waitFor(condition[, options])

  • condition <Function> | <AsyncFunction> Функция-проверка, вызываемая периодически до успешного завершения или истечения тайм-аута опроса. Успех — отсутствие исключения и отклонения промиса. Аргументов не принимает, может вернуть любое значение.
  • options <Object> Необязательные параметры опроса:
    • interval <number> Пауза в миллисекундах после неуспешного вызова condition перед следующей попыткой. По умолчанию: 50.
    • timeout <number> Общий тайм-аут опроса в миллисекундах. Если за это время condition не завершилась успешно — ошибка. По умолчанию: 1000.
  • Возвращает: <Promise> Выполняется значением, возвращённым condition.

Опрашивает condition, пока она не завершится успешно или не истечёт тайм-аут.

Class: SuiteContext

В каждую функцию набора передаётся экземпляр SuiteContext для работы с раннером. Конструктор SuiteContext в API не экспортируется.

context.filePath

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

context.fullName

Имя набора и предков через >.

context.name

Имя набора.

context.signal

Позволяет прервать подзадачи теста при его отмене.

context.passed

Указывает, прошли ли набор и все его подтесты.

context.attempt

Номер попытки набора (с нуля). Удобно с --test-rerun-failures, чтобы знать номер текущей попытки.

context.diagnostic(message)

  • message <string> Текст диагностики.

Выводит диагностическое сообщение, обычно для журналирования о наборе или его тестах.

1
2
3
test.describe('my suite', (suite) => {
    suite.diagnostic('Suite diagnostic message');
});

Комментарии