Привет, друзья! Я Кирилл Мыльников, frontend-разработчик в ГК Юзтех. Сегодня хочу поделиться своей экспертизой в тестировании с использованием фреймворка Jest. Полностью настроенный проект будет доступен на GitHub, где вы сможете ознакомиться с ним и клонировать его. Мы рассмотрим широкий спектр тем – от базовых до продвинутых приёмов тестирования. Статья будет полезна как для тех, кто хочет освежить свои знания, так и для новичков, только начинающих погружаться в мир тестирования. Также отмечу, что статья состоит из нескольких частей.
В серии статей разберем следующие темы:
Jest основы:
- структура тестов;
- виды проверок;
- настройка и запуск.
Особенности Jest:
- моки и для чего они нужны;
- тестирование ошибок и покрытие кода.
Прежде чем приступим к практике, давайте вспомним, какие виды тестирования существуют.
Виды тестирования
Unit-тесты
Unit-тесты — это форма тестирования, которая направлена на проверку правильности работы отдельных небольших компонентов кода, таких как функции, классы, модули, компоненты и другие. Они помогают выявлять ошибки и оперативно их устранять при внесении изменений или добавлении нового функционала.
Интеграционное тестирование
Интеграционное тестирование — это тестирование на работоспособность двух или более модулей системы. Они решают проблему после покрытия кода unit-тестов, проверяют, как ведут себя связные модули.
End to End
End to End — это метод тестирования, который проверяет полностью функциональности ПО от начала до конца, включает в себя проверку всех уровней системы, начиная от пользовательского интерфейса и заканчивая базой данных и серверной логикой. В основном, это робот, имитирующий пользователя.
UI тестирование
UI тестирование — это метод тестирования, направленный на корректность работы и удобства пользовательского интерфейса, например: внешний вид интерфейса, отзывчивость, элементы управления и так далее.
Основная стратегия тестирования представлена в виде пирамиды, которая показывает, что начинать следует с юнит-тестов, затем переходить к интеграционным тестам, чтобы сочетать их, и, наконец, проводить end-to-end тесты. Идея пирамиды заключается в том, что чем выше находится тестовый уровень в пирамиде, тем тесты дороже. Ну и этот подход обычно подходит новым проектам, где сразу пишем тесты.

Когда проект уже существует, стратегия тестирования может быть адаптирована. В таких случаях часто начинают с проведения end-to-end тестов для проверки работы приложения в целом. Затем переходят к интеграционным тестам, чтобы убедиться в правильном взаимодействии компонентов. И только после этого фокусируются на unit-тестах, чтобы покрыть отдельные части логики приложения. Такой подход помогает обеспечить надёжное тестирование всего приложения в целом перед более детальной проверкой отдельных компонентов.
Итак, мы с вами разобрали, какие виды тестирования существуют. Пора переходить к практике, а именно, к Jest и его основам.
Jest
Jest — это фреймворк для тестирования JavaScript, который обладает удобным синтаксисом для написания и запуска тестов. Он хорошо документирован, легко настраивается и может быть легко расширен по мере необходимости.
Основные возможности:
- Детальное описание причин падения тестов;
- Наличие полноценного набора инструментов из коробки;
- Быстрый и надёжный запуск тестов в параллельных потоках;
- Работа с проектами, использующими Babel, Rect, Vue, Angular, TypeScript, Node;
- Можно писать как и unit-тесты, так и интеграционные;
- Инструменты для имитации.
Вообще, у нас будет два отдельных проекта-примера, один естественно с React (реализация будет в другой статье), а другой просто настроим с нуля.
Создаём папку, где у вас будет проект, и инициализируем проект, в моём случае с помощью команды:
yarn init
Должна появиться такая структура:

После чего нужно будет установить сам пакет Jest с помощью команды:
yarn add -D jest
Как говорится в документации Jest, для расширенных настроек понадобится установить ещё ряд пакетов:
yarn add -D babel-jest @babel/core @babel/preset-env
После установки всех необходимых пакетов создаём файл babel.config.js в корневой директории проекта и указываем необходимые конфигурационные параметры. Если мы хотим использовать TypeScript, мы должны установить соответствующий пакет и добавить соответствующую зависимость в наш файл конфигурации:
yarn add -D @babel/preset-typescript
yarn add -D typescript
yarn add -D ts-node
module.exports = {
presets: [
["@babel/preset-env", { targets: { node: "current" } }],
"@babel/preset-typescript",
],
};
Не забывайте про важную деталь: чтобы Jest проверял типы по мере их выполнения, нужно установить ещё один пакет ts-jest и для предоставления глобальных типов @types/jest
yarn add -D ts-jest
yarn add -D @types/jest
Давайте пока переключимся на конфигурацию Jest. В корне проекта создаём jest.config.ts и указываем следующую настройку:
import type { Config } from "jest";
const config: Config = {
verbose: true,
preset: "ts-jest"
};
export default config;
Опция verbose: true обеспечивает более подробный вывод информации при запуске тестов в случае возникновения проблем.
Используем preset: “ts-jest” для преобразования TypeScript в JavaScript перед запуском тестов.
Также импортируем тип Config для получения подсказок о других доступных настройках в целом.
Можно теперь проверить, запускаются ли тесты вообще. В package-json указываем команду:
"scripts": {
"start": "jest"
},
Создаём файл utils, в котором содержится функция, выполняющая деление двух чисел. Нам необходимо протестировать эту функцию, поэтому создаём файл utils.test.ts и пишем первый тест.
import { divideNumbers } from "./utils";
test("should divide numbers", () => {
const res = divideNumbers(10, 5);
expect(res).toBe(2);
});
И запускаем с помощью команды yarn start. Если всё настроили правильно, то должно быть так:

В одном файле можно создавать произвольное количество тестов, используя директивы test или it. Хорошей практикой является начинать названия тестов с ключевого слова "should", чтобы чётко определить ожидаемое поведение. Это способствует пониманию того, что тестируется, и повышает читаемость кода. Кроме того, использование describe() для группировки тестов помогает упорядочить и структурировать код, делая его более ясным и лёгким для поддержки. Важно быть особенно внимательным к именованию тестов, чтобы обеспечить их эффективную организацию и понимание.
Пример:
describe("utils", () => {
test("should divide numbers", () => {
const res = divideNumbers(10, 5);
expect(res).toBe(2);
});
it("should check age positive", () => {
const res = checkAge(19);
expect(res).toBe(true);
});
test("should check age negative", () => {
const res = checkAge(15);
expect(res).toBe(false);
});
});
Результат:

Давайте разберём, что такое expect. Это глобальная функция, в которую мы передаём определённый результат и которая возвращает нам объект с разными вариантами проверок. Основные проверки, которые используются:
toBe() — работает как тройное сравнение, идёт проверка на true/false, хорошо подходит для примитивов:
test("should divide numbers", () => {
const res = divideNumbers(10, 5);
expect(res).toBe(2);
});
toEqual() — работает с объектами, сравнивает и возвращает true, если равны объекты, в противном случае false.
test("should check object", () => {
const obj = { b: 2 };
expect(obj).toEqual({ b: 2 }); // true
});
toHaveLength() — проверяет длину массива.
test("should check array length", () => {
const array = [4, 3, 1];
expect(array).toHaveLength(3);
});
toContain() — проверяет, есть ли конкретный элемент в массиве.
test("should check element array", () => {
const array = [4, 3, 1, "a"];
expect(array).toContain("a");
});
toBeUndefined() и toBeNull() — проверяют результат на null/undefined.
test("should check null/undefined", () => {
expect(undefined).toBeUndefined();
expect(null).toBeNull();
});
Думаю, суть вы поняли, проверок достаточно много. О них можно почитать в официальной документации Jest.
Параметризованные тесты
Параметризованные тесты позволяют эффективно повторно использовать один и тот же тестовый сценарий при различных условиях на каждом запуске. Это оптимизирует управление тестами, уменьшает дублирование кода и делает процесс тестирования более гибким и обслуживаемым.
Возможности test и it включают методы, такие как skip, который позволяет игнорировать определенный тест, и only, который позволяет выполнять только выбранные тесты, оставляя остальные в игноре. Эти функции облегчают управление тестами.
describe("Parameterized utils", () => {
test.skip("should divide numbers", () => {
const res = divideNumbers(10, 5);
expect(res).toBe(2);
});
it.only("should check age positive", () => {
const res = checkAge(19);
expect(res).toBe(true);
});
});
only:

skip:

Этих методов у нас достаточно много. Подробнее можете посмотреть в официальной документации Jest.
Особенности Jest
Mocks — это фейковые данные, которые нужны для тестового кейса.
Spies — отслеживание работы методов.
Функции-фейки — нужны для тестирования количества вызовов колбэков.
Что такое моки (mocks)?
Давайте рассмотрим пример тестирования с мок-данными. У нас есть функция, которая фильтрует массив:
export const filterArray = <T>(
array: T[],
callback: (a: T) => boolean
): T[] => {
const newArray: T[] = [];
for (let i = 0; i < array.length; i++) {
if (callback(array[i])) {
newArray.push(array[i]);
}
}
return newArray;
};
Пример:
import {
mocks_data,
MocksDataType,
result_mocks_data,
} from "./__mocks__/mocks";
import { filterArray } from "./filterArray";
describe("filterArray tests", () => {
const cb = jest.fn();
it("should not invoke callback when an array is empty", () => {
// mock function
filterArray([], cb);
expect(cb).not.toHaveBeenCalled();
});
it("should invoke callback function for each element in the array", () => {
const array = [1, 2, 3];
filterArray(array, cb);
expect(cb).toHaveBeenCalledTimes(array.length);
});
it("should filter an array", () => {
const hasPrice = ({ price }: MocksDataType) => price > 100;
const result = filterArray(mocks_data, hasPrice);
expect(result).toEqual(result_mocks_data);
});
});
- В первом случае мы проверяем, что функция не была вызвана;
- Во втором случае мы проверяем, что функция была вызвана, получила массив и вернула количество вызовов;
- В третьем случае мы используем предоставленные мок-данные для возвращения отфильтрованного массива.
Те самые мок-данные можно посмотреть тут, где result_mocks_data — результат после фильтрации, mocks_data — просто мок-данные, которые получает функция.
Результат:

Следующее, что мы с вами разберём, это Spies
Spies
Spies используют для отслеживания вызовов конкретных функций с целью убедиться, что они были вызваны или вызваны с определенными параметрами. Рассмотрим следующий случай: мы хотим отследить console.log, который используется в нашем цикле. Для этого мы можем использовать метод spyOn, который принимает два аргумента. Первый аргумент — это объект, за которым мы хотим следить (в данном случае это console), а второй аргумент — это метод объекта, за которым мы хотим следить (например, log).
export const filterArray = <T>(
array: T[],
callback: (a: any) => boolean
): T[] => {
const newArray: T[] = [];
for (let i = 0; i < array.length; i++) {
console.log("array[i]", array[i]);
if (callback(array[i])) {
newArray.push(array[i]);
}
}
return newArray;
};
Пример:
describe("filterArray spies", () => {
it("should logSpy console", () => {
const logSpy = jest.spyOn(console, "log");
const hasPrice = ({ price }: MocksDataType) => price > 100;
const result = filterArray(mocks_data, hasPrice);
expect(result).toEqual(result_mocks_data);
expect(logSpy).toHaveBeenCalledTimes(mocks_data.length);
});
});
Результат:

Мы видим конкретные элементы массива.
Тестирование асинхронного кода
Давайте создадим асинхронную функцию, которая будет создавать нам что-то:
export async function createTodoAsync(text: string) {
const response = await fetch("https://jsonplaceholder.typicode.com/todos", {
method: "POST",
body: JSON.stringify({ title: text }),
headers: {
"Content-type": "application/json; charset=UTF-8",
},
});
if (!response.ok) {
throw new Error("Ошибка создания задачи");
}
return response.json();
}
И сразу напишем тесты к данной функции:
Пример:
import { createTodoAsync } from "./createTodo";
import { mockResponseTodo, mockTodo } from "./__mocks__/mocks";
const mockFetch = jest.fn();
mockFetch.mockReturnValueOnce(
Promise.resolve({
ok: true,
json: () => Promise.resolve(mockTodo),
headers: new Headers(),
redirected: false,
status: 200,
statusText: "OK",
url: "https://example.com",
type: "basic",
body: null,
bodyUsed: false,
})
);
describe("createTodo tests", () => {
it("should create todo", async () => {
const result = await createTodoAsync("todo");
expect(result).toEqual(mockResponseTodo);
});
});
Далее в тесте используем метод mockReturnValueOnce, чтобы задать поведение моковой функции fetch. Мы возвращаем объект с флагом ok: true и методом json, который возвращает объект задачи из тестовых данных.
Мок-данные, которые мы ожидаем, в ответе.
Пример:
export const mockResponseTodo = {
id: 201,
title: "todo",
};
Тестирование ошибок
Теперь нужно проверить негативные кейсы, когда приходит ошибка. Оставляем ту же функцию, но теперь пишем вот такой тест-кейс:
if (!response.ok) {
throw new Error("Ошибка создания задачи");
}
В функции createTodoAsync ожидаем, что если ошибка, то выводим текст «Ошибка создания задачи».
Пример:
it("should throw error when response is not ok", () => {
mockFetch.mockReturnValueOnce(Promise.resolve({ ok: false }));
expect(createTodoAsync("todo")).rejects.toThrow("Ошибка создания задачи");
});
В данном примере изменили мок-данные на значение ok: false и ожидаем появления ошибки с конкретным текстом, который определён в функции.
Покрытие кода тестами
Покрытие кода тестами — это процент кода, который выполняется во время запуска тестов. Чем выше покрытие тестами, тем больше уверенности, что код работает правильно. Статистика покрытия тестами помогает определить, какие части кода нуждаются в дополнительных тестах. Она позволяет улучшить качество кода и обнаружить потенциальные проблемы.
Чтобы начать собирать статистику тестов по проекту, нам нужно прописать дополнительные настройки в файле jest.config.ts
Прописываем такое свойство:
collectCoverage: true,
Этим свойством мы разрешаем сбор информации, по умолчанию — false.
Дальше прописываем:
collectCoverageFrom: [
"<rootDir>/*.{js,ts}",
"!**/node_modules/**",
"!<rootDir>/*.mock*",
"!<rootDir>/*.config.*",
],
Указываем в виде массива пути, которые мы хотим протестировать или не хотим. К примеру, всё, что начинается со знака "!", мы отмечаем как папки или файлы, которые будут проигнорированы.
Если всё сделали правильно, запускаем тесты и проверяем статистику.
Результат:

Теперь мы видим, что протестировано, а что нет. Написано, где именно, например, в файле createTodo забыли протестировать функцию.
export function createTodo(text: string) {
return {
id: Math.random(),
text,
completed: false,
};
}
Давайте напишем для неё тесты и опять запустим.
Тесты к функции.
describe("createTodo function", () => {
it("should return an object with id, text, and completed properties", () => {
const todo = createTodo("New todo item");
expect(todo).toHaveProperty("id");
expect(todo).toHaveProperty("text");
expect(todo).toHaveProperty("completed");
});
it("should set the text property to the provided text", () => {
const text = "New todo item";
const todo = createTodo(text);
expect(todo.text).toBe(text);
});
it("should set the completed property to false", () => {
const todo = createTodo("New todo item");
expect(todo.completed).toBe(false);
});
it("should generate a random id", () => {
const todo1 = createTodo("New todo item");
const todo2 = createTodo("Another todo item");
expect(todo1.id).not.toBe(todo2.id);
});
});
Запускаем снова тесты.
Результат:

Также обратите внимание, что в корне проекта у нас создалась папка coverage, там находится вся информация о тестах.

Больше всего нас интересует index.html который нам нужно открыть.

Мы увидим таблицу, которая у нас была в console, но тут можем провалиться в файл и посмотреть, что конкретно у нас не протестировано.
Пример:

Также в конфиге можем указать пороговые значения тестов для нашего проекта:
coverageThreshold: {
global: {
branches: 70,
functions: 70,
lines: 90,
statements: 100,
},
},
Запускаем и видим, что мы не прошли порог по тестам.
Результат:

Итак, мы с вами создали проект «с нуля» и прошли базовые темы. Важно, что мы научились настраивать конфигурации для различных задач, работать с jest. Напоминаю, что это лишь первая часть. Впереди нас ждут более сложные и захватывающие задачи.
Материалы
Jest — https://jestjs.io/
GitHub — https://github.com/kirill0202/Jest
Комментарии