Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

Тестирование кода, взаимодействующего с файлами JS: Продвинутое тестирование

Наиболее типичный побочный эффект – взаимодействие с файлами (файловые операции). В основном это либо чтение файлов, либо запись в них. С чтением разбираться значительно проще, поэтому с него и начнём.

Чтение файлов

В большинстве случаев чтение файлов не доставляет особых проблем. Оно ничего не изменяет и выполняется локально, в отличие от сетевых запросов. Это значит, что при наличии необходимого файла и нужных прав, вероятность случайных ошибок крайне низка.

При тестировании функций, читающих файлы, должно выполняться ровно одно условие. Функция должна позволять менять путь до файла. В таком случае, достаточно создать файл нужной структуры в фикстурах.

// Функция читает файл со списком пользователей системы и возвращает их имена
// В линуксе это файл /etc/passwd
const userNames = readUserNames();

В тестах читать /etc/passwd нельзя, потому что содержимое этого файла зависит от окружения, в котором запущены тесты. Для тестирования нужно создать файл аналогичной структуры в фикстурах и указать его при запуске функции:

import fs from 'fs';

const getFixturePath = (filename) => `${__dirname}/../__fixtures__/${filename}`;

test('readUserNames', () => {
  // ../__fixtures__/passwd
  const passwdPath = getFixturePath('passwd');
  const userNames = readUserNames(passwdPath);
  expect(userNames).toEqual(/* ожидаемый список */);
});

Запись файлов

С записью файлов уже сложнее. Главная проблема – отсутствие гарантированной идемпотентности. Это значит, что повторный вызов функции, записывающей файлы, может вести себя не как первый вызов, например, завершаться с ошибкой, либо приводить к другим результатам.

Почему? Представьте себе, что мы пишем тесты на функцию log(message), которая дописывает все переданные в неё сообщения в файл:

const log = makeLogger('development.log');
await log('first message');
// cat development.log
// first message
await log('second message');
// cat development.log
// first message
// second message

Это значит, что каждый запуск тестов будет немного другим. При первом запуске тестов создаётся файл для хранения логов. Затем он начнёт заполняться. Это приводит к целой пачке проблем:

  • Наверняка внутри этой функции процесс создания файла это особый случай, который нужно тестировать отдельно. Повторные запуски тестов перестанут проверять эту ситуацию.
  • Сложнее написать предсказуемый тест. Придётся дополнительно придумывать хитрые схемы, например проверять только последнюю строку в файле. Такой подход понижает качество теста.
  • Не особенно критично, но всё же: в процессе запуска тестов появляется файл, который постоянно растёт в размерах.

При правильной организации тестов, каждый тест работает в идентичном окружении на каждом запуске. Для этого, например, можно удалять файл после выполнения каждого теста. В Jest есть хук afterEach который выполняется после каждого теста. Эту задачу можно попробовать решить с его помощью:

import fs from 'fs';

test('log', async () => {
  const log = makeLogger('development.log');

  await log('first message');
  const data1 = await fs.readFile('development.log', 'utf-8');
  expect(data1).toEqual(/* ... */)

  await log('second message');
  const data2 = await fs.readFile('development.log', 'utf-8');
  expect(data2).toEqual(/* ... */)
});

afterEach(async () => {
  await fs.unlink('development.log');
});

В большинстве ситуаций такое решение работает нормально, но всё же не во всех. Выполнение кода тестов — это не атомарная операция. Нет никакой гарантии, что хук afterEach() выполнится. Есть много причин, по которым этого может не произойти, начиная от внезапного отключения электроэнергии, заканчивая ошибками в самом Jest.

Есть только один надёжный способ делать очистку – делать это до теста, а не после, в beforeEach(). С таким подходом есть только одна небольшая сложность. При первом запуске тестов файла нет. Это значит, что прямой вызов unlink() завершится с ошибкой и тесты не смогут выполниться. Чтобы избежать этого, можно подавить ошибку:

import _ from 'lodash';

beforeEach(async () => {
  await fs.unlink('development.log').catch(_.noop);
});

Другой вопрос при записи файлов. Куда их сохранять? Однозначно избегайте записи файлов прямо внутри проекта. Если тестируемый код позволяет сконфигурировать место записи, то используйте системную временную директорию. Её можно получить через модуль os:

import os from 'os';

console.log(os.tmpdir());

Виртуальная файловая система (ФС)

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

import mock from 'mock-fs';

// Конфигурация fs
// Любые операции с этими файлами будут происходить в памяти
// без взаимодействия с реальной файловой системой
mock({
  'path/to/fake/dir': {
    'some-file.txt': 'file content here',
    'empty-dir': {/** empty directory */}
  },
  'path/to/some.png': Buffer.from([8, 6, 7, 5, 3, 0, 9]),
  'some/other/path': {/** another empty directory */}
});

await fs.unlink('some-file.txt');

Этот способ даёт идемпотентность из коробки. Вызов функции mock формирует окружение на каждый запуск с нуля. То есть достаточно добавить её в beforeEach и можно приступать к тестированию.


Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты.

Ошибки, сложный материал, вопросы >
Нашли опечатку или неточность?

Выделите текст, нажмите ctrl + enter и отправьте его нам. В течение нескольких дней мы исправим ошибку или улучшим формулировку.

Что-то не получается или материал кажется сложным?

Загляните в раздел «Обсуждение»:

  • задайте вопрос. Вы быстрее справитесь с трудностями и прокачаете навык постановки правильных вопросов, что пригодится и в учёбе, и в работе программистом;
  • расскажите о своих впечатлениях. Если курс слишком сложный, подробный отзыв поможет нам сделать его лучше;
  • изучите вопросы других учеников и ответы на них. Это база знаний, которой можно и нужно пользоваться.
Об обучении на Хекслете

Для полного доступа к курсу нужна профессиональная подписка

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

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

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

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

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

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

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff
Рекомендуемые программы

С нуля до разработчика. Возвращаем деньги, если не удалось найти работу.

Иконка программы Фронтенд-разработчик
Профессия
Разработка фронтенд-компонентов веб-приложений
22 сентября 8 месяцев
Иконка программы Node.js-разработчик
Профессия
Разработка бэкенд-компонентов веб-приложений
22 сентября 8 месяцев

Есть вопрос или хотите участвовать в обсуждении?

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы соглашаетесь c «Политикой конфиденциальности» и «Условиями оказания услуг»