Expect
При написании тестов часто требуется проверять, соответствуют ли значения определенным условиям. expect предоставляет доступ к ряду «сопоставителей», которые позволяют проверять разные вещи.
Дополнительные сопоставители Jest, поддерживаемые сообществом Jest, доступны в jest-extended.
Примеры TypeScript на этой странице будут работать в соответствии с документацией, только если вы явно импортируете API Jest:
import {expect, jest, test} from '@jest/globals';
Подробные сведения о настройке Jest для работы с TypeScript см. в руководстве Начало работы.
Справочник
- Expect
- Модификаторы
-
Сопоставители
.toBe(value).toHaveBeenCalled().toHaveBeenCalledTimes(number).toHaveBeenCalledWith(arg1, arg2, ...).toHaveBeenLastCalledWith(arg1, arg2, ...).toHaveBeenNthCalledWith(nthCall, arg1, arg2, ....).toHaveReturned().toHaveReturnedTimes(number).toHaveReturnedWith(value).toHaveLastReturnedWith(value).toHaveNthReturnedWith(nthCall, value).toHaveLength(number).toHaveProperty(keyPath, value?).toBeCloseTo(number, numDigits?).toBeDefined().toBeFalsy().toBeGreaterThan(number | bigint).toBeGreaterThanOrEqual(number | bigint).toBeLessThan(number | bigint).toBeLessThanOrEqual(number | bigint).toBeInstanceOf(Class).toBeNull().toBeTruthy().toBeUndefined().toBeNaN().toContain(item).toContainEqual(item).toEqual(value).toMatch(regexp | string).toMatchObject(object).toMatchSnapshot(propertyMatchers?, hint?).toMatchInlineSnapshot(propertyMatchers?, inlineSnapshot).toStrictEqual(value).toThrow(error?).toThrowErrorMatchingSnapshot(hint?).toThrowErrorMatchingInlineSnapshot(inlineSnapshot)
-
Асимметричные сопоставители
expect.anything()expect.any(constructor)expect.arrayContaining(array)expect.not.arrayContaining(array)expect.arrayOf(value)expect.not.arrayOf(value)expect.closeTo(number, numDigits?)expect.objectContaining(object)expect.not.objectContaining(object)expect.stringContaining(string)expect.not.stringContaining(string)expect.stringMatching(string | regexp)expect.not.stringMatching(string | regexp)
- Количество проверок
- Утилиты расширения
- Состояние сопоставителя
- Сериализуемые свойства
Expect
expect(value)
Функция expect используется каждый раз, когда вы хотите протестировать значение. Вы редко вызываете expect отдельно. Вместо этого вы используете expect вместе с функцией-сопоставителем, чтобы проверить некоторое утверждение о значении.
Проще понять это на примере. Допустим, у вас есть метод bestLaCroixFlavor(), который должен возвращать строку 'grapefruit'. Вот как можно это протестировать:
test('the best flavor is grapefruit', () => {
expect(bestLaCroixFlavor()).toBe('grapefruit');
});
В этом случае toBe — функция-сопоставитель. Существует множество различных функций-сопоставителей, описанных ниже, которые помогут вам проверять разные вещи.
Аргументом expect должно быть значение, полученное в результате выполнения вашего кода, а аргументом сопоставителя — правильное значение. Если перепутать их местами, тесты всё равно будут работать, но сообщения об ошибках при их падении будут выглядеть странно.
Модификаторы
.not
Если вы знаете, как проверить что-либо, .not позволяет проверить противоположное. Например, этот код проверяет, что лучший вкус La Croix — не кокос:
test('the best flavor is not coconut', () => {
expect(bestLaCroixFlavor()).not.toBe('coconut');
});
.resolves
Используйте resolves, чтобы извлечь значение из выполненного промиса и применить к нему любой другой сопоставитель. Если промис отклонен, проверка завершается ошибкой.
Например, этот код проверяет, что промис выполняется, а полученное значение равно 'lemon':
test('resolves to lemon', () => {
// make sure to add a return statement
return expect(Promise.resolve('lemon')).resolves.toBe('lemon');
});
Поскольку вы всё ещё тестируете промисы, тест остается асинхронным. Поэтому необходимо указать Jest, что нужно дождаться результата, вернув проверку извлеченного значения.
Кроме того, можно использовать async/await вместе с .resolves:
test('resolves to lemon', async () => {
await expect(Promise.resolve('lemon')).resolves.toBe('lemon');
await expect(Promise.resolve('lemon')).resolves.not.toBe('octopus');
});
.rejects
Используйте .rejects, чтобы извлечь причину отклонения промиса и применить к ней любой другой сопоставитель. Если промис выполнен, проверка завершается ошибкой.
Например, этот код проверяет, что промис отклоняется с причиной 'octopus':
test('rejects to octopus', () => {
// make sure to add a return statement
return expect(Promise.reject(new Error('octopus'))).rejects.toThrow(
'octopus',
);
});
Поскольку вы всё ещё тестируете промисы, тест остается асинхронным. Поэтому необходимо указать Jest, что нужно дождаться результата, вернув проверку извлеченного значения.
Кроме того, можно использовать async/await вместе с .rejects.
test('rejects to octopus', async () => {
await expect(Promise.reject(new Error('octopus'))).rejects.toThrow('octopus');
});
Сопоставители
.toBe(value)
Используйте .toBe для сравнения примитивных значений или проверки ссылочной идентичности экземпляров объектов. Он вызывает Object.is для сравнения значений, что для тестирования даже лучше, чем оператор строгого равенства ===.
Например, этот код проверит некоторые свойства объекта can:
const can = {
name: 'pamplemousse',
ounces: 12,
};
describe('the can', () => {
test('has 12 ounces', () => {
expect(can.ounces).toBe(12);
});
test('has a sophisticated name', () => {
expect(can.name).toBe('pamplemousse');
});
});
Не используйте .toBe с числами с плавающей точкой. Например, из-за округления в JavaScript 0.2 + 0.1 не равно строго 0.3. Если вы работаете с числами с плавающей точкой, попробуйте вместо этого .toBeCloseTo.
Хотя сопоставитель .toBe проверяет ссылочную идентичность, при сбое проверки он выводит результат глубокого сравнения значений. Если различия между свойствами не помогают понять, почему тест завершился ошибкой, особенно если отчет большой, сравнение можно перенести в функцию expect. Например, чтобы проверить, являются ли элементы одним и тем же экземпляром:
- замените
expect(received).toBe(expected)наexpect(Object.is(received, expected)).toBe(true) - замените
expect(received).not.toBe(expected)наexpect(Object.is(received, expected)).toBe(false)
.toHaveBeenCalled()
Используйте .toHaveBeenCalled, чтобы проверить, что макетная функция была вызвана.
Допустим, у вас есть функция drinkAll(drink, flavour), которая принимает функцию drink и применяет её ко всем доступным напиткам. Возможно, вы захотите проверить, что вызывается drink. Это можно сделать с помощью такого набора тестов:
function drinkAll(callback, flavour) {
if (flavour !== 'octopus') {
callback(flavour);
}
}
describe('drinkAll', () => {
test('drinks something lemon-flavoured', () => {
const drink = jest.fn();
drinkAll(drink, 'lemon');
expect(drink).toHaveBeenCalled();
});
test('does not drink something octopus-flavoured', () => {
const drink = jest.fn();
drinkAll(drink, 'octopus');
expect(drink).not.toHaveBeenCalled();
});
});
.toHaveBeenCalledTimes(number)
Используйте .toHaveBeenCalledTimes, чтобы проверить, что макетная функция была вызвана точное число раз.
Допустим, у вас есть функция drinkEach(drink, Array<flavor>), которая принимает функцию drink и применяет её к массиву переданных напитков. Возможно, вы захотите проверить, что функция drink была вызвана точное число раз. Это можно сделать с помощью такого набора тестов:
test('drinkEach drinks each drink', () => {
const drink = jest.fn();
drinkEach(drink, ['lemon', 'octopus']);
expect(drink).toHaveBeenCalledTimes(2);
});
.toHaveBeenCalledWith(arg1, arg2, ...)
Используйте .toHaveBeenCalledWith, чтобы проверить, что макетная функция была вызвана с определенными аргументами. Аргументы проверяются тем же алгоритмом, который использует .toEqual.
Например, допустим, напиток можно зарегистрировать с помощью функции register, а applyToAll(f) должна применить функцию f ко всем зарегистрированным напиткам. Чтобы убедиться, что это работает, можно написать:
test('registration applies correctly to orange La Croix', () => {
const beverage = new LaCroix('orange');
register(beverage);
const f = jest.fn();
applyToAll(f);
expect(f).toHaveBeenCalledWith(beverage);
});
.toHaveBeenLastCalledWith(arg1, arg2, ...)
Если у вас есть макетная функция, можно использовать .toHaveBeenLastCalledWith, чтобы проверить, с какими аргументами она была вызвана в последний раз. Например, допустим, у вас есть функция applyToAllFlavors(f), которая применяет f к нескольким вкусам, и вы хотите убедиться, что при её вызове последним обработанным вкусом будет 'mango'. Можно написать:
test('applying to all flavors does mango last', () => {
const drink = jest.fn();
applyToAllFlavors(drink);
expect(drink).toHaveBeenLastCalledWith('mango');
});
.toHaveBeenNthCalledWith(nthCall, arg1, arg2, ....)
Если у вас есть макетная функция, можно использовать .toHaveBeenNthCalledWith, чтобы проверить, с какими аргументами она была вызвана в n-й раз. Например, допустим, у вас есть функция drinkEach(drink, Array<flavor>), которая применяет f к нескольким вкусам, и вы хотите убедиться, что при её вызове первым обработанным вкусом будет 'lemon', а вторым — 'octopus'. Можно написать:
test('drinkEach drinks each drink', () => {
const drink = jest.fn();
drinkEach(drink, ['lemon', 'octopus']);
expect(drink).toHaveBeenNthCalledWith(1, 'lemon');
expect(drink).toHaveBeenNthCalledWith(2, 'octopus');
});
Номер аргумента должен быть положительным целым числом, начиная с 1.
.toHaveReturned()
Если у вас есть макетная функция, можно использовать .toHaveReturned, чтобы проверить, что она хотя бы один раз успешно вернула результат (то есть не выбросила ошибку). Например, допустим, у вас есть макетная функция drink, которая возвращает true. Можно написать:
test('drinks returns', () => {
const drink = jest.fn(() => true);
drink();
expect(drink).toHaveReturned();
});
.toHaveReturnedTimes(number)
Используйте .toHaveReturnedTimes, чтобы проверить, что макетная функция успешно вернула результат (то есть не выбросила ошибку) точное число раз. Вызовы макетной функции, завершившиеся выбросом ошибки, не учитываются при подсчете успешных возвратов.
Например, допустим, у вас есть макетная функция drink, которая возвращает true. Можно написать:
test('drink returns twice', () => {
const drink = jest.fn(() => true);
drink();
drink();
expect(drink).toHaveReturnedTimes(2);
});
.toHaveReturnedWith(value)
Используйте .toHaveReturnedWith, чтобы проверить, что макетная функция вернула определенное значение.
Например, допустим, у вас есть макетная функция drink, которая возвращает название выпитого напитка. Можно написать:
test('drink returns La Croix', () => {
const beverage = {name: 'La Croix'};
const drink = jest.fn(beverage => beverage.name);
drink(beverage);
expect(drink).toHaveReturnedWith('La Croix');
});
.toHaveLastReturnedWith(value)
Используйте .toHaveLastReturnedWith, чтобы проверить конкретное значение, возвращенное макетной функцией при последнем вызове. Если последний вызов макетной функции завершился выбросом ошибки, этот сопоставитель завершит проверку ошибкой независимо от указанного ожидаемого значения.
Например, допустим, у вас есть макетная функция drink, которая возвращает название выпитого напитка. Можно написать:
test('drink returns La Croix (Orange) last', () => {
const beverage1 = {name: 'La Croix (Lemon)'};
const beverage2 = {name: 'La Croix (Orange)'};
const drink = jest.fn(beverage => beverage.name);
drink(beverage1);
drink(beverage2);
expect(drink).toHaveLastReturnedWith('La Croix (Orange)');
});
.toHaveNthReturnedWith(nthCall, value)
Используйте .toHaveNthReturnedWith, чтобы проверить конкретное значение, возвращенное макетной функцией при n-м вызове. Если n-й вызов макетной функции завершился выбросом ошибки, этот сопоставитель завершит проверку ошибкой независимо от указанного ожидаемого значения.
Например, допустим, у вас есть макетная функция drink, которая возвращает название выпитого напитка. Можно написать:
test('drink returns expected nth calls', () => {
const beverage1 = {name: 'La Croix (Lemon)'};
const beverage2 = {name: 'La Croix (Orange)'};
const drink = jest.fn(beverage => beverage.name);
drink(beverage1);
drink(beverage2);
expect(drink).toHaveNthReturnedWith(1, 'La Croix (Lemon)');
expect(drink).toHaveNthReturnedWith(2, 'La Croix (Orange)');
});
Номер аргумента должен быть положительным целым числом, начиная с 1.
.toHaveLength(number)
Используйте .toHaveLength, чтобы проверить, что у объекта есть свойство .length с определенным числовым значением.
Это особенно полезно для проверки размера массивов или строк.
expect([1, 2, 3]).toHaveLength(3);
expect('abc').toHaveLength(3);
expect('').not.toHaveLength(5);
.toHaveProperty(keyPath, value?)
Используйте .toHaveProperty, чтобы проверить, существует ли у объекта свойство по указанному пути keyPath. Для проверки глубоко вложенных свойств объекта можно использовать точечную нотацию или массив, содержащий путь keyPath к вложенному свойству.
Можно указать необязательный аргумент value, чтобы сравнить полученное значение свойства (рекурсивно для всех свойств экземпляров объектов; это также называется глубоким равенством, как в сопоставителе toEqual).
В следующем примере объект houseForSale содержит вложенные свойства. Мы используем toHaveProperty, чтобы проверить наличие различных свойств объекта и их значения.
// Object containing house features to be tested
const houseForSale = {
bath: true,
bedrooms: 4,
kitchen: {
amenities: ['oven', 'stove', 'washer'],
area: 20,
wallColor: 'white',
'nice.oven': true,
},
livingroom: {
amenities: [
{
couch: [
['large', {dimensions: [20, 20]}],
['small', {dimensions: [10, 10]}],
],
},
],
},
'ceiling.height': 2,
};
test('this house has my desired features', () => {
// Example Referencing
expect(houseForSale).toHaveProperty('bath');
expect(houseForSale).toHaveProperty('bedrooms', 4);
expect(houseForSale).not.toHaveProperty('pool');
// Deep referencing using dot notation
expect(houseForSale).toHaveProperty('kitchen.area', 20);
expect(houseForSale).toHaveProperty('kitchen.amenities', [
'oven',
'stove',
'washer',
]);
expect(houseForSale).not.toHaveProperty('kitchen.open');
// Deep referencing using an array containing the keyPath
expect(houseForSale).toHaveProperty(['kitchen', 'area'], 20);
expect(houseForSale).toHaveProperty(
['kitchen', 'amenities'],
['oven', 'stove', 'washer'],
);
expect(houseForSale).toHaveProperty(['kitchen', 'amenities', 0], 'oven');
expect(houseForSale).toHaveProperty(
'livingroom.amenities[0].couch[0][1].dimensions[0]',
20,
);
expect(houseForSale).toHaveProperty(['kitchen', 'nice.oven']);
expect(houseForSale).not.toHaveProperty(['kitchen', 'open']);
// Referencing keys with dot in the key itself
expect(houseForSale).toHaveProperty(['ceiling.height'], 2);
});
.toBeCloseTo(number, numDigits?)
Используйте toBeCloseTo для приблизительного сравнения чисел с плавающей точкой.
Необязательный аргумент numDigits ограничивает количество цифр для проверки после десятичной точки. Для значения по умолчанию 2 критерий проверки равен Math.abs(expected - received) < 0.005 (то есть 10 ** -2 / 2).
Интуитивно понятные сравнения на равенство часто не работают, поскольку при арифметических операциях с десятичными значениями (основание 10) часто возникают ошибки округления из-за ограниченной точности двоичного представления (основание 2). Например, этот тест завершится ошибкой:
test('adding works sanely with decimals', () => {
expect(0.2 + 0.1).toBe(0.3); // Fails!
});
Он завершается ошибкой, потому что в JavaScript значение 0.2 + 0.1 на самом деле равно 0.30000000000000004.
Например, этот тест проходит при точности в 5 знаков:
test('adding works sanely with decimals', () => {
expect(0.2 + 0.1).toBeCloseTo(0.3, 5);
});
Поскольку toBeCloseTo предназначен для решения проблемы ошибок чисел с плавающей точкой, он не поддерживает значения больших целых чисел.
.toBeDefined()
Используйте .toBeDefined, чтобы проверить, что переменная не равна undefined. Например, если вы хотите проверить, что функция fetchNewFlavorIdea() возвращает что-нибудь, можно написать:
test('there is a new flavor idea', () => {
expect(fetchNewFlavorIdea()).toBeDefined();
});
Можно написать expect(fetchNewFlavorIdea()).not.toBe(undefined), но лучше не обращаться напрямую к undefined в коде.
.toBeFalsy()
Используйте .toBeFalsy, если вам не важно, чему равно значение, и вы хотите убедиться, что в логическом контексте оно ложно. Например, допустим, у вас есть код приложения, который выглядит так:
drinkSomeLaCroix();
if (!getErrors()) {
drinkMoreLaCroix();
}
Возможно, вам не важно, что именно возвращает getErrors: это может быть false, null или 0, и код всё равно будет работать. Поэтому, если вы хотите проверить, что после употребления La Croix не возникает ошибок, можно написать:
test('drinking La Croix does not lead to errors', () => {
drinkSomeLaCroix();
expect(getErrors()).toBeFalsy();
});
В JavaScript есть шесть ложных значений: false, 0, '', null, undefined и NaN. Все остальные значения истинны.
.toBeGreaterThan(number | bigint)
Используйте toBeGreaterThan, чтобы сравнить received > expected для числовых значений или значений больших целых чисел. Например, проверьте, что ouncesPerCan() возвращает значение больше 10 унций:
test('ounces per can is more than 10', () => {
expect(ouncesPerCan()).toBeGreaterThan(10);
});
.toBeGreaterThanOrEqual(number | bigint)
Используйте toBeGreaterThanOrEqual, чтобы сравнить received >= expected для числовых значений или значений больших целых чисел. Например, проверьте, что ouncesPerCan() возвращает значение не меньше 12 унций:
test('ounces per can is at least 12', () => {
expect(ouncesPerCan()).toBeGreaterThanOrEqual(12);
});
.toBeLessThan(number | bigint)
Используйте toBeLessThan, чтобы сравнить received < expected для числовых значений или значений больших целых чисел. Например, проверьте, что ouncesPerCan() возвращает значение меньше 20 унций:
test('ounces per can is less than 20', () => {
expect(ouncesPerCan()).toBeLessThan(20);
});
.toBeLessThanOrEqual(number | bigint)
Используйте toBeLessThanOrEqual, чтобы сравнить received <= expected для числовых значений или значений больших целых чисел. Например, проверьте, что ouncesPerCan() возвращает значение не больше 12 унций:
test('ounces per can is at most 12', () => {
expect(ouncesPerCan()).toBeLessThanOrEqual(12);
});
.toBeInstanceOf(Class)
Используйте .toBeInstanceOf(Class), чтобы проверить, что объект является экземпляром класса. В основе этого сопоставителя лежит instanceof.
class A {}
expect(new A()).toBeInstanceOf(A);
expect(() => {}).toBeInstanceOf(Function);
expect(new A()).toBeInstanceOf(Function); // throws
.toBeNull()
.toBeNull() работает так же, как .toBe(null), но сообщения об ошибках немного понятнее. Поэтому используйте .toBeNull(), когда хотите проверить, что значение равно null.
function bloop() {
return null;
}
test('bloop returns null', () => {
expect(bloop()).toBeNull();
});
.toBeTruthy()
Используйте .toBeTruthy, если вам не важно, чему равно значение, и вы хотите убедиться, что в логическом контексте оно истинно. Например, допустим, у вас есть код приложения, который выглядит так:
drinkSomeLaCroix();
if (thirstInfo()) {
drinkMoreLaCroix();
}
Возможно, вам не важно, что именно возвращает thirstInfo: это может быть true или сложный объект, и код всё равно будет работать. Поэтому, если вы хотите проверить, что после употребления La Croix значение thirstInfo будет истинным, можно написать:
test('drinking La Croix leads to having thirst info', () => {
drinkSomeLaCroix();
expect(thirstInfo()).toBeTruthy();
});
В JavaScript есть шесть ложных значений: false, 0, '', null, undefined и NaN. Все остальные значения истинны.
.toBeUndefined()
Используйте .toBeUndefined, чтобы проверить, что переменная равна undefined. Например, если вы хотите проверить, что функция bestDrinkForFlavor(flavor) возвращает undefined для вкуса 'octopus', поскольку напитков с хорошим вкусом осьминога не существует:
test('the best drink for octopus flavor is undefined', () => {
expect(bestDrinkForFlavor('octopus')).toBeUndefined();
});
Можно написать expect(bestDrinkForFlavor('octopus')).toBe(undefined), но лучше не обращаться напрямую к undefined в коде.
.toBeNaN()
Используйте .toBeNaN при проверке того, что значение равно NaN.
test('passes when value is NaN', () => {
expect(NaN).toBeNaN();
expect(1).not.toBeNaN();
});
.toContain(item)
Используйте .toContain, если хотите проверить, что элемент содержится в массиве. Для проверки элементов массива используется === — проверка строгого равенства. .toContain также может проверить, является ли одна строка подстрокой другой.
Например, если getAllFlavors() возвращает массив вкусов и вы хотите убедиться, что в нем есть lime, можно написать:
test('the flavor list contains lime', () => {
expect(getAllFlavors()).toContain('lime');
});
Этот сопоставитель также принимает другие итерируемые объекты, например строки, множества, списки узлов и коллекции HTML-элементов.
.toContainEqual(item)
Используйте .toContainEqual, если хотите проверить, что в массиве содержится элемент с определенной структурой и значениями. При проверке элементов массива сопоставитель рекурсивно проверяет равенство всех полей, а не идентичность объектов.
describe('my beverage', () => {
test('is delicious and not sour', () => {
const myBeverage = {delicious: true, sour: false};
expect(myBeverages()).toContainEqual(myBeverage);
});
});
.toEqual(value)
Используйте .toEqual для рекурсивного сравнения всех свойств экземпляров объектов (также известного как «глубокое» равенство). Он вызывает Object.is для сравнения примитивных значений, что для тестирования даже лучше, чем оператор строгого равенства ===.
Например, .toEqual и .toBe ведут себя в этом наборе тестов по-разному, поэтому все тесты проходят:
const can1 = {
flavor: 'grapefruit',
ounces: 12,
};
const can2 = {
flavor: 'grapefruit',
ounces: 12,
};
describe('the La Croix cans on my desk', () => {
test('have all the same properties', () => {
expect(can1).toEqual(can2);
});
test('are not the exact same can', () => {
expect(can1).not.toBe(can2);
});
});
toEqual не учитывает ключи объектов со свойствами undefined, элементы undefined, разреженность массивов и несовпадение типов объектов. Чтобы учитывать их, используйте вместо этого .toStrictEqual.
.toEqual не выполняет проверку глубокого равенства двух ошибок. Для проверки равенства учитывается только свойство message объекта Error. Для сравнения ошибок рекомендуется использовать сопоставитель .toThrow.
Если различия между свойствами не помогают понять, почему тест завершился ошибкой, особенно если отчет большой, сравнение можно перенести в функцию expect. Например, используйте метод equals класса Buffer, чтобы проверить, содержат ли буферы одинаковые данные:
- замените
expect(received).toEqual(expected)наexpect(received.equals(expected)).toBe(true) - замените
expect(received).not.toEqual(expected)наexpect(received.equals(expected)).toBe(false)
.toMatch(regexp | string)
Используйте .toMatch, чтобы проверить соответствие строки регулярному выражению.
Например, вы можете не знать, что именно возвращает essayOnTheBestFlavor(), но знаете, что это очень длинная строка и где-то в ней должна быть подстрока grapefruit. Это можно проверить так:
describe('an essay on the best flavor', () => {
test('mentions grapefruit', () => {
expect(essayOnTheBestFlavor()).toMatch(/grapefruit/);
expect(essayOnTheBestFlavor()).toMatch(new RegExp('grapefruit'));
});
});
Этот сопоставитель также принимает строку, соответствие которой он будет проверять:
describe('grapefruits are healthy', () => {
test('grapefruits are a fruit', () => {
expect('grapefruits').toMatch('fruit');
});
});
.toMatchObject(object)
Используйте .toMatchObject, чтобы проверить, соответствует ли объект JavaScript подмножеству свойств другого объекта. Он сочтет полученные объекты соответствующими, даже если в них есть свойства, которых нет в ожидаемом объекте.
Можно также передать массив объектов; в этом случае метод вернет true, только если каждый объект полученного массива соответствует (в смысле toMatchObject, описанном выше) соответствующему объекту ожидаемого массива. Это полезно, если нужно проверить, что два массива содержат одинаковое количество элементов, в отличие от arrayContaining, который допускает наличие дополнительных элементов в полученном массиве.
Свойства можно сопоставлять со значениями или с сопоставителями.
const houseForSale = {
bath: true,
bedrooms: 4,
kitchen: {
amenities: ['oven', 'stove', 'washer'],
area: 20,
wallColor: 'white',
},
};
const desiredHouse = {
bath: true,
kitchen: {
amenities: ['oven', 'stove', 'washer'],
wallColor: expect.stringMatching(/white|yellow/),
},
};
test('the house has my desired features', () => {
expect(houseForSale).toMatchObject(desiredHouse);
});
describe('toMatchObject applied to arrays', () => {
test('the number of elements must match exactly', () => {
expect([{foo: 'bar'}, {baz: 1}]).toMatchObject([{foo: 'bar'}, {baz: 1}]);
});
test('.toMatchObject is called for each elements, so extra object properties are okay', () => {
expect([{foo: 'bar'}, {baz: 1, extra: 'quux'}]).toMatchObject([
{foo: 'bar'},
{baz: 1},
]);
});
});
.toMatchSnapshot(propertyMatchers?, hint?)
Этот сопоставитель проверяет, соответствует ли значение последнему снимку. Дополнительные сведения см. в руководстве по тестированию с помощью снимков.
Можно передать необязательный объект propertyMatchers со значениями-сопоставителями для асимметричного сравнения подмножества ожидаемых свойств, если полученное значение является экземпляром объекта. Это похоже на toMatchObject с гибкими критериями для подмножества свойств, после чего остальные свойства проверяются по снимку с точным совпадением.
Можно передать необязательную строку hint, которая будет добавлена к имени теста. Хотя Jest всегда добавляет в конец имени снимка номер, короткие описательные подсказки могут быть полезнее номеров для различения нескольких снимков в пределах одного блока it или test. Jest сортирует снимки по имени в соответствующем файле .snap.
.toMatchInlineSnapshot(propertyMatchers?, inlineSnapshot)
Проверяет, соответствует ли значение последнему снимку.
Можно передать необязательный объект propertyMatchers со значениями-сопоставителями для асимметричного сравнения подмножества ожидаемых свойств, если полученное значение является экземпляром объекта. Это похоже на toMatchObject с гибкими критериями для подмножества свойств, после чего остальные свойства проверяются по снимку с точным совпадением.
При первом запуске теста Jest добавляет строковый аргумент inlineSnapshot к сопоставителю в файле теста (вместо отдельного файла .snap).
Дополнительные сведения см. в разделе Встроенные снимки.
.toStrictEqual(value)
Используйте .toStrictEqual, чтобы проверить, что объекты имеют одинаковую структуру и тип.
Отличия от .toEqual:
- проверяются ключи со свойствами
undefined; например,{a: undefined, b: 2}не будет равен{b: 2}; -
учитываются элементы
undefined; например,[2]не будет равен[2, undefined]; - проверяется разреженность массива; например,
[, 1]не будет равен[undefined, 1]; - проверяются типы объектов; например, экземпляр класса с полями
aиbне будет равен литеральному объекту с полямиaиb.
class LaCroix {
constructor(flavor) {
this.flavor = flavor;
}
}
describe('the La Croix cans on my desk', () => {
test('are not semantically the same', () => {
expect(new LaCroix('lemon')).toEqual({flavor: 'lemon'});
expect(new LaCroix('lemon')).not.toStrictEqual({flavor: 'lemon'});
});
});
.toThrow(error?)
Используйте .toThrow, чтобы проверить, выбрасывает ли функция ошибку при вызове. Например, если мы хотим проверить, что drinkFlavor('octopus') выбрасывает ошибку, поскольку напиток со вкусом осьминога слишком отвратителен, чтобы его пить, можно написать:
test('throws on octopus', () => {
expect(() => {
drinkFlavor('octopus');
}).toThrow();
});
Необходимо поместить код в функцию, иначе ошибка не будет перехвачена и проверка завершится неудачей.
Можно указать необязательный аргумент, чтобы проверить, что выбрасывается определённая ошибка:
- регулярное выражение: сообщение об ошибке соответствует шаблону
- строка: сообщение об ошибке содержит подстроку
- объект ошибки: сообщение об ошибке равно свойству сообщения объекта
- класс ошибки: объект ошибки является экземпляром класса
Например, предположим, что drinkFlavor написана так:
function drinkFlavor(flavor) {
if (flavor === 'octopus') {
throw new DisgustingFlavorError('yuck, octopus flavor');
}
// Do some other stuff
}
Проверить, что эта ошибка выбрасывается, можно несколькими способами:
test('throws on octopus', () => {
function drinkOctopus() {
drinkFlavor('octopus');
}
// Test that the error message says "yuck" somewhere: these are equivalent
expect(drinkOctopus).toThrow(/yuck/);
expect(drinkOctopus).toThrow('yuck');
// Test the exact error message
expect(drinkOctopus).toThrow(/^yuck, octopus flavor$/);
expect(drinkOctopus).toThrow(new Error('yuck, octopus flavor'));
// Test that we get a DisgustingFlavorError
expect(drinkOctopus).toThrow(DisgustingFlavorError);
});
.toThrowErrorMatchingSnapshot(hint?)
Используйте .toThrowErrorMatchingSnapshot, чтобы проверить, что при вызове функция выбрасывает ошибку, соответствующую последнему снимку.
Можно указать необязательный строковый аргумент hint, который добавляется к имени теста. Хотя Jest всегда добавляет число в конец имени снимка, короткие описательные подсказки могут быть полезнее чисел для различения нескольких снимков в рамках одного блока it или test. Jest сортирует снимки по имени в соответствующем файле .snap.
Например, предположим, у вас есть функция drinkFlavor, которая выбрасывает ошибку, если вкус равен 'octopus', и написана так:
function drinkFlavor(flavor) {
if (flavor === 'octopus') {
throw new DisgustingFlavorError('yuck, octopus flavor');
}
// Do some other stuff
}
Тест этой функции будет выглядеть так:
test('throws on octopus', () => {
function drinkOctopus() {
drinkFlavor('octopus');
}
expect(drinkOctopus).toThrowErrorMatchingSnapshot();
});
В результате будет создан следующий снимок:
exports[`drinking flavors throws on octopus 1`] = `"yuck, octopus flavor"`;
Дополнительную информацию о тестировании снимков см. в статье Тестирование снимков дерева React.
.toThrowErrorMatchingInlineSnapshot(inlineSnapshot)
Используйте .toThrowErrorMatchingInlineSnapshot, чтобы проверить, что при вызове функция выбрасывает ошибку, соответствующую последнему снимку.
При первом запуске теста Jest добавляет строковый аргумент inlineSnapshot непосредственно к матчерy в файле теста (а не во внешний файл .snap).
Дополнительную информацию см. в разделе Встроенные снимки.
Асимметричные матчеры
expect.anything()
expect.anything() соответствует любому значению, кроме null или undefined. Его можно использовать внутри toEqual или toHaveBeenCalledWith вместо литерального значения. Например, если нужно проверить, что мок-функция вызвана с аргументом, отличным от null:
test('map calls its argument with a non-null argument', () => {
const mock = jest.fn();
[1].map(x => mock(x));
expect(mock).toHaveBeenCalledWith(expect.anything());
});
expect.any(constructor)
expect.any(constructor) соответствует всему, что было создано с помощью указанного конструктора, а также примитивам переданного типа. Его можно использовать внутри toEqual или toHaveBeenCalledWith вместо литерального значения. Например, если нужно проверить, что мок-функция вызвана с числом:
class Cat {}
function getCat(fn) {
return fn(new Cat());
}
test('randocall calls its callback with a class instance', () => {
const mock = jest.fn();
getCat(mock);
expect(mock).toHaveBeenCalledWith(expect.any(Cat));
});
function randocall(fn) {
return fn(Math.floor(Math.random() * 6 + 1));
}
test('randocall calls its callback with a number', () => {
const mock = jest.fn();
randocall(mock);
expect(mock).toHaveBeenCalledWith(expect.any(Number));
});
expect.arrayContaining(array)
expect.arrayContaining(array) соответствует полученному массиву, содержащему все элементы ожидаемого массива. Иными словами, ожидаемый массив является подмножеством полученного массива. Поэтому он соответствует полученному массиву, который содержит элементы, не входящие в ожидаемый массив.
Его можно использовать вместо литерального значения:
- в
toEqualилиtoHaveBeenCalledWith - для сопоставления свойства в
objectContainingилиtoMatchObject
describe('arrayContaining', () => {
const expected = ['Alice', 'Bob'];
it('matches even if received contains additional elements', () => {
expect(['Alice', 'Bob', 'Eve']).toEqual(expect.arrayContaining(expected));
});
it('does not match if received does not contain expected elements', () => {
expect(['Bob', 'Eve']).not.toEqual(expect.arrayContaining(expected));
});
});
describe('Beware of a misunderstanding! A sequence of dice rolls', () => {
const expected = [1, 2, 3, 4, 5, 6];
it('matches even with an unexpected number 7', () => {
expect([4, 1, 6, 7, 3, 5, 2, 5, 4, 6]).toEqual(
expect.arrayContaining(expected),
);
});
it('does not match without an expected number 2', () => {
expect([4, 1, 6, 7, 3, 5, 7, 5, 4, 6]).not.toEqual(
expect.arrayContaining(expected),
);
});
});
expect.not.arrayContaining(array)
expect.not.arrayContaining(array) соответствует полученному массиву, который не содержит все элементы ожидаемого массива. Иными словами, ожидаемый массив не является подмножеством полученного массива.
Это противоположность expect.arrayContaining.
describe('not.arrayContaining', () => {
const expected = ['Samantha'];
it('matches if the actual array does not contain the expected elements', () => {
expect(['Alice', 'Bob', 'Eve']).toEqual(
expect.not.arrayContaining(expected),
);
});
});
expect.arrayOf(value)
expect.arrayOf(value) соответствует полученному массиву, элементы которого соответствуют заданному значению. Это полезно для проверки того, что каждый элемент массива удовлетворяет определённому условию или типу.
Пример:
test('all elements in array are strings', () => {
expect(['apple', 'banana', 'cherry']).toEqual(
expect.arrayOf(expect.any(String)),
);
});
Этот матчер особенно полезен для проверки массивов, содержащих сложные структуры:
test('array of objects with specific properties', () => {
expect([
{id: 1, name: 'Alice'},
{id: 2, name: 'Bob'},
]).toEqual(
expect.arrayOf(
expect.objectContaining({
id: expect.any(Number),
name: expect.any(String),
}),
),
);
});
expect.not.arrayOf(value)
expect.not.arrayOf(value) соответствует полученному массиву, в котором не все элементы соответствуют заданному матчерy.
Пример:
test('not all elements in array are strings', () => {
expect(['apple', 123, 'cherry']).toEqual(
expect.not.arrayOf(expect.any(String)),
);
});
expect.closeTo(number, numDigits?)
expect.closeTo(number, numDigits?) полезен для сравнения чисел с плавающей точкой в свойствах объектов или элементах массива. Для сравнения чисел используйте .toBeCloseTo.
Необязательный аргумент numDigits ограничивает количество проверяемых цифр после десятичной точки. Для значения по умолчанию 2 критерий теста — Math.abs(expected - received) < 0.005 (that is, 10 ** -2 / 2).
Например, этот тест проходит при точности в 5 цифр:
test('compare float in object properties', () => {
expect({
title: '0.1 + 0.2',
sum: 0.1 + 0.2,
}).toEqual({
title: '0.1 + 0.2',
sum: expect.closeTo(0.3, 5),
});
});
expect.objectContaining(object)
expect.objectContaining(object) соответствует любому полученному объекту, рекурсивно соответствующему ожидаемым свойствам. Иными словами, ожидаемый объект является подмножеством полученного объекта. Поэтому он соответствует полученному объекту, содержащему свойства, которые присутствуют в ожидаемом объекте.
Вместо литеральных значений свойств ожидаемого объекта можно использовать матчеры, expect.anything() и так далее.
Например, предположим, что мы ожидаем вызов функции onPress с объектом Event и хотим проверить только наличие у события свойств event.x и event.y. Это можно сделать так:
test('onPress gets called with the right thing', () => {
const onPress = jest.fn();
simulatePresses(onPress);
expect(onPress).toHaveBeenCalledWith(
expect.objectContaining({
x: expect.any(Number),
y: expect.any(Number),
}),
);
});
expect.not.objectContaining(object)
expect.not.objectContaining(object) соответствует любому полученному объекту, который рекурсивно не соответствует ожидаемым свойствам. Иными словами, ожидаемый объект не является подмножеством полученного объекта. Поэтому он соответствует полученному объекту, содержащему свойства, которых нет в ожидаемом объекте.
Это противоположность expect.objectContaining.
describe('not.objectContaining', () => {
const expected = {foo: 'bar'};
it('matches if the actual object does not contain expected key: value pairs', () => {
expect({bar: 'baz'}).toEqual(expect.not.objectContaining(expected));
});
});
expect.stringContaining(string)
expect.stringContaining(string) соответствует полученному значению, если оно является строкой, содержащей в точности ожидаемую строку.
expect.not.stringContaining(string)
expect.not.stringContaining(string) соответствует полученному значению, если оно не является строкой или если это строка, не содержащая в точности ожидаемую строку.
Это противоположность expect.stringContaining.
describe('not.stringContaining', () => {
const expected = 'Hello world!';
it('matches if the received value does not contain the expected substring', () => {
expect('How are you?').toEqual(expect.not.stringContaining(expected));
});
});
expect.stringMatching(string | regexp)
expect.stringMatching(string | regexp) соответствует полученному значению, если оно является строкой, соответствующей ожидаемой строке или регулярному выражению.
Его можно использовать вместо литерального значения:
- в
toEqualилиtoHaveBeenCalledWith - для сопоставления элемента в
arrayContaining - для сопоставления свойства в
objectContainingилиtoMatchObject
Этот пример также показывает, как можно вкладывать несколько асимметричных матчеров, используя expect.stringMatching внутри expect.arrayContaining.
describe('stringMatching in arrayContaining', () => {
const expected = [
expect.stringMatching(/^Alic/),
expect.stringMatching(/^[BR]ob/),
];
it('matches even if received contains additional elements', () => {
expect(['Alicia', 'Roberto', 'Evelina']).toEqual(
expect.arrayContaining(expected),
);
});
it('does not match if received does not contain expected elements', () => {
expect(['Roberto', 'Evelina']).not.toEqual(
expect.arrayContaining(expected),
);
});
});
expect.not.stringMatching(string | regexp)
expect.not.stringMatching(string | regexp) соответствует полученному значению, если оно не является строкой или если это строка, не соответствующая ожидаемой строке или регулярному выражению.
Это противоположность expect.stringMatching.
describe('not.stringMatching', () => {
const expected = /Hello world!/;
it('matches if the received value does not match the expected regex', () => {
expect('How are you?').toEqual(expect.not.stringMatching(expected));
});
});
Количество проверок
expect.assertions(number)
expect.assertions(number) проверяет, что во время теста вызвано определённое количество проверок. Это часто полезно при тестировании асинхронного кода, чтобы убедиться, что проверки в обратном вызове действительно были выполнены.
Например, предположим, что у нас есть функция doAsync, принимающая два обратных вызова callback1 и callback2 и асинхронно вызывающая их в неизвестном порядке. Это можно проверить так:
test('doAsync calls both callbacks', () => {
expect.assertions(2);
function callback1(data) {
expect(data).toBeTruthy();
}
function callback2(data) {
expect(data).toBeTruthy();
}
doAsync(callback1, callback2);
});
Вызов expect.assertions(2) гарантирует, что оба обратных вызова действительно будут вызваны.
expect.hasAssertions()
expect.hasAssertions() проверяет, что во время теста вызвана хотя бы одна проверка. Это часто полезно при тестировании асинхронного кода, чтобы убедиться, что проверки в обратном вызове действительно были выполнены.
Например, предположим, что у нас есть несколько функций, работающих с состоянием. prepareState вызывает обратный вызов с объектом состояния, validateState выполняется для этого объекта состояния, а waitOnState возвращает промис, который ожидает завершения всех обратных вызовов prepareState. Это можно проверить так:
test('prepareState prepares a valid state', () => {
expect.hasAssertions();
prepareState(state => {
expect(validateState(state)).toBeTruthy();
});
return waitOnState();
});
Вызов expect.hasAssertions() гарантирует, что обратный вызов prepareState действительно будет вызван.
Расширение утилит
expect.addEqualityTesters(testers)
С помощью expect.addEqualityTesters можно добавить собственные методы для проверки равенства двух объектов. Например, предположим, что в вашем коде есть класс, представляющий объём и определяющий, равны ли два объёма, заданные в разных единицах измерения. Возможно, вы захотите, чтобы toEqual (и другие матчеры равенства) использовал этот пользовательский метод сравнения при сравнении объектов класса Volume. Можно добавить пользовательский тестер равенства, чтобы toEqual обнаруживал объекты класса Volume и применял к ним пользовательскую логику сравнения:
- JavaScript
- TypeScript
// For simplicity in this example, we'll just support the units 'L' and 'mL'
export class Volume {
constructor(amount, unit) {
this.amount = amount;
this.unit = unit;
}
toString() {
return `[Volume ${this.amount}${this.unit}]`;
}
equals(other) {
if (this.unit === other.unit) {
return this.amount === other.amount;
} else if (this.unit === 'L' && other.unit === 'mL') {
return this.amount * 1000 === other.unit;
} else {
return this.amount === other.unit * 1000;
}
}
}
import {expect} from '@jest/globals';
import {Volume} from './Volume.js';
function areVolumesEqual(a, b) {
const isAVolume = a instanceof Volume;
const isBVolume = b instanceof Volume;
if (isAVolume && isBVolume) {
return a.equals(b);
} else if (isAVolume === isBVolume) {
return undefined;
} else {
return false;
}
}
expect.addEqualityTesters([areVolumesEqual]);
import {expect, test} from '@jest/globals';
import {Volume} from '../Volume.js';
import '../areVolumesEqual.js';
test('are equal with different units', () => {
expect(new Volume(1, 'L')).toEqual(new Volume(1000, 'mL'));
});
// For simplicity in this example, we'll just support the units 'L' and 'mL'
export class Volume {
public amount: number;
public unit: 'L' | 'mL';
constructor(amount: number, unit: 'L' | 'mL') {
this.amount = amount;
this.unit = unit;
}
toString(): string {
return `[Volume ${this.amount}${this.unit}]`;
}
equals(other: Volume): boolean {
if (this.unit === other.unit) {
return this.amount === other.amount;
} else if (this.unit === 'L' && other.unit === 'mL') {
return this.amount * 1000 === other.amount;
} else {
return this.amount === other.amount * 1000;
}
}
}
import {expect} from '@jest/globals';
import {Volume} from './Volume.js';
function areVolumesEqual(a: unknown, b: unknown): boolean | undefined {
const isAVolume = a instanceof Volume;
const isBVolume = b instanceof Volume;
if (isAVolume && isBVolume) {
return a.equals(b);
} else if (isAVolume === isBVolume) {
return undefined;
} else {
return false;
}
}
expect.addEqualityTesters([areVolumesEqual]);
import {expect, test} from '@jest/globals';
import {Volume} from '../Volume.js';
import '../areVolumesEqual.js';
test('are equal with different units', () => {
expect(new Volume(1, 'L')).toEqual(new Volume(1000, 'mL'));
});
API пользовательских тестеров равенства
Пользовательские тестеры — это функции, возвращающие результат (true или false) сравнения равенства двух заданных аргументов либо undefined, если тестер не обрабатывает переданные объекты и должен передать проверку равенства другим тестерам (например, встроенным тестерам равенства).
Пользовательские тестеры вызываются с тремя аргументами: двумя сравниваемыми объектами и массивом пользовательских тестеров (используется рекурсивными тестерами; см. следующий раздел).
Вспомогательные функции и свойства, доступные внутри пользовательского тестера, находятся в this:
this.equals(a, b, customTesters?)
Это функция глубокого сравнения, возвращающая true, если значения двух объектов совпадают (рекурсивно). При необходимости ей можно передать список пользовательских тестеров равенства, применяемых при глубоком сравнении. Если вы используете эту функцию, передайте ей пользовательские тестеры, полученные вашим тестером, чтобы дальнейшие проверки равенства, выполняемые equals, также могли использовать пользовательские тестеры, настроенные автором теста. Подробнее см. пример в разделе Рекурсивные пользовательские тестеры равенства.
Матчеры и тестеры
Матчеры — это методы, доступные в expect, например expect().toEqual(). toEqual — это матчер. Тестер — это метод, используемый матчерами, выполняющими проверку равенства, чтобы определить, одинаковы ли объекты.
Пользовательские матчеры полезны, когда нужно предоставить пользовательскую проверку, которую авторы тестов смогут использовать в своих тестах. Например, пример toBeWithinRange в разделе expect.extend — это хороший пример пользовательского матчера. Иногда автору теста нужно проверить точное равенство двух чисел — для этого следует использовать toBe. В других случаях автор теста может захотеть допустить некоторую гибкость, и тогда более подходящей проверкой будет toBeWithinRange.
Пользовательские тестеры равенства полезны для глобального расширения матчеров Jest собственной логикой сравнения равенства. Авторы тестов не могут включать пользовательские тестеры для одних проверок и отключать для других (если требуется такое поведение, следует использовать пользовательский матчер). Например, определение способа сравнения двух объектов Volume для всех матчеров — подходящий сценарий для пользовательского тестера равенства.
Рекурсивные пользовательские тестеры равенства
Если пользовательские тестеры равенства сравнивают объекты со свойствами, которые нужно сравнивать глубоко, следует использовать вспомогательную функцию this.equals, доступную тестерам равенства. Этот метод equals совпадает с методом глубокого сравнения, который Jest использует внутри для всех таких сравнений. Именно этот метод вызывает ваш пользовательский тестер равенства. Третьим аргументом он принимает массив пользовательских тестеров равенства. Пользовательские тестеры равенства также получают массив пользовательских тестеров третьим аргументом. Передайте этот аргумент третьим аргументом в equals, чтобы при дальнейших проверках равенства на более глубоких уровнях объекта тоже могли использоваться пользовательские тестеры равенства.
Например, предположим, у вас есть класс Book, содержащий массив объектов класса Author, и для обоих классов заданы пользовательские тестеры. Пользовательский тестер Book должен выполнить глубокое сравнение массива объектов Author и передать ему полученные пользовательские тестеры, чтобы применялся пользовательский тестер равенства для объектов Author:
function areAuthorEqual(a, b) {
const isAAuthor = a instanceof Author;
const isBAuthor = b instanceof Author;
if (isAAuthor && isBAuthor) {
// Authors are equal if they have the same name
return a.name === b.name;
} else if (isAAuthor === isBAuthor) {
return undefined;
} else {
return false;
}
}
function areBooksEqual(a, b, customTesters) {
const isABook = a instanceof Book;
const isBBook = b instanceof Book;
if (isABook && isBBook) {
// Books are the same if they have the same name and author array. We need
// to pass customTesters to equals here so the Author custom tester will be
// used when comparing Authors
return (
a.name === b.name && this.equals(a.authors, b.authors, customTesters)
);
} else if (isABook === isBBook) {
return undefined;
} else {
return false;
}
}
expect.addEqualityTesters([areAuthorsEqual, areBooksEqual]);
Объявляйте тестеры равенства как обычные функции, а не как стрелочные, чтобы получить доступ к вспомогательным функциям контекста тестера (например, this.equals).
expect.addSnapshotSerializer(serializer)
Можно вызвать expect.addSnapshotSerializer, чтобы добавить модуль для форматирования структур данных, специфичных для приложения.
Для отдельного файла теста добавленный модуль имеет приоритет над модулями из конфигурации snapshotSerializers, а те, в свою очередь, — над стандартными сериализаторами снимков для встроенных типов JavaScript и элементов React. Последний добавленный модуль проверяется первым.
import serializer from 'my-serializer-module'; expect.addSnapshotSerializer(serializer); // affects expect(value).toMatchSnapshot() assertions in the test file
Если добавить сериализатор снимков в отдельные файлы тестов, а не в конфигурацию snapshotSerializers:
- Зависимость будет явной, а не неявной.
- Вы избежите ограничений конфигурации, из-за которых может потребоваться отказаться от create-react-app.
Дополнительную информацию см. в разделе Настройка Jest.
expect.extend(matchers)
С помощью expect.extend можно добавлять собственные матчеры в Jest. Например, предположим, что вы тестируете библиотеку числовых утилит и часто проверяете, попадают ли числа в определённые диапазоны. Эту проверку можно вынести в матчер toBeWithinRange:
- JavaScript
- TypeScript
import {expect} from '@jest/globals';
function toBeWithinRange(actual, floor, ceiling) {
if (
typeof actual !== 'number' ||
typeof floor !== 'number' ||
typeof ceiling !== 'number'
) {
throw new TypeError('These must be of type number!');
}
const pass = actual >= floor && actual <= ceiling;
if (pass) {
return {
message: () =>
`expected ${this.utils.printReceived(
actual,
)} not to be within range ${this.utils.printExpected(
`${floor} - ${ceiling}`,
)}`,
pass: true,
};
} else {
return {
message: () =>
`expected ${this.utils.printReceived(
actual,
)} to be within range ${this.utils.printExpected(
`${floor} - ${ceiling}`,
)}`,
pass: false,
};
}
}
expect.extend({
toBeWithinRange,
});
import {expect, test} from '@jest/globals';
import '../toBeWithinRange';
test('is within range', () => expect(100).toBeWithinRange(90, 110));
test('is NOT within range', () => expect(101).not.toBeWithinRange(0, 100));
test('asymmetric ranges', () => {
expect({apples: 6, bananas: 3}).toEqual({
apples: expect.toBeWithinRange(1, 10),
bananas: expect.not.toBeWithinRange(11, 20),
});
});
// optionally add a type declaration, e.g. it enables autocompletion in IDEs
declare module 'expect' {
interface AsymmetricMatchers {
toBeWithinRange(floor: number, ceiling: number): void;
}
interface Matchers<R> {
toBeWithinRange(floor: number, ceiling: number): R;
}
}
export {};
import {expect} from '@jest/globals';
import type {MatcherFunction} from 'expect';
const toBeWithinRange: MatcherFunction<[floor: unknown, ceiling: unknown]> =
// `floor` and `ceiling` get types from the line above
// it is recommended to type them as `unknown` and to validate the values
function (actual, floor, ceiling) {
if (
typeof actual !== 'number' ||
typeof floor !== 'number' ||
typeof ceiling !== 'number'
) {
throw new TypeError('These must be of type number!');
}
const pass = actual >= floor && actual <= ceiling;
if (pass) {
return {
message: () =>
// `this` context will have correct typings
`expected ${this.utils.printReceived(
actual,
)} not to be within range ${this.utils.printExpected(
`${floor} - ${ceiling}`,
)}`,
pass: true,
};
} else {
return {
message: () =>
`expected ${this.utils.printReceived(
actual,
)} to be within range ${this.utils.printExpected(
`${floor} - ${ceiling}`,
)}`,
pass: false,
};
}
};
expect.extend({
toBeWithinRange,
});
declare module 'expect' {
interface AsymmetricMatchers {
toBeWithinRange(floor: number, ceiling: number): void;
}
interface Matchers<R> {
toBeWithinRange(floor: number, ceiling: number): R;
}
}
import {expect, test} from '@jest/globals';
import '../toBeWithinRange';
test('is within range', () => expect(100).toBeWithinRange(90, 110));
test('is NOT within range', () => expect(101).not.toBeWithinRange(0, 100));
test('asymmetric ranges', () => {
expect({apples: 6, bananas: 3}).toEqual({
apples: expect.toBeWithinRange(1, 10),
bananas: expect.not.toBeWithinRange(11, 20),
});
});
Объявление типа матчера можно разместить в файле .d.ts или в импортируемом модуле .ts (см. примеры для JS и TS выше соответственно). Если объявление находится в файле .d.ts, убедитесь, что он включён в программу и является корректным модулем, то есть содержит как минимум пустой export {}.
Вместо импорта модуля toBeWithinRange в файл теста можно включить матчер для всех тестов, переместив вызов expect.extend в скрипт setupFilesAfterEnv:
import {expect} from '@jest/globals';
// remember to export `toBeWithinRange` as well
import {toBeWithinRange} from './toBeWithinRange';
expect.extend({
toBeWithinRange,
});
Асинхронные матчеры
expect.extend также поддерживает асинхронные матчеры. Асинхронные матчеры возвращают Promise, поэтому возвращённое значение необходимо ожидать с помощью await. Рассмотрим пример матчера, который покажет, как они используются. Мы реализуем матчер с именем toBeDivisibleByExternalValue, в котором число, на которое нужно делить, будет получено из внешнего источника.
expect.extend({
async toBeDivisibleByExternalValue(received) {
const externalValue = await getExternalValueFromRemoteSource();
const pass = received % externalValue === 0;
if (pass) {
return {
message: () =>
`expected ${received} not to be divisible by ${externalValue}`,
pass: true,
};
} else {
return {
message: () =>
`expected ${received} to be divisible by ${externalValue}`,
pass: false,
};
}
},
});
test('is divisible by external value', async () => {
await expect(100).toBeDivisibleByExternalValue();
await expect(101).not.toBeDivisibleByExternalValue();
});
API пользовательских матчеров
Матчеры должны возвращать объект (или Promise с объектом) с двумя ключами. pass указывает, было ли найдено соответствие, а message содержит функцию без аргументов, возвращающую сообщение об ошибке в случае неудачи. Поэтому, когда pass имеет значение false, message должен возвращать сообщение об ошибке для случая, когда проверка expect(x).yourMatcher() завершается неудачей. А когда pass имеет значение true, message должен возвращать сообщение об ошибке для случая, когда проверка expect(x).not.yourMatcher() завершается неудачей.
Матчеры вызываются с аргументом, переданным в expect(x), за которым следуют аргументы, переданные в .yourMatcher(y, z):
expect.extend({
yourMatcher(x, y, z) {
return {
pass: true,
message: () => '',
};
},
});
Вспомогательные функции и свойства, доступные внутри пользовательского матчера, находятся в this:
this.isNot
Логическое значение, указывающее, что этот матчер был вызван с модификатором отрицания .not; оно позволяет сформировать понятную и корректную подсказку для матчера (см. пример кода).
this.promise
Строка, позволяющая сформировать понятную и корректную подсказку для матчера:
-
'rejects', если матчер вызван с модификатором Promise.rejects -
'resolves', если матчер вызван с модификатором Promise.resolves -
'', если матчер вызван без модификатора Promise
this.equals(a, b, customTesters?)
Это функция глубокого сравнения, возвращающая true, если значения двух объектов совпадают (рекурсивно). При необходимости ей можно передать список пользовательских тестеров равенства для глубокого сравнения (см. this.customTesters ниже).
this.expand
Логическое значение, указывающее, что этот матчер был вызван с параметром expand. Если Jest запущен с флагом --expand, с помощью this.expand можно определить, ожидается ли вывод полных различий и ошибок.
this.utils
В this.utils доступен ряд полезных инструментов, в основном экспортируемых из пакета jest-matcher-utils.
Наиболее полезны matcherHint, printExpected и printReceived — они помогают красиво форматировать сообщения об ошибках. Например, посмотрите реализацию матчера toBe:
const {diff} = require('jest-diff');
expect.extend({
toBe(received, expected) {
const options = {
comment: 'Object.is equality',
isNot: this.isNot,
promise: this.promise,
};
const pass = Object.is(received, expected);
const message = pass
? () =>
// eslint-disable-next-line prefer-template
this.utils.matcherHint('toBe', undefined, undefined, options) +
'\n\n' +
`Expected: not ${this.utils.printExpected(expected)}\n` +
`Received: ${this.utils.printReceived(received)}`
: () => {
const diffString = diff(expected, received, {
expand: this.expand,
});
return (
// eslint-disable-next-line prefer-template
this.utils.matcherHint('toBe', undefined, undefined, options) +
'\n\n' +
(diffString && diffString.includes('- Expect')
? `Difference:\n\n${diffString}`
: `Expected: ${this.utils.printExpected(expected)}\n` +
`Received: ${this.utils.printReceived(received)}`)
);
};
return {actual: received, message, pass};
},
});
Будет выведено примерно следующее:
expect(received).toBe(expected)
Expected value to be (using Object.is):
"banana"
Received:
"apple"
При сбое проверки сообщение об ошибке должно содержать достаточно информации, чтобы пользователь мог быстро устранить проблему. Сформулируйте точное сообщение об ошибке, чтобы обеспечить удобство работы разработчикам, использующим ваши пользовательские проверки.
this.customTesters
Если ваш матчер выполняет глубокое сравнение с помощью this.equals, возможно, вам потребуется передать предоставленные пользователем тестеры в this.equals. Пользовательские тестеры равенства, добавленные пользователем с помощью API addEqualityTesters, доступны через это свойство. Встроенные матчеры Jest передают this.customTesters (наряду с другими встроенными тестерами) в this.equals для глубокого сравнения; ваши пользовательские матчеры могут поступать так же.
Пользовательские матчеры снимков
Чтобы использовать тестирование снимков внутри пользовательского матчера, импортируйте jest-snapshot и используйте его в матчерe.
Ниже приведён матчер снимков, который обрезает строку до заданной длины перед сохранением, .toMatchTrimmedSnapshot(length):
const {toMatchSnapshot} = require('jest-snapshot');
expect.extend({
toMatchTrimmedSnapshot(received, length) {
return toMatchSnapshot.call(
this,
received.slice(0, length),
'toMatchTrimmedSnapshot',
);
},
});
it('stores only 10 characters', () => {
expect('extra long string oh my gerd').toMatchTrimmedSnapshot(10);
});
/*
Stored snapshot will look like:
exports[`stores only 10 characters: toMatchTrimmedSnapshot 1`] = `"extra long"`;
*/
Также можно создавать пользовательские матчеры для встроенных снимков — снимки будут корректно добавлены к пользовательским матчерам. Однако встроенный снимок всегда пытается добавиться к первому аргументу или ко второму, если первый аргумент является матчерoм свойства. Поэтому пользовательские аргументы в пользовательских матчерах принимать нельзя.
const {toMatchInlineSnapshot} = require('jest-snapshot');
expect.extend({
toMatchTrimmedInlineSnapshot(received, ...rest) {
return toMatchInlineSnapshot.call(this, received.slice(0, 10), ...rest);
},
});
it('stores only 10 characters', () => {
expect('extra long string oh my gerd').toMatchTrimmedInlineSnapshot();
/*
The snapshot will be added inline like
expect('extra long string oh my gerd').toMatchTrimmedInlineSnapshot(
`"extra long"`
);
*/
});
асинхронный
Если ваш пользовательский матчер встроенных снимков асинхронный, то есть использует async-await, может появиться ошибка вида «Несколько встроенных снимков для одного вызова не поддерживаются». Jest требуется дополнительная контекстная информация, чтобы определить, где был использован пользовательский матчер встроенных снимков, и правильно обновить снимки.
const {toMatchInlineSnapshot} = require('jest-snapshot');
expect.extend({
async toMatchObservationInlineSnapshot(fn, ...rest) {
// The error (and its stacktrace) must be created before any `await`
this.error = new Error();
// The implementation of `observe` doesn't matter.
// It only matters that the custom snapshot matcher is async.
const observation = await observe(async () => {
await fn();
});
return toMatchInlineSnapshot.call(this, recording, ...rest);
},
});
it('observes something', async () => {
await expect(async () => {
return 'async action';
}).toMatchTrimmedInlineSnapshot();
/*
The snapshot will be added inline like
await expect(async () => {
return 'async action';
}).toMatchTrimmedInlineSnapshot(`"async action"`);
*/
});
Прерывание
Обычно jest пытается сопоставить все снимки, ожидаемые в тесте.
Иногда продолжать тест после сбоя предыдущего снимка не имеет смысла. Например, при создании снимков конечного автомата после различных переходов тест можно прервать, как только один из переходов приведёт к неправильному состоянию.
В этом случае можно реализовать пользовательский матчер снимков, который выбрасывает ошибку при первом несовпадении вместо сбора всех несовпадений.
const {toMatchInlineSnapshot} = require('jest-snapshot');
expect.extend({
toMatchStateInlineSnapshot(...args) {
this.dontThrow = () => {};
return toMatchInlineSnapshot.call(this, ...args);
},
});
let state = 'initial';
function transition() {
// Typo in the implementation should cause the test to fail
if (state === 'INITIAL') {
state = 'pending';
} else if (state === 'pending') {
state = 'done';
}
}
it('transitions as expected', () => {
expect(state).toMatchStateInlineSnapshot(`"initial"`);
transition();
// Already produces a mismatch. No point in continuing the test.
expect(state).toMatchStateInlineSnapshot(`"loading"`);
transition();
expect(state).toMatchStateInlineSnapshot(`"done"`);
});
Состояние матчера
expect.getState()
Возвращает текущее состояние модуля expect. Это полезно для доступа к метаданным теста внутри beforeEach, afterEach, пользовательских матчеров или любого кода, выполняющегося во время теста.
Объект состояния содержит следующие часто используемые свойства (и другие). Обратите внимание: при вызове вне активного теста эти поля могут быть undefined:
currentTestName
Полное имя выполняемого в данный момент теста, включая имена всех родительских блоков describe, разделённые пробелами. Например:
describe('my suite', () => {
describe('nested', () => {
beforeEach(() => {
const {currentTestName} = expect.getState();
console.log(currentTestName); // 'my suite nested my test'
});
test('my test', () => {
const {currentTestName} = expect.getState();
console.log(currentTestName); // 'my suite nested my test'
});
});
});
describe('my suite', () => {
describe('nested', () => {
beforeEach(() => {
const {currentTestName} = expect.getState();
console.log(currentTestName); // 'my suite nested my test'
});
test('my test', () => {
const {currentTestName} = expect.getState();
console.log(currentTestName); // 'my suite nested my test'
});
});
});
Это особенно полезно для следующих задач:
- Присвоение имён снимкам экрана при визуальном регрессионном тестировании
- Загрузка фикстур для конкретного теста
- Ведение журналов и отладка
testPath
Абсолютный путь к выполняемому файлу теста.
beforeEach(() => {
const {testPath} = expect.getState();
console.log(testPath); // '/path/to/project/myTest.test.js'
});
beforeEach(() => {
const {testPath} = expect.getState();
console.log(testPath); // '/path/to/project/myTest.test.ts'
});
expand
Логическое значение, указывающее, был ли Jest запущен с флагом --expand. Это то же значение, которое доступно как this.expand внутри пользовательских матчеров.
expect.setState(state)
Объединяет переданный объект с текущим состоянием матчера. В основном используется внутри Jest и пользовательских матчеров.
expect.setState({key: 'value'});
const {key} = expect.getState();
console.log(key); // 'value'
// To use custom properties with setState in TypeScript,
// augment the MatcherState interface:
declare module 'expect' {
interface MatcherState {
key?: string;
}
}
expect.setState({key: 'value'});
const {key} = expect.getState();
console.log(key); // 'value'
Задавайте только пользовательские свойства. Перезапись встроенных свойств состояния (например, currentTestName) может привести к непредвиденному поведению.
Сериализуемые свойства
SERIALIZABLE_PROPERTIES
Сериализуемые свойства — это набор свойств, которые Jest считает сериализуемыми. Этот набор используется для определения того, является ли свойство сериализуемым. Если объект содержит свойство, отсутствующее в этом наборе, оно считается несериализуемым и не выводится в сообщениях об ошибках.
В этот набор можно добавить собственные свойства, чтобы объекты выводились правильно. Например, если у вас есть класс Volume и нужно, чтобы выводились только свойства amount и unit, добавьте их в SERIALIZABLE_PROPERTIES:
import {SERIALIZABLE_PROPERTIES} from 'jest-matcher-utils';
class Volume {
constructor(amount, unit) {
this.amount = amount;
this.unit = unit;
}
get label() {
throw new Error('Not implemented');
}
}
Volume.prototype[SERIALIZABLE_PROPERTIES] = ['amount', 'unit'];
expect(new Volume(1, 'L')).toEqual(new Volume(10, 'L'));
Это выведет в сообщении об ошибке только свойства amount и unit, игнорируя свойство label.
expect(received).toEqual(expected) // deep equality
Expected: {"amount": 10, "unit": "L"}
Received: {"amount": 1, "unit": "L"}
© 2022 Facebook, Inc.
Licensed under the MIT License.
https://jestjs.io/docs/expect