При работе с валидацией на основе схем важно разделять два слоя логики: декларативное описание правил (схема) и внешние зависимости, которые могут влиять на результат проверки. В экосистеме JavaScript библиотека Yup часто используется для описания правил валидации форм, DTO и API-слоёв. Однако как только схема начинает включать асинхронные проверки или обращение к внешним данным, становится необходимым контролируемое тестирование через моки и стабы.
Моки и стабы позволяют изолировать поведение схемы от внешнего мира, обеспечивая предсказуемость тестов и воспроизводимость сценариев.
Стабы (stubs) используются для подмены конкретной реализации функции с фиксированным результатом. В контексте схем Yup это чаще всего функции, возвращающие промисы: запросы к API, проверки уникальности, обращения к базе данных.
Моки (mocks) идут дальше: они не только подменяют поведение, но и фиксируют факт вызова, его параметры и количество обращений. Это особенно важно при проверке того, как схема взаимодействует с внешними зависимостями.
В тестировании валидации различие проявляется следующим образом:
Схемы Yup поддерживают асинхронные проверки через
.test() или кастомные методы, возвращающие Promise. Это
создаёт зависимость от внешнего мира:
Без изоляции такие тесты становятся нестабильными: сетевые задержки, изменчивость данных и недоступность сервисов делают результаты непредсказуемыми.
Пример проблемного кода:
const schema = Yup.object({
email: Yup.string()
.email()
.test("unique", "Email уже используется", async (value) => {
const res = await fetch(`/api/users/check?email=${value}`);
const data = await res.json();
return data.unique;
})
});
Такую схему невозможно тестировать без стабов для
fetch.
При тестировании обычно используется подмена глобальных функций или
модулей. В Jest это реализуется через jest.fn() и
jest.mock().
Пример стабирования fetch:
global.fetch = jest.fn();
Далее задаётся поведение:
fetch.mockResolvedValue({
json: async () => ({ unique: true })
});
Теперь схема Yup получает предсказуемый ответ, не обращаясь к сети.
Тестирование становится детерминированным:
await expect(
schema.validate({ email: "test@mail.com" })
).resolves.toBeTruthy();
Когда важно не только поведение, но и факт взаимодействия, применяются моки.
expect(fetch).toHaveBeenCalledWith(
"/api/users/check?email=test@mail.com"
);
Это позволяет проверять:
Валидационные схемы часто становятся частью бизнес-логики, поэтому контроль взаимодействий критичен.
Метод .test() в Yup позволяет создавать собственные
правила:
Yup.string().test(
"is-available",
"Недоступно",
async function (value) {
const exists = await checkExists(value);
return !exists;
}
);
Функция checkExists — типичный кандидат на
стабирование.
import * as api from "./api";
jest.mock("./api");
api.checkExists.mockResolvedValue(false);
Теперь тест проверяет только логику схемы, а не реальный API.
Глобальные моки подходят для простых случаев, но более масштабируемый подход — модульное мокирование.
Пример:
import { checkEmail } from "./service";
jest.mock("./service");
Далее:
checkEmail.mockResolvedValue(true);
Это позволяет:
В реальных приложениях схема Yup может содержать несколько уровней асинхронных проверок:
Пример:
Yup.object({
username: Yup.string().test(
"available",
"Занято",
async (value) => {
const exists = await api.checkUsername(value);
const blocked = await api.checkBlacklist(value);
return !exists && !blocked;
}
)
});
Тестирование требует комбинирования стабов:
api.checkUsername.mockResolvedValue(false);
api.checkBlacklist.mockResolvedValue(false);
И моков для контроля вызовов:
expect(api.checkUsername).toHaveBeenCalledTimes(1);
expect(api.checkBlacklist).toHaveBeenCalledWith("john");
Одной из ключевых проблем является утечка состояния между тестами. Моки и стабы должны сбрасываться:
afterEach(() => {
jest.clearAllMocks();
});
Это гарантирует:
Стабы полезны не только для успешных сценариев, но и для проверки ошибок.
api.checkUsername.mockRejectedValue(new Error("Network error"));
Теперь можно проверить поведение схемы при сбое:
await expect(
schema.validate({ username: "test" })
).rejects.toThrow();
Это важно для устойчивости валидации в реальных условиях.
Не все зависимости асинхронны. Иногда схема использует синхронные утилиты:
const isForbidden = (value) => forbiddenList.includes(value);
Мокирование:
jest.spyOn(utils, "isForbidden").mockReturnValue(true);
Это позволяет моделировать разные бизнес-сценарии без изменения исходного кода.
В сложных схемах порядок проверок может быть важен.
expect(api.checkUsername.mock.invocationCallOrder[0])
Или упрощённо:
expect(api.checkUsername).toHaveBeenCalledBefore(api.checkBlacklist);
Это помогает выявлять архитектурные проблемы в логике валидации.
При грамотном подходе схема Yup рассматривается как чистая функция:
(input) → validation result
Моки и стабы обеспечивают:
Схема перестаёт зависеть от инфраструктуры и становится тестируемым модулем бизнес-логики.
В более сложных проектах вместо точечного мокирования создаются fake-реализации сервисов:
const fakeApi = {
checkUsername: jest.fn().mockResolvedValue(false),
checkBlacklist: jest.fn().mockResolvedValue(false)
};
И передача их в схему через контекст:
schema.validate(data, { context: fakeApi });
Такой подход снижает связность тестов и делает архитектуру более гибкой.
Асинхронные проверки в Yup могут выполняться параллельно. Моки позволяют выявлять гонки:
api.checkUsername.mockImplementation(
() => new Promise(resolve => setTimeout(() => resolve(false), 100))
);
Это используется для проверки устойчивости схемы к задержкам и конкурентным вызовам.
Если валидация зависит от debounce-логики, используются таймерные моки:
jest.useFakeTimers();
И управление временем:
jest.advanceTimersByTime(300);
Это позволяет тестировать поведение схемы без реального ожидания.
При структурированном подходе тестирование схем валидации строится на трёх уровнях:
Благодаря этому Yup-схемы перестают быть просто декларацией правил и становятся полностью проверяемым компонентом бизнес-логики, устойчивым к изменениям внешней среды и инфраструктуры.