В этом уроке мы научимся писать тесты для API, используя встроенный в Node.js тестовый фреймворк. В данном контексте мы говорим об интеграционных тестах, которые проверяют корректность работы API. Создание таких тестов обычно повторяет один и тот же алгоритм, что делает их разработку относительно простой. Общий алгоритм выглядит следующим образом:
- Добавляем необходимые данные в базу данных если нужно.
- Готовим данные для запроса. Опять же, если нужно.
- Выполняем ровно один запрос, который соответствует тестируемому API.
- Проверяем код ответа, содержимое в базе данных и тело ответа.
Создадим наш первый тест для тестирования списка пользователей. Для этого добавим файл test/routes/api/users.test.js со следующим кодом:
import { test } from 'node:test'
import * as assert from 'node:assert'
import { build } from '../../helper.js'
test('get users', async (t) => {
const app = await build(t)
const res = await app.inject({
url: '/api/users',
})
assert.equal(res.statusCode, 200, res.body)
}
Этот тест проверяет, что API для получения списка пользователей работает корректно и возвращает ответ с кодом 200, указывающим на успешное выполнение запроса.
Разбор кода
- Функция
build()
была сгенерирована утилитойfastify
. Внутри себя она создает объект Fastify и, фактически, инициализирует весь фреймворк на каждый тест. Таким образом достигается изоляция тестов друг от друга. Благодаря использованию sqlite в памяти, каждый тест работает с одинаковым начальным состоянием базы данных и любые изменения "забываются" после теста. - Благодаря сидам в базе данных есть пользователи, поэтому этот запрос возвращает не пустые данные.
- Метод
inject()
имитирует запрос, который вызывает внутри Fastify нужный обработчик. В реальности HTTP запроса не происходит, что не влияет на функциональность, но значительно ускоряет тесты. К тому же, не нужно отдельно поднимать сервер с приложением. - Проверяется только статус ответа. Структуру ответа проверять не нужно, в следующих уроках мы познакомимся с JSON Scheme и автоматической валидацией на ее базе.
- В проверке третьим параметром передан ответ от приложения, который будет показан в случае ошибки. Этот маленький трюк бесплатно упрощает и ускоряет анализ ошибок в тестах.
Запустить тесты на выполнение можно командой npm test
внутри которой прописана строчка node --test test/**/*.test.js
. К сожалению, такой формат не запускает тесты во внутренних директориях, в нашем случае api. Поэтому команду можно заменить на такую node --test test/**/*.test.js
. И пример запуска:
npm test
> node --test test/routes/**/*.test.js
✔ get users (192.2045ms)
Теперь посмотрим на эндпоинт для обращения к конкретному пользователю:
test('get users/:id', async (t) => {
const app = await build(t)
const user = await app.db.query.users.findFirst()
assert.ok(user)
const res = await app.inject({
url: `/api/users/${user.id}`,
})
assert.equal(res.statusCode, 200, res.body)
})
Он выглядит практически идентично, за исключением того, что нам нужен идентификатор пользователя, которого мы планируем извлечь. Для простоты, здесь идет обращение к первому пользователю в базе данных. Он туда попал благодаря сидам.
Несмотря на то, что мы уверены в наличии пользователя, не будет лишним поставить проверку assert.ok(user)
, которая поможет быстрее найти проблему если пользователь отсутствует и упростит сам тест если он написан на TypeScript. Иначе придется писать условную конструкцию на проверку существования.
От выборок перейдем к изменению. Ниже код теста создания пользователя:
import { buildUser } from '../../../lib/data.js'
test('post users', async (t) => {
const app = await build(t)
const body = buildUser()
const res = await app.inject({
method: 'post',
url: `/api/users`,
body: body,
})
assert.equal(res.statusCode, 201, res.body)
})
Ключевое отличение здесь в необходимости сгенерировать данные и передать их через свойство body
в запрос. Мы заранее упростили себе эту задачу, создав методы для генерации данных, которые использовали в сидах. Теперь эти же методы мы используем и в тестах.
Тест на обновление выглядит практически как и на создание, кроме того, что меняется адрес:
test('patch users/:id', async (t) => {
const app = await build(t)
const user = await app.db.query.users.findFirst()
assert.ok(user)
const authHeader = await getAuthHeader(app)
const res = await app.inject({
method: 'patch',
url: `/api/users/${user.id}`,
body: buildUser(),
})
assert.equal(res.statusCode, 200, res.body)
})
И тест на удаление:
test('delete users/:id', async (t) => {
const app = await build(t)
const user = await app.db.query.users.findFirst()
assert.ok(user)
const res = await app.inject({
method: 'delete',
url: `/api/users/${user.id}`,
})
assert.equal(res.statusCode, 204, res.body)
})
Код последних двух тестов почти не отличается. Разница лишь в теле запроса и коде ответа.
Логирование
По умолчанию, логирование выключено в тестах. Это усложняет отладку, если что-то идет не так. Не видно какой был запрос, куда и с какими параметрами. В случае ошибки 500 не видно исключения. Это можно легко исправить в файле test/helper.js, где происходит конфигурирование Fastify. Для этого нужно выполнить два действия:
Создать метод
serverConfig()
:function serverConfig() { return { logger: { level: 'error', transport: { target: 'pino-pretty', options: { colorize: true, }, }, }, } }
Добавить вызов этого метода в процесс инициализации внутри функции
build()
// Было // const app = await helper.build(argv, config()) // Стало const app = await helper.build(argv, config(), serverConfig())
После этих действий, Fastify начнет выводить информацию о запросе и трейс прямо в консоль.
Самостоятельная работа
- Добавьте тесты для каждой операции CRUD
- Запушьте изменения в репозиторий
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.