Темная тема
Введение
Что такое Jest?
Jest - это фреймворк для тестирования JavaScript.
Виды тестирования
- Unit-тесты - тестирование функции, класса, модуля, компонента;
- Интеграционные - комбинация unit-тестов;
- End to End - работа приложения в целом (имитация действий пользователя);
Установка Jest и первый тест
Выполним команды:
sh
mkdir jest-proj
cd jest-proj
npm init -y
npm install --save-dev jest
npm install --save-dev babel-jest @babel/core @babel/preset-env
npm install --save-dev @types/jest-y- автоматическое создание файлаpackage.json;--save-dev- установка пакета как dev-зависимость (зависимость для разработки -devDependenciesфайлаpackage.json);
Изменим файл babel.config.js:
js
module.exports = {
presets: [['@babel/preset-env', {targets: {node: 'current'}}]],
};Изменим файл jest.config.js:
js
/** @type {import('jest').Config} */
const config = {
verbose: true,
}
module.exports = configИзменим файл package.json:
json
{
"name": "jest-proj",
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"description": "",
"devDependencies": {
"jest": "^29.7.0"
}
}Изменим файл math.js:
js
export function sum(a, b) {
return a + b
}Изменим файл math.test.js:
js
import { sum } from './math.js'
test('Складывает 1 + 2, получает 3', () => {
expect(sum(1, 2)).toBe(3)
})Выполним команду:
sh
npm testexpect и матчеры
Матчеры:
toBe(value)- строгое сравнение (===);toThrow()- проверка, что функция выбрасывает ошибку;toEqual(obj)- глубокое сравнение объектов и массивов;toBeNull()- проверка на null;toBeDefined()- проверка, что значение определено;toBeUndefined()- проверка, что значение не определено;toBeTruthy()- проверка на true;toBeFalsy()- проверка на false;toContain()- проверка значения в массиве и строке;toHaveLenght()- проверка длинны массива;
Например, изменим файл math.js:
js
export function divide(a, b) {
if (b === 0) throw new Error('Деление на ноль')
return a / b
}Например, изменим файл math.test.js:
js
import { divide } from './math.js'
test('Делит 6 на 2, получает 3', () => {
expect(divide(6, 2)).toBe(3)
})
test('Делит 10 на 2, возвращает число', () => {
expect(typeof divide(10, 2)).toBe('number')
})
test('Делит на 0, выбрасывает ошибку', () => {
expect(() => divide(5, 0)).toThrow('Деление на ноль')
})
test('Сравнение объектов', () => {
expect({ name: 'Alex' }).toEqual({ name: 'Alex' })
})
test('Сравнение массивов', () => {
expect([1, 2, 3]).toEqual([1, 2, 3])
})
test('Проверка на null', () => {
expect(null).toBeNull()
})
test('Проверка, что значение определено', () => {
expect(123).toBeDefined()
})
test('Проверка, что значение не определено', () => {
expect(undefined).toBeUndefined()
})
test('Проверка на true', () => {
expect(123).toBeTruthy()
})
test('Проверка на false', () => {
expect('').toBeFalsy()
})
test('Проверка элемента строки', () => {
expect('abc').toContain('b')
})
test('Проверка элемента массива', () => {
expect(['a', 'b', 'c']).toContain('b')
})
test('Проверка длинны массива', () => {
expect([1, 2, 3]).toHaveLength(3)
})Также можно использовать not. Например:
js
test('Проверка на отсутствие элемента строки', () => {
expect('abc').not.toContain('f')
})Выполним команду:
sh
npm testГруппировка тестов
Например, изменим файл math.js:
js
export function sum(a, b) {
return a + b
}Например, изменим файл math.test.js:
js
import { sum } from './math.js'
describe('Группа тестов', () => {
test('Складывает 1 + 2, получает 3', () => {
expect(sum(1, 2)).toBe(3)
})
test('Складывает 2 + 3, получает 5', () => {
expect(sum(2, 3)).toBe(5)
})
})Можно помещать describe внутрь describe. Например:
js
describe('Группа тестов', () => {
describe('Группа 1', () => {
... // тесты
})
describe('Группа 2', () => {
... // тесты
})
describe('Группа 3', () => {
... // тесты
})
})Выполним команду:
sh
npm testПараметризованные тесты
skip- пропуск теста;
Например, изменим файл math.test.js:
js
test('Проверка длинны массива (3)', () => {
expect([1, 2, 3]).toHaveLength(3)
})
// тест будет пропущен
test.skip('Проверка длинны массива (4)', () => {
expect([1, 2, 3, 4]).toHaveLength(4)
})
test('Проверка длинны массива (5)', () => {
expect([1, 2, 3, 4, 5]).toHaveLength(5)
})only- протестировать только этот тест;
Например, изменим файл math.test.js:
js
test('Проверка длинны массива (3)', () => {
expect([1, 2, 3]).toHaveLength(3)
})
// будет протестирован только этот тест
test.only('Проверка длинны массива (4)', () => {
expect([1, 2, 3, 4]).toHaveLength(4)
})
test('Проверка длинны массива (5)', () => {
expect([1, 2, 3, 4, 5]).toHaveLength(5)
})each- набор тест-кейсов;
Например, изменим файл math.test.js:
js
test.each([
{ input: [1, 2, 3], expected: 3 },
{ input: [1, 2, 3, 4], expected: 4 },
{ input: [1, 2, 3, 4, 5], expected: 5 },
])('Проверка длинны массива $input, ожидаем $expected', ({ input, expected }) => {
expect(input).toHaveLength(expected)
})todo- то, что мы планируем делать;
Например, изменим файл math.test.js:
js
test.todo('Написать тест для чего-то...')concurent- запуск тестов в параллельном (конкурентном) режиме;
Алиасы
При тестировании можно использовать как test(), так и it().
Например:
js
test('Проверка длинны массива', () => {
expect([1, 2, 3]).toHaveLength(3)
})
it('Проверка длинны массива', () => {
expect([1, 2, 3]).toHaveLength(3)
})test() и it() - полностью взаимозаменяемые функции.
Существует алиас для it.skip() - xit(). Например:
js
it.skip('Проверка длинны массива (4)', () => {
expect([1, 2, 3, 4]).toHaveLength(4)
})
xit('Проверка длинны массива (5)', () => {
expect([1, 2, 3, 4, 5]).toHaveLength(5)
})Существует алиас для it.only() - fit(). Например:
js
it('Проверка длинны массива (4)', () => {
expect([1, 2, 3, 4]).toHaveLength(4)
})
fit('Проверка длинны массива (5)', () => {
expect([1, 2, 3, 4, 5]).toHaveLength(5)
})Хуки
Хук (hook) в Jest - это специальная функция, которая выполняется до или после тестов, или групп тестов. Они используются для подготовки и очистки окружения, чтобы каждый тест выполнялся в контролируемых условиях.
beforeEach- вызывается перед каждым тестом;afterEach- вызывается после каждого теста;beforeAll- вызывается перед всеми тестами;afterAll- вызывается после всех тестов;
Например, изменим файл math.test.js:
js
beforeAll(() => {
console.log('Запуск до всех тестов')
})
beforeEach(() => {
console.log('Запуск перед каждым тестом')
})
afterEach(() => {
console.log('Запуск после каждого теста')
})
afterAll(() => {
console.log('Запуск всех тестов')
})
test('Проверка длинны массива', () => {
expect([1, 2, 3]).toHaveLength(3)
})
test('Проверка длинны массива', () => {
expect([1, 2, 3, 4]).toHaveLength(4)
})Если внутри describe будет вложен ещё один describe, то эти хуки будут работать и для него.
Если эти хуки будут вынесены за пределы describe, то они будут работать вообще для всего.
Моки (mocks)
Мок (mock) - это подменная функция, которая заменяет настоящую. Её используют, чтобы проверить, как и сколько раз её вызвали, без выполнения реальной логики.
Например, изменим файл math.js:
js
// Фильтрует массив с помощью callback-функции
export function filterArray(array, callback) {
const newArray = []
for (let i = 0; i < array.length; i += 1) {
if (callback(array[i])) newArray.push(array[i])
}
return newArray
}Например, изменим файл math.test.js:
js
import { filterArray } from './math'
test('Проверка, что функция cb не вызывалась', () => {
// это мок
const cb = jest.fn() // создание фейковой функции
filterArray([], cb)
expect(cb).not.toHaveBeenCalled()
})
test('Проверка, что функция вызывалась 3 раза', () => {
const cb = jest.fn()
const arr = [1, 2, 3]
filterArray(arr, cb)
expect(cb).toHaveBeenCalledTimes(arr.length)
})Создание тестовой функции можно вынести на уровень выше. Например, изменим файл math.test.js:
js
import { filterArray } from './math'
const cb = jest.fn()
test('Проверка, что функция cb не вызывалась', () => {
filterArray([], cb)
expect(cb).not.toHaveBeenCalled()
})
test('Проверка, что функция вызывалась 3 раза', () => {
const arr = [1, 2, 3]
filterArray(arr, cb)
expect(cb).toHaveBeenCalledTimes(arr.length)
})Но если мы поменяем первый и второй тест местами, то второй тест не пройдёт. Например, изменим файл math.test.js:
js
import { filterArray } from './math'
const cb = jest.fn()
test('Проверка, что функция вызывалась 3 раза', () => {
const arr = [1, 2, 3]
filterArray(arr, cb)
expect(cb).toHaveBeenCalledTimes(arr.length)
})
// этот тест не пройдёт
test('Проверка, что функция cb не вызывалась', () => {
filterArray([], cb)
expect(cb).not.toHaveBeenCalled()
})Дело в том, что мы сталкиваемся с "побочным эффектом". В первом тесте наша "фейковая функция" уже была вызвана 3 раза. Т.е. необходимо обнулить мок после каждого теста. Например, изменим файл math.test.js:
js
import { filterArray } from './math'
const cb = jest.fn()
afterEach(() => {
jest.clearAllMocks()
})
test('Проверка, что функция вызывалась 3 раза', () => {
const arr = [1, 2, 3]
filterArray(arr, cb)
expect(cb).toHaveBeenCalledTimes(arr.length)
})
test('Проверка, что функция cb не вызывалась', () => {
filterArray([], cb)
expect(cb).not.toHaveBeenCalled()
})Ещё один пример использования моков и фикстур.
Фикстуры (fixtures) - это предопределённые данные или состояния, которые используются в тестах для создания стабильных и воспроизводимых условий.
Например, изменим файл math.test.js:
js
import { filterArray } from './math'
const cb = jest.fn()
afterEach(() => {
jest.clearAllMocks()
})
const noPrice = [
{ id: 1, name: 'TV', price: 1000 },
{ id: 2, name: 'PS4', price: 0 },
{ id: 3, name: 'Monitor', price: 800 },
]
const filteredWithPrice = [
{ id: 1, name: 'TV', price: 1000 },
{ id: 3, name: 'Monitor', price: 800 },
]
test('Проверка, что функция вызывалась 3 раза', () => {
const arr = [1, 2, 3]
filterArray(arr, cb)
expect(cb).toHaveBeenCalledTimes(arr.length)
})
test('Проверка, что функция cb не вызывалась', () => {
filterArray([], cb)
expect(cb).not.toHaveBeenCalled()
})
test('Проверка, что возвращается массив только с ценами', () => {
const hasPrice = (order) => order.price > 0
const result = filterArray(noPrice, hasPrice)
expect(result).toEqual(filteredWithPrice)
}) Шпионы (spies)
Шпион (spy) - это способ отслеживать вызовы реальной функции. С его помощью можно узнать: вызывалась ли функция, с какими аргументами и сколько раз.
Например, изменим файл math.js:
js
export function filterArray(array, callback) {
const newArray = []
for (let i = 0; i < array.length; i += 1) {
console.log(array[i])
if (callback(array[i])) newArray.push(array[i])
}
return newArray
}Например, изменим файл math.test.js:
js
import { filterArray } from './math'
const cb = jest.fn()
// это шпион
const logSpy = jest.spyOn(console, 'log') // отслеживание консоли
afterEach(() => {
jest.clearAllMocks()
})
const noPrice = [
{ id: 1, name: 'TV', price: 1000 },
{ id: 2, name: 'PS4', price: 0 },
{ id: 3, name: 'Monitor', price: 800 },
]
const filteredWithPrice = [
{ id: 1, name: 'TV', price: 1000 },
{ id: 3, name: 'Monitor', price: 800 },
]
test('Проверка, что функция вызывалась 3 раза', () => {
const arr = [1, 2, 3]
filterArray(arr, cb)
expect(cb).toHaveBeenCalledTimes(arr.length)
expect(logSpy).toHaveBeenCalledTimes(arr.length)
})
test('Проверка, что функция cb и logSpy не вызывались', () => {
filterArray([], cb)
expect(cb).not.toHaveBeenCalled()
expect(logSpy).not.toHaveBeenCalled()
})
test('Проверка, что функция cb не вызывалась', () => {
const hasPrice = (order) => order.price > 0
const result = filterArray(noPrice, hasPrice)
expect(result).toEqual(filteredWithPrice)
expect(logSpy).toHaveBeenCalledTimes(noPrice.length)
})