Темная тема
Введение
Что такое 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 test
expect и матчеры
Матчеры:
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)
})