Далеко не всегда результат работы функции связан с побочным эффектом, как это было в предыдущем уроке. Иногда побочный эффект это просто дополнительное действие, которое скорее мешает протестировать основную логику.
Представьте себе функцию, которая регистрирует пользователя. Она создает запись в базе данных и отправляет приветственное письмо:
const params = {
email: 'lala@example.com',
password: 'qwerty',
};
registerUser(params);
Эта функция делает много всего, но главное, что нас волнует — правильная регистрация пользователя. Типичная регистрация сводится к добавлению в базу данных записи о новом пользователе. Именно это и нужно проверять — наличие новой записи в базе данных с правильно заполненными данными. А вот возврат функции нам никак не поможет.
Как правило, базу данных в тестах не прячут. В веб-фреймворках она доступна в тестовой среде и работает как обычно. Идемпотентность в ней достигается за счет транзакций. Перед тестом транзакция начинается и после теста откатывается. Благодаря этому каждый тест запускается в идентичном окружении и не важно как он его меняет:
// Гипотетический пример
const ctx = /* connect to db */;
beforeEach(() => ctx.beginTransaction());
test('registerUser', () => {
// Внутри идет добавление данных в базу
const id = registerUser({ name: 'Mike' });
const user = User.find(id);
expect(user).toHaveProperty('name', 'Mike');
})
// За счет отката база возвращается в исходное состояние
afterEach(() => ctx.rollbackTransaction());
А вот с отправкой писем все сложнее. Ее точно делать нельзя, но как добиться такого поведения? Посмотрите на то, как примерно может выглядеть функция регистрации пользователя:
import sendEmail from './emailSender.js';
const registerUser = (params) => {
const user = new User(params);
if (user.save()) {
sendEmail('registration', { user });
return true;
}
return false;
}
Существует несколько подходов, позволяющих отключить отправку в тестах. Самый простой — переменная окружения, которая показывает среду выполнения:
// Выполняем этот код только если мы не в тестовой среде
if (process.env.NODE_ENV !== 'test') {
sendEmail('registration', { user });
}
Несмотря на простоту использования, такой подход считается плохой практикой. Формально, из-за него происходит нарушение абстракции, код начинает знать о том, где он выполняется. Со временем таких проверок становится все больше и код становится грязнее. Более того, если нам все же надо убедиться, что письмо отправляется (с правильными данными!), то мы не сможем этого сделать.
Следующий способ — поддержка режима тестирования внутри самой библиотеки. Например, где-нибудь на этапе инициализации тестов можно сделать так:
// setup.js в jest
import sendEmail from './emailSender.js';
// У этого подхода много разновидностей, начиная от установки флага,
// заканчивая заменой функций в прототипе.
sendEmail.test = true;
Теперь в любом другом месте, где импортируется и используется функция sendEmail()
, реальная отправка происходить не будет:
// Ничего не происходит
sendEmail('registration', { user });
// В отличие от первого варианта, прикладной код ни о чем не догадывается
Это довольно популярное решение. Обычно информация о том, как правильно включить режим тестирования, находится в официальной документации конкретной библиотеки.
Что делать, если используемая библиотека не поддерживает режим тестирования? Существует еще один, наиболее универсальный способ. Он основан на применении инверсии зависимостей. Программу можно изменить так, чтобы она вызывала функцию sendEmail()
не напрямую, а принимала ее как параметр:
import sendEmail from './emailSender.js';
// Ставим значение по умолчанию, чтобы не пришлось постоянно указывать функцию
const registerUser = (params, send = sendEmail) => {
const user = new User(params);
if (user.save()) {
send('registration', { user });
return true;
}
return false;
}
Сначала создадим функцию-замену, которая будет имитировать работу реальной функции отправки писем, но без фактической отправки. Внутри этой функции мы можем выполнять какие-то действия, в зависимости от того, что хотим получить. Например, текст письма можно вывести в терминал для удобства отладки. А можем вообще ничего не делать, оставить тело пустым
const fakeSendEmail = (...args) => {
// Тут выполняем какие-то действия
// Или вообще ничего не делаем
};
Теперь сам тест. Передаем нашу фейковую функцию отправки письма в качестве параметра при регистрации пользователя
test('registerUser', () => {
const id = registerUser({ name: 'Mike' }, fakeSendEmail);
const user = User.find(id);
expect(user).toHaveProperty('name', 'Mike');
});
Ее вызов внутри функции registerUser()
отработает, но письмо отправляться не будет
Такой способ сложнее в реализации, особенно если функция находится глубоко в стеке вызовов. Это значит, что придется прокидывать нужные зависимости через всю цепочку функций сверху вниз. Самих зависимостей может быть много, и чем больше используется инверсия, тем сложнее код. За гибкость приходится платить.
Теперь плюсы. Ни библиотека, ни код ничего не знают про тесты. Этот способ наиболее гибкий, он позволяет задавать конкретное поведение для конкретной ситуации. В некоторых экосистемах инверсия зависимостей определяет процесс сборки приложения. Особенно в мире PHP, Java и C#.
Дополнительные материалы
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты
- Статья «Как учиться и справляться с негативными мыслями»
- Статья «Ловушки обучения»
- Статья «Сложные простые задачи по программированию»
- Вебинар «Как самостоятельно учиться»
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.