Зарегистрируйтесь, чтобы продолжить обучение

Тестирование JS: REST API (Fastify)

В этом уроке мы научимся писать тесты для API, используя встроенный в Node.js тестовый фреймворк. В данном контексте мы говорим об интеграционных тестах, которые проверяют корректность работы API. Создание таких тестов обычно повторяет один и тот же алгоритм, что делает их разработку относительно простой. Общий алгоритм выглядит следующим образом:

  1. Добавляем необходимые данные в базу данных если нужно.
  2. Готовим данные для запроса. Опять же, если нужно.
  3. Выполняем ровно один запрос, который соответствует тестируемому API.
  4. Проверяем код ответа, содержимое в базе данных и тело ответа.

Создадим наш первый тест для тестирования списка пользователей. Для этого добавим файл 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. Для этого нужно выполнить два действия:

  1. Создать метод serverConfig():

    function serverConfig() {
      return {
        logger: {
          level: 'error',
          transport: {
            target: 'pino-pretty',
            options: {
              colorize: true,
            },
          },
        },
      }
    }
    
  2. Добавить вызов этого метода в процесс инициализации внутри функции build()

    // Было
    // const app = await helper.build(argv, config())
    // Стало
    const app = await helper.build(argv, config(), serverConfig())
    

После этих действий, Fastify начнет выводить информацию о запросе и трейс прямо в консоль.


Самостоятельная работа

  1. Добавьте тесты для каждой операции CRUD
  2. Запушьте изменения в репозиторий

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff