Тестовые данные, которые проходят вашу же валидацию
Половина тестовых баз собрана из боевой выгрузки, вторая половина не проходит собственную валидацию проекта. Разбираем третий путь: вымышленные данные, которые устроены по правилам настоящих.
Тестовый контур собрали быстрым способом: сняли ночную копию боевой базы, вырезали пароли, заменили почту на служебную и выкатили на стенд. Через полгода на этом стенде работали подрядчик по фронтенду, аналитик из соседнего отдела и стажёр, у которого доступ выдали на неделю и забыли закрыть. Персональные данные полутора миллионов клиентов лежали там же, где и в проде, только без половины защитных мер и без внимания службы безопасности.
Второй вариант той же истории выглядит безобиднее. Данные для тестов написал разработчик: двенадцать случайных цифр в поле ИНН, одиннадцать — в СНИЛС, номер карты из четырёх групп по четыре. Тест на форму регистрации зелёный. Он и должен быть зелёным: сохранение вызывается с валидными по формату строками, а собственная проверка контрольного разряда в этом сценарии никогда не отрабатывает положительный исход — она только отвергает.
Обе команды получили тестовые данные. Первая — вместе с риском утечки, вторая — вместе с иллюзией покрытия.
Где именно течёт
Проблема с тестовыми данными почти никогда не выглядит как проблема. Стенд работает, тесты зелёные, демонстрация проходит гладко. Расплата приходит в другом месте и позже.
Копия боевой базы на тестовом стенде
- Что происходит
- Реальные ФИО, телефоны и паспорта лежат в контуре, где доступ шире, журналирование скромнее, а бэкапы никто не шифрует.
- Во что обходится
- Инцидент с персональными данными не делает скидку на то, что база называлась тестовой. Плюс постоянная работа: каждую выгрузку надо обезличивать заново, и каждый раз кто-то забывает одно поле.
- Чем закрывается
- Вымышленные записи, устроенные по тем же правилам. Стенд наполняется без выгрузки из прода, а значит без согласований и без данных, которые нельзя потерять.
Случайные цифры вместо реквизитов
- Что происходит
- В тестовой фикстуре ИНН — двенадцать случайных цифр. Собственная проверка контрольного разряда отвергает такую строку на входе.
- Во что обходится
- Половина сценариев не проходит дальше валидации, и разработчик отключает её в тестовом окружении. С этого момента вся логика, которая идёт после проверки, не покрыта вовсе.
- Чем закрывается
- Контрольные разряды рассчитываются по алгоритмам ФНС, карта — по алгоритму Луна, VIN — по ISO 3779. Данные проходят вашу проверку, поэтому её не нужно выключать.
Плавающие данные в автотесте
- Что происходит
- Генератор выдаёт новые значения при каждом запуске. Проверку «пользователь с таким ИНН виден в списке» написать нельзя: завтра ИНН другой.
- Во что обходится
- Остаются проверки «данные вообще есть» — они пропускают ровно те ошибки, ради которых тест писался. Упавший тест невозможно повторить: входные данные исчезли вместе с запуском.
- Чем закрывается
- Начальное значение генератора возвращается в ответе. Записав его в тест, вы получаете ту же выдачу до последней цифры и можете воспроизвести падение через месяц.
Сколько стоит «просто снять копию»
Скрипт обезличивания — не разовая задача. Он живёт столько же, сколько живёт схема базы, и ломается при каждом её изменении.
| Работа | Копия прода с обезличиванием | Сгенерированные записи |
|---|---|---|
| Первичная настройка | Скрипт по всем таблицам с персональными данными, согласование со службой безопасности | Список нужных полей и количество записей |
| Новое поле в схеме | Правка скрипта, иначе поле уедет на стенд как есть | Ничего: генератор отдаёт только запрошенное |
| Обновление стенда | Ночная выгрузка, прогон скрипта, проверка результата | Запрос в момент подготовки данных теста |
| Риск утечки | Есть всегда, пока данные настоящие | Отсутствует: восстанавливать нечего |
| Воспроизводимость | Зависит от того, что было в базе ночью | Точная, по сохранённому начальному значению |
Цену запроса считать просто: единица работы — одна запись. Сто записей стоят сто единиц и расходуют сто единиц дневного лимита. Предел в сто записей на вызов существует не из-за производительности — сотня строится за доли миллисекунды, — а чтобы опечатка в параметре не списала заметную сумму одним запросом. Актуальные цены — на странице тарифов.
Как устроены реквизиты, которые проходят проверку
Разница между «случайными цифрами» и «тестовым реквизитом» — в двух свойствах: у номера сходится контрольная сумма, и он согласован с остальными полями своей записи.
| Поле | Как построено | С чем согласовано |
|---|---|---|
| ИНН физлица | 12 цифр, два контрольных разряда по алгоритму ФНС | Первые четыре цифры — код налогового органа, начинается с кода региона записи |
| ИНН юрлица и КПП | 10 цифр с контрольным разрядом; КПП — 9 знаков | Первые четыре знака КПП совпадают с кодом налогового органа в ИНН той же записи |
| СНИЛС | 11 цифр, контрольное число по остатку от деления на 101 | Самостоятельное поле |
| Расчётный счёт | 20 цифр, контрольный разряд рассчитан по БИК | Валиден только в паре с БИК своей записи; название банка соответствует БИК |
| Карта | Номер по алгоритму Луна, платёжная система и срок действия | Срок действия всегда в будущем — иначе карта бесполезна в тестах оплаты |
| Паспорт и автомобильный номер | Серия и номер; знак вида А123ВС777 | Код региона в обоих — регион записи, он же в первых цифрах индекса |
| VIN | 17 символов с контрольным символом по ISO 3779 | Самостоятельное поле |
Из согласованности следует практическое правило: перемешивать поля между записями нельзя. Взять ИНН из одной строки, а КПП из другой — значит получить пару, которую ваша же перекрёстная проверка отвергнет, и потратить вечер на поиск ошибки в коде, где её нет.
Код: фикстуры, которые повторяются
Все примеры работают на публичном демо-ключе ak_sandbox_demo_mockdata_v1.
Отдельного режима моков у генератора нет и не нужно: сервис локальный и ничего не ищет во
внешних источниках, поэтому демо-ключ получает настоящий ответ — тот же, что и боевой.
# Справочник доступных полей — бесплатно
curl -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
"https://atlorium.com/api/testdata/fields"
# Пять записей с тремя полями и закреплённым начальным значением
curl -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
"https://atlorium.com/api/testdata?count=5&fields=fullName,innPerson,snils&seed=42"
# То же телом запроса — удобнее, когда полей много
curl -X POST \
-H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
-H "Content-Type: application/json" \
-d '{"count":5,"seed":42,"fields":["fullName","innCompany","kpp","bic","bankAccount"],"gender":"female"}' \
"https://atlorium.com/api/testdata"
# CSV: использованное начальное значение приезжает заголовком ответа
curl -D - -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
"https://atlorium.com/api/testdata?count=100&format=csv&fields=fullName,innPerson,phone"
import requests
BASE = "https://atlorium.com"
HEADERS = {"Authorization": "Bearer ak_sandbox_demo_mockdata_v1"}
# Начальное значение закреплено в коде теста: пока оно не меняется,
# данные не меняются тоже — и проверку можно писать на конкретный ИНН.
COMPANY_SEED = 20260818
def testdata(count: int, fields: list[str], seed: int | None = None, gender: str = "any") -> dict:
resp = requests.post(
f"{BASE}/api/testdata",
json={"count": count, "fields": fields, "seed": seed, "gender": gender},
headers=HEADERS,
timeout=15,
)
resp.raise_for_status()
return resp.json()
def company_fixture() -> dict:
"""Карточка юрлица для теста: реквизиты согласованы между собой."""
data = testdata(
count=1,
fields=["companyName", "innCompany", "kpp", "ogrn", "bic", "bankName", "bankAccount"],
seed=COMPANY_SEED,
)
# Все значения строковые: тестовые данные почти всегда идут дальше как текст.
return data["records"][0]
company = company_fixture()
# Проверяем не «что-нибудь сохранилось», а конкретную запись.
assert company["innCompany"] == create_counterparty(company)["inn"]
# Поля в ответе идут в порядке каталога, а не в порядке запроса:
# CSV из двух разных запросов с одним набором полей имеет одинаковые колонки.
print(company_fixture() == company_fixture()) # True: тот же seed — та же выдача
// Набор данных для нагрузочного прогона: сто записей за вызов — предел,
// объём больше набирается несколькими запросами, каждый со своим seed.
public sealed record TestDataResponseDto(
int Seed,
int Count,
List<string> Fields,
List<Dictionary<string, string>> Records,
string Disclaimer);
private static async Task<IReadOnlyList<Dictionary<string, string>>> LoadAsync(
HttpClient http, int total, int seedBase)
{
var records = new List<Dictionary<string, string>>(total);
for (var offset = 0; offset < total; offset += 100)
{
var take = Math.Min(100, total - offset);
// Свой seed на каждую сотню: иначе пачки повторяли бы друг друга,
// и нагрузочный прогон измерял бы работу кэша, а не системы.
var url = $"/api/testdata?count={take}&seed={seedBase + offset}" +
"&fields=fullName,innPerson,phone,address";
var page = await http.GetFromJsonAsync<TestDataResponseDto>(url);
records.AddRange(page!.Records);
}
return records;
}
// GET /api/testdata?count=2&fields=fullName,innPerson,snils&seed=42
{
"seed": 42,
"count": 2,
"fields": ["fullName", "innPerson", "snils"],
"records": [
{
"fullName": "Анисимов Ерлан Ефимович",
"innPerson": "026881836682",
"snils": "356-272-088 87"
},
{
"fullName": "Симонов Заур Давидович",
"innPerson": "167396633704",
"snils": "740-159-186 86"
}
],
"disclaimer": "Данные вымышлены и предназначены исключительно для тестирования. Совпадение с реальными людьми и организациями случайно."
}
Тот же вызов на шести языках (Python, TypeScript, Go, Java, C#, PHP) — в репозитории test-data-generator-api-client. Примеры запускаются сразу: ключ в них уже стоит демонстрационный.
Три детали контракта, о которые чаще всего спотыкаются. Все значения в записи — строки: тестовые данные почти всегда идут дальше как текст, в CSV, в форму или в SQL-скрипт, и единый тип избавляет от разбора разнотипного JSON. Порядок полей задаёт каталог, а не запрос, — поэтому файлы из разных запросов складываются друг с другом без сверки заголовков. И напоминание о вымышленности едет отдельным полем в каждом ответе, чтобы выгрузка, попавшая в журнал или в чужие руки, сама говорила о своей природе.
Чего генератор не умеет
Он не проверяет, существует ли получившийся номер. Сервис не обращается ни к одному реестру: он строит строку, устроенную по правилам. Поэтому она проходит проверку контрольного разряда, но не соответствует ни одному живому человеку или предприятию. Совпадение с настоящим реквизитом случайно — и это единственное честное утверждение, которое здесь можно сделать.
Адреса берутся из встроенных наборов, а не из государственного реестра. Они правдоподобны и согласованы с регионом записи, но искать такой адрес в ГАР/ФИАС бессмысленно. Нужен настоящий существующий адрес — это отдельные сервисы: справочник ГАР/ФИАС и стандартизация адреса.
Одно поле всё-таки зависит от календаря. Срок действия карты отсчитывается от текущего года, потому что просроченная карта бесполезна в тестах оплаты: через год тот же seed даст срок на год дальше. Остальные поля, включая дату рождения, от даты запроса не зависят — иначе тест, стабильный вчера, покраснел бы сам собой.
Регион записи не выбирается. Пол задать можно — мужской, женский или случайный по каждой записи. Регион выбирает генератор, потому что он связывает сразу несколько полей: ИНН, серию паспорта, автомобильный номер и почтовый индекс. Отдельный параметр здесь означал бы, что запись собирается из кусков, а не строится целиком.
Это не инструмент для обхода чужих проверок. Данные предназначены для тестов, стендов, нагрузочных прогонов и обучающих материалов. Выдавать их за настоящие реквизиты, оформлять на них заявки и регистрации или использовать для подделки документов недопустимо — и напоминание об этом мы возвращаем вместе с каждым ответом не для формальности.
Частые вопросы
Частые вопросы
Эти ИНН и СНИЛС настоящие?
Нет. Они вымышлены и никому не принадлежат: сервис не обращается ни к каким реестрам и не проверяет, существует ли получившийся номер. Он строит строку, устроенную по правилам, поэтому она проходит проверку контрольного разряда, но не соответствует ни одному живому человеку или предприятию.
Чем это отличается от генератора случайных строк?
Тем, что данные проходят вашу проверку. Двенадцать случайных цифр — это не ИНН: валидация отвергнет их на входе, и тест не проверит ничего, кроме умения валидации говорить «нет». Здесь контрольные разряды рассчитаны по алгоритмам ФНС, счёт согласован со своим БИК, карта валидна по алгоритму Луна, а VIN несёт контрольный символ по ISO 3779.
Как получить одинаковые тестовые данные дважды?
Передайте одно и то же начальное значение генератора (seed) при том же наборе полей и количестве записей — выдача совпадёт до последней цифры. Если seed не задан, сервис выбирает его сам и возвращает: в JSON отдельным полем, в CSV — заголовком ответа X-Atlorium-Seed. Сохранив это число, вы повторите выдачу в любой момент.
Сколько записей можно получить за один запрос?
До ста. Предел не про производительность, а про предсказуемость расхода: запрос стоит столько единиц, сколько в нём записей, и потолок не даёт опечаткой в параметре списать заметную сумму одним вызовом. Нужен больший объём — это несколько запросов, каждый со своим начальным значением.
Можно ли использовать такие данные для регистрации на сайтах?
Нет. Данные предназначены для тестов, демонстрационных стендов, нагрузочных прогонов и обучающих материалов. Выдавать их за настоящие реквизиты, оформлять заявки от несуществующих лиц и обходить проверки в чужих системах недопустимо.
Почему поля в ответе идут не в том порядке, что в запросе?
Порядок задаёт каталог полей, а не запрос. Так CSV из двух разных запросов с одним набором полей всегда имеет одинаковые колонки, и файлы можно складывать друг с другом, не сверяя заголовки. Каталог сгруппирован по смыслу, а не по алфавиту: родственные поля стоят рядом. Дубликаты в списке полей молча схлопываются.
Сервисы из этой статьи
Вымышленные, но формально верные российские реквизиты для тестов: ИНН, СНИЛС, ОГРН, счёт с БИК, паспорт, карта — все контрольные суммы сходятся
Попробовать прямо сейчас — без регистрации
Демо-ключ ak_sandbox_demo_mockdata_v1 — публичный и общий для всех.
С ним API отвечает моками: данные правдоподобные, но сгенерированные,
и они не меняются от запроса к запросу — на них удобно писать тесты.
Настоящие данные приходят с личным ключом.
curl -H "Authorization: Bearer ak_sandbox_demo_mockdata_v1" \
https://atlorium.com/openapi/td_ru.json