Научитесь строить тестовые фикстуры для валидации email, покрывающие опечатки, мёртвые домены, catch‑all и disposable‑адреса, и автоматизировать это в CI.

Начните с нормализации ввода и тестирования того «грязного» ввода, который реально вводят пользователи: пробелы в начале/конце, заглавные буквы в домене, завершающие точки и плюс‑теги. Затем добавьте фикстуры для проверки существования домена, наличия MX, поведения catch‑all и обнаружения disposable, чтобы тесты отражали реальные риски при регистрации.
Регулярное выражение лишь говорит, что строка выглядит как email. Оно не подтверждает, что домен существует, что у него есть MX‑записи, или что это не disposable‑провайдер. В результате вы можете разрешать адреса, которые будут отскакивать или привлекать спам‑регистрации.
Разбейте валидацию на слои, которые можно тестировать отдельно: синтаксис и нормализация (офлайн), сигналы домена и MX (сетевая граница), тип провайдера (disposable или нормальный) и ваши бизнес‑правила риска. Так проще понять, какой фикстурой какой слой должен отлавливаться.
Используйте градацию результатов, например: valid, risky, invalid и unknown, вместо простого pass/fail. Catch‑all домены и таймауты — частые причины неопределённости: их трудно доказать как доставляемые, поэтому статус «risky/unknown» даёт продукту выбор: разрешить, предупредить или потребовать подтверждение.
Включите опечатки и ошибки форматирования, мёртвые домены (NXDOMAIN или отсутствие MX), случаи catch‑all, disposable‑провайдеры и похожие домены, а также ролевые и подозрительные шаблоны вроде admin@ или длинных цифровых строк. Связывайте каждую фикстуру с ожидаемым решением, чтобы тестировали поведение, а не просто парсинг строки.
Делайте фикстуры структурированными: ввод, ожидаемый результат, код причины и заметки. Добавляйте теги вроде typo, dead_domain, disposable или catch_all, чтобы при изменении правила можно было запускать только релевантный поднабор.
Делайте unit‑тесты полностью офлайн: мокайте результаты DNS/MX/API и записывайте/повторяйте ответы для контрактных тестов. Зависимости от живого интернета приводят к флаттеру и случайным падениям, которые не связаны с вашим кодом.
Мокайте границу с валидатором, чтобы поток регистрации тестировался end‑to‑end с фиксированными ответами, включая таймауты и ошибки. Отдельно держите небольшой live‑check (nightly или ручной), который бьёт по реальным DNS/сервисам, чтобы ловить дрейф без шума в каждом PR.
Считайте catch‑all сигналом, а не доказательством существования почтового ящика, и ожидайте неоднозначного результата вроде «домен OK, почтовый ящик неизвестен». В продуктовой логике разумно разрешать регистрацию, но требовать подтверждение или вводить ограничения — а тесты должны фиксировать именно такую политику.
Зафиксируйте ожидаемые структурированные ответы для каждой фикстуры и обновляйте их только при намеренном изменении политики. Если вы используете Verimail, сопоставляйте фикстуры с его сигнальными стадиями (syntax, domain, MX, disposable/blocklist) и держите CI детерминированным через заглушки, одновременно периодически прогоняя небольшой live‑набор для обнаружения изменений классификации.
Большинство наборов тестов по валидации email выглядят надёжно, но всё равно терпят неудачу в продакшене, потому что реальные адреса — это бардак. Люди вставляют пробелы, используют плюс‑теги, смешивают регистры, добавляют завершающие точки или печатают быстро на мобильном и пропускают символ. Если ваши фикстуры покрывают только чистые, учебные адреса, вы тестируете мир, в котором ваши пользователи не живут.
Ещё одна распространённая проблема — тестирование только самого простого слоя: синтаксиса. Синтаксические проверки ловят явные ошибки, но многие проблемные регистрации используют адреса, которые выглядят валидными, но домены не существуют, у доменов нет почтовой настройки, или это disposable‑провайдеры, которые постоянно меняются. Набор тестов, который останавливается на regex, создаёт ложное чувство безопасности.
Цель — не блокировать всё подозрительное. Цель — остановить плохие регистрации, не блокируя реальных людей. Это означает тестирование с обеих сторон: отвергать то, что действительно нужно отвергнуть, и принимать распространённые легитимные паттерны (например, плюс‑адресацию и субдомены).
Эти паттерны ошибок повторяются снова и снова:
Хорошие тесты покрывают слои, которые соответствуют тому, как работает валидация при реальной регистрации: синтаксис (правила в духе RFC), сигналы домена (существует ли домен, MX‑записи), сигналы почтового ящика (поведение catch‑all или шаблоны приёма) и сигналы риска (блок‑листы и обнаружение disposable).
Установите ожидания заранее: некоторые исходы вероятностны. Catch‑all домены могут принимать почту для любого имени без подтверждения существования конкретного ящика. Списки disposable меняются ежедневно. Даже инструменты уровня предприятия, такие как Verimail, могут возвращать рискованные сигналы, которые лучше обрабатывать продуктовыми правилами (разрешить, заблокировать или требовать доп. проверку), вместо того чтобы притворяться, что для каждого случая есть идеальный ответ.
Валидация email кажется одной проверкой, но на деле это цепочка маленьких проверок. Разделите эту цепочку на слои — и ваши тесты станут яснее и проще в поддержке. Это также помогает создавать фикстуры, которые соответствуют реальным вводам пользователей, а не случайным строкам.
Практичная модель слоёв выглядит так:
Не каждый слой следует тестировать одинаково. Некоторые слои — чистая логика и могут выполняться быстро и офлайн (синтаксис, большинство правил риска). Другие зависят от сети (DNS, MX‑lookup, realtime блок‑листы). Обращайтесь с ними по‑разному, чтобы набор тестов оставался стабильным.
Далее решите, что ваше приложение делает с результатами каждого слоя. Многие команды сводят всё к pass/fail и получают запутанные крайние случаи. Градация исходов обычно удобнее:
Практический пример: пользователь вводит [email protected]. Синтаксис проходит, но ваши правила опечаток отмечают это как рискованное. Вы можете разрешить регистрацию, но попросить подтвердить адрес. Если введён [email protected], синтаксис проходит, но домен или MX не проходят — значит вы блокируете.
Пишете тесты — сопоставляйте каждую фикстуру со слоем, который должен её поймать, и с ожидаемым исходом. Для сетевых слоёв предпочитайте краевые тесты (например, как вы обрабатываете ошибку DNS vs реальный результат no‑MX) и держите остальное как быстрые unit‑тесты. Если вы используете API вроде Verimail, вы можете отразить его многоступенчатые результаты (syntax, domain, MX, disposable‑проверки) в собственных ожиданиях по слоям.
Хорошие фикстуры для валидации email похожи на то, что реально вводят пользователи, а не на случайные строки. Создавая фикстуры вокруг нескольких понятных категорий, вы делаете тесты читабельными и легко заметите пропуски.
Держите эти основные категории в каждом наборе:
@, двойной @@, пробелы в начале/конце, двойные точки, точка прямо перед @ и перестановка символов в известных доменах. Эти случаи должны быстро падать на слое синтаксиса до любых сетевых проверок.admin@, support@, info@ и подозрительные шаблоны вроде длинных числовых строк. Тестируйте вашу политическую логику (разрешать, предупреждать или блокировать), вместо того чтобы считать их всегда плохими.Чтобы категории были полезными, связывайте каждую фикстуру с ожидаемым решением, а не только с валидностью. Метки вроде syntax_invalid, domain_invalid, disposable_block, risky_allow_with_warning, unknown_catch_all упрощают интерпретацию ошибок.
Если вы используете валидатор API, например Verimail, сопоставляйте эти категории со стадиями пайплайна, которые вы ожидаете вызвать (syntax, domain checks, MX lookup, disposable blocklist match). Тогда упавший тест сообщит, какого рода ошибка произошла, а не просто что «что‑то сломалось».
Хорошие фикстуры выглядят как реальные регистрации, а не как набор случайных символов. Когда тест падает, вы должны сразу понимать, что произошло и почему это важно.
Рассматривайте фикстуры как маленькие записи с согласованной структурой: входной email, ожидаемый исход (принять, отклонить или требовать доп. проверку) и короткая причина. Добавьте поле заметок для всего, что вы быстро забудете (почему выбран домен или какое поведение ожидается при смене провайдера).
Вот компактный пример, который можно скопировать в репозиторий:
[
{
"name": "typo_gmail_missing_dot",
"input": "alex@gmailcom",
"expected": "reject",
"reason": "typo",
"tags": ["typo"],
"notes": "Missing dot in domain; should fail domain validation."
},
{
"name": "disposable_known_provider",
"input": "[email protected]",
"expected": "reject",
"reason": "disposable",
"tags": ["disposable"],
"notes": "Should be blocked by disposable provider list."
}
]
Теги облегчают расширение набора. Вместо одного огромного файла держите небольшие именованные наборы по категориям (typos, dead domains, catch‑all, disposable, edge cases). Тогда вы сможете запускать только то, что нужно, например только disposable‑кейсы после обновления классификации.
Правила нормализации тоже должны быть в фикстурах, потому что пользователи вставляют «грязные» данные. Включите случаи, которые доказывают, что вы обрабатываете:
Версионируйте фикстуры как код. Каждое новое добавление должно отвечать на один вопрос: какую ошибку оно предотвратило? Добавьте короткую заметку, требуйте ревью при изменениях и удаляйте дубликаты. Со временем фикстуры станут живой картой ошибок и атак, с которыми сталкивается ваша регистрация.
Если вы полагаетесь на ответ API (например, Verimail), храните прикреплённый ожидаемый ответ для каждой фикстуры и обновляйте его только при намеренной смене политики.
Проверки email касаются вещей, которые меняются без предупреждения: записи DNS, настройки почтовых серверов и экосистема disposable‑провайдеров. Если ваши фикстуры зависят от живого интернета, тесты будут падать в случайный вторник по причинам, не связанным с вашим кодом.
Начните с разделения того, что можно сделать детерминированным, и того, что нет. Синтаксические случаи просты: они не должны требовать сетевых вызовов. Сложность возникает с тем, что проверяет достижимость домена или почтового ящика.
Стабильный набор фикстур обычно формируется из нескольких источников:
Избегайте использования реальных адресов людей в фикстурах, даже если они публичны. Используйте синтетические локальные части (например, "user" или "test") и явно фейковые имена. Это снижает риск утечки приватных данных и предотвращает случайные отправки, если тестовые данные попадут в логи или downstream‑системы.
Для сценариев мёртвых доменов самый стабильный подход — трактовать отказ DNS как тестовые данные, а не как живую реальность. Записывайте или мокаьте результат резолва (например, NXDOMAIN или отсутствие MX) и проверяйте, что логика обрабатывает это корректно. Живые мёртвые домены нестабильны, потому что домены могут быть куплены и настроены позже.
Disposable‑детекция требует особого ухода. Провайдеры появляются, исчезают и меняют домены часто. Держите версионированный снапшот вашего disposable‑списка (или классификаций вендора) и определите политику по дрейфу. Например: unit‑тесты запускаются против снапшота, а отдельная задача по расписанию проверяет изменения и создаёт задачу на ревью.
Запишите, какие фикстуры чувствительны ко времени и что вы будете делать при их изменении: обновлять снапшот, ослаблять утверждение или переводить кейс в контрактный тест. Если вы используете Verimail в продакшене, рассматривайте его ре‑тайм классификацию как то, что тестируется зафиксированными записями, а затем периодически проверяйте небольшим контролируемым live‑набором.
Быстрые и надёжные тесты начинаются с частей валидации, которые не касаются сети. Рассматривайте валидатор как набор чистых функций (ввод —> вывод) и зафиксируйте контракт того, чего остальная часть приложения ожидает от них.
Unit‑тесты должны покрывать синтаксис и нормализацию, потому что на них легко влияют мелкие рефакторы. Примеры: удаление пробелов, приведение домена к нижнему регистру, отклонение двойного @ и обработка очевидных опечаток вроде пропущенной точки в домене.
При создании фикстур делайте каждую запись самодокументируемой: ввод, ожидаемое нормализованное значение (или пустое) и короткий код причины. Коды причин проще проверять, чем полные фразы.
Простой паттерн — table‑driven тесты: проходите по таблице фикстур и проверяйте и статус, и причину для каждого случая. Это держит файл тестов коротким, покрывая при этом много сценариев.
Контрактные тесты отвечают на другой вопрос: возвращает ли модуль валидации всегда одну и ту же структуру? Если одна команда ожидает { status, reason, normalized } и другая версия невзначай меняет её, вы получите баги, которые выглядят как случайные.
Контрактные тесты должны проверять такие вещи, как:
status, reason, normalized_email)valid, invalid, risky)Тесты на свойства (property‑based tests) тоже помогают. Вместо ручной записи каждой опечатки генерируйте «почти‑угаданные» вариации: лишние пробелы, перестановки символов вокруг @, повторяющиеся точки или смешанный регистр в домене. Цель — поймать баги парсера и крайние случаи, которые вы могли не предусмотреть.
Снапшот‑тестирование полезно для UI‑сообщений, но используйте его осторожно. Предпочитайте снапшоты стабильных кодов ошибок, а не полного текста. Если нужно фиксировать сообщения, делайте их короткими и консистентными, чтобы мелкие правки текста не ломали половину тестовой базы.
Именно на интеграционных тестах логика валидации email часто становится ненадёжной. Как только тест зависит от живого DNS, MX‑lookup или доступности стороннего сервиса, вы получаете случайные падения, не связанные с вашим кодом.
Для повседневных прогонов в CI стремитесь к быстрым и воспроизводимым тестам. Относитесь к сети как к входным данным, которые вы контролируете.
Практический подход — мокать сетевую границу и проверять, что делает ваше приложение с каждым результатом. Если ваш сервис регистрации вызывает API валидатора, замените этот вызов заглушкой, возвращающей известные ответы для ваших фикстур. Вы всё ещё тестируете полный поток (контроллер, сервис, решение по политике, сообщение об ошибке), но не тестируете интернет.
Паттерны, которые обычно работают:
Таймауты заслуживают особого внимания. Решите политику и протестируйте её явно: при таймауте считаете ли вы email недействительным или как неизвестный и позволяете пользователю продолжить с дополнительной верификацией? Оба подхода могут быть верны, но только если они последовательны.
Мокнутые тесты могут дрейфовать относительно реального мира. Домен, у которого были MX‑записи, может умереть; классификация disposable может измениться. Чтобы поймать дрейф, держите отдельную задачу, которая делает реальные сетевые вызовы, но не запускайте её на каждый PR.
Типичная настройка — ночной прогон или ручной workflow. Держите этот слой маленьким: несколько репрезентативных адресов по категориям (опечатки, мёртвые домены, catch‑all, disposable) достаточно, чтобы заметить дрейф без большого шума.
Если вы используете Verimail в продакшене, ваши CI‑тесты могут стабыть его ответ disposable: true для фикстуры вроде [email protected], и вы проверяете, что UI блокирует регистрацию и показывает нужное сообщение. Ночной прогон может делать реальные запросы к API с контролируемым набором адресов и уведомлять, если результаты поменялись — чтобы вы обновили фикстуры или политику до того, как на это обратят внимание пользователи.
Набор тестов может выглядеть насыщенно и при этом пропускать ошибки, которые бьют по продакшну. Самый большой риск — ложная уверенность: тесты проходят, но реальные пользователи блокируются или фейковые регистрации проходят.
Одна из ловушек — считать catch‑all доказательством существования почтового ящика. Catch‑all лишь означает, что домен принимает почту для любого адреса. Если вы автоматически одобряете всё на catch‑all, фикстуры тихо приучат вас принимать мусорные адреса.
Другая ловушка — использовать только regex и называть это валидацией. Regex ловит очевидные проблемы с форматом, но не скажет, существует ли домен, есть ли у него MX или принадлежит ли адрес disposable‑провайдеру. Если тесты покрывают лишь строковые шаблоны, вы тестируете ваш regex, а не поведение email.
Ещё частая ошибка — жёстко блокировать регистрации при временных проблемах DNS. Сети временно отваливаются. Если ваши тесты покрывают только сцену «DNS работает» и «DNS не работает», вы рискуете отвергать хороших пользователей во время краткой аутентификации. Лучше трактовать некоторые ошибки как unknown и ретраить или разрешать регистрацию с пометкой на последующую верификацию.
Международные домены и современные правила почты легко забыть. Плюс‑адресация (например, [email protected]) валидна и широко используется. У некоторых пользователей домены с не‑ASCII символами. Если ваши фикстуры никогда не включают такие случаи, вы выпустите валидатор, который ломается на нормальном вводе.
Несколько способов, как команды себя обманывают:
+ недопустим или удаляют его неправильно.Практическая защита — разделять результаты на ясные бакеты (invalid, risky, unknown, valid) и тестировать переходы между ними. Инструменты вроде Verimail помогают, возвращая структурированные сигналы (syntax, domain, MX, disposable), что облегчает ассерты по поведению, а не по одному pass/fail.
Перед мерджем пробегитесь быстро по покрытию и уверенности. Цель — не протестировать каждый email на планете, а убедиться, что фикстуры ведут себя как реальные регистрации и падают предсказуемо.
Просмотрите набор фикстур по категориям. Если в категории только один‑два примера, маленькое изменение кода может повредить реальным пользователям, а тесты останутся зелёными. Стремитесь к небольшому кластерику случаев на категорию (практическая цель — 5–10 на категорию для опечаток, мёртвых доменов, catch‑all, disposable и ваших бизнес‑правил).
Используйте этот краткий чек‑лист:
Не пренебрегайте стабильностью. Тесты, зависящие от живых доменов, со временем портятся, а поведение catch‑all может измениться без предупреждения. Для unit‑тестов предпочитайте фикстуры без сети и держите поведение доменов в виде моков. Для интеграционных тестов ограничьте область и делайте их осознанными. Простой паттерн — небольшой ночной job, который прогоняет несколько известных кейсов против вашего валидатора, пока CI остаётся сосредоточенным на детерминированных unit‑тестах.
Обычная проблема SaaS: форма регистрации выглядит хорошо при ручном тестировании, но после запуска вы получаете волны спам‑регистраций. Многие используют disposable‑адреса — это плохие лиды, больше bounceов и запутанная база пользователей.
Практичный подход — определить чёткое поведение и зафиксировать его фикстурами. Например: блокировать disposable, отвергать мёртвые домены и разрешать (но предупреждать о) catch‑all.
Небольшой набор фикстур, покрывающий реалистичные входы без опоры на случайные строки:
[email protected] (выглядит как реальный адрес, но домен с опечаткой)[email protected] (используйте .invalid для домена, который никогда не должен резолвиться)[email protected] (тут в моках трактуйте как catch‑all, а не как факт из DNS)[email protected] (в моках помечайте как disposable, а не брать за настоящий провайдер)[email protected] (нормальный адрес для «happy path»)Ключевая мысль — ваше приложение не должно пытаться доказывать catch‑all или disposable статус через публичный интернет в unit‑тестах. Вместо этого слой валидации должен возвращать нормализованный результат, на который ваша логика регистрации опирается.
Простые правила для endpoint регистрации:
is_disposable = true: блокируйте регистрацию с понятной ошибкойdomain_status = dead: отклоняйте и просите другой emailis_catch_all = true: разрешайте регистрацию, но показывайте предупреждение (и рассматривайте доп. верификацию)Чтобы CI был быстрым и предсказуемым, разделите тесты на два класса: быстрые unit‑тесты для логики принятия решений и замоканные интеграционные тесты для границы валидатора.
// Example: decision logic unit tests (no network)
const cases = [
{ email: "[email protected]", result: { is_disposable: true }, expect: "BLOCK" },
{ email: "[email protected]", result: { domain_status: "dead" }, expect: "REJECT" },
{ email: "[email protected]", result: { is_catch_all: true }, expect: "WARN" },
{ email: "[email protected]", result: { suggestion: "gmail.com" }, expect: "REJECT" },
{ email: "[email protected]", result: { ok: true }, expect: "ALLOW" },
];
for (const c of cases) {
const decision = decideSignup(c.email, c.result);
expect(decision).toBe(c.expect);
}
В CI мокнутый интеграционный тест может проверить, что ваш код регистрации вызывает валидатор ровно один раз и корректно обрабатывает таймауты и ошибки ответа, не делая реальных DNS или MX‑lookup запросов.
Если вы хотите реальные проверки (RFC‑совместный синтаксис, проверка домена, MX‑lookup и сопоставление с disposable/blocklist), не строя и не поддерживая эту логику самостоятельно, API‑владельцы по валидации email — хорошая альтернатива. Для команд, которые уже используют Verimail, простой паттерн — держать большинство тестов замоканными и детерминированными, а затем запускать небольшой набор контрактных проверок, чтобы убедиться, что интеграция с verimail.co по‑прежнему соответствует ожидаемой структуре ответа.