Привет, Хабр! Наша компания создает множество полезных программ и сервисов, в том числе для автоматизации работы с данными граждан. Сегодня расскажем вам о том, как у нас тестируются сложные API формы, и как мы справились с основными проблемами в автоматизации их тестирования.
Пример
Для системы записи ребенка в некой школе требуется передать его данные по API в виде структуры JSON, вида:
{“surname”: “Соколов”,
“name”: “Артур”,
“patronymic”: “Сергеевич”,
“birthdate”: “2008-06-26”,
“gender”: “m”,
“birth_no”: “XX-МЯ 123456‘:
“pass_no”: “4432123456”}
Для первичной проверки на каждое поле наложены ограничения (мы сильно сократили пример, так как нужно описать суть проблем, а не напугать читателя):
Фамилия “surname”
Буквы русского алфавита, римские цифры, дефис и апостроф
1-50 символов
Обязательное
Возможные ошибки:
Фамилия может содержать только буквы русского алфавита, римские цифры, дефис и апостроф
Фамилия не должна быть длиннее 50 символов
Фамилия обязательна для заполнения
Имя “name”:
Буквы русского алфавита, римские цифры, дефис, апостроф
1-50 символов
Обязательное
Возможные ошибки:
Имя может содержать только буквы русского алфавита, римские цифры, дефис и апостроф
Имя не должно быть длиннее 50 символов
Имя обязательно для заполнения
Отчество “patronymic”:
Буквы русского алфавита, римские цифры, дефис, апостроф
1-50 символов
Необязательное
Возможные ошибки:
Отчество может содержать только буквы русского алфавита, римские цифры, дефис и апостроф
Отчество не должно быть длиннее 50 символов
Дата рождения “birthdate”:
Дата в формате ГГГГ-ММ-ДД
Ребенок не может быть старше 18 лет
Обязательное
Возможные ошибки:
Дата рождения должна быть в формате ГГГГ-ММ-ДД
Ребенок не может быть старше 18 лет
Дата рождения обязательна для заполнения
Пол “gender”:
Символ m или f
Необязательное
Возможные ошибки:
Пол может содержать только: символ m или f
Номер свидетельства о рождении “birth_no”:
Cтрока вида Х-ЯЯ 000000, где Х - римское число, подстрока длиной от 1 до 7, содержащая любые из следующих латинских букв: I, V, X, L, C, D, M; Я - любая русская буква, 0 - любая цифра
11-17 символов
Обязательно для детей младше 14 лет
Возможные ошибки:
Неверно заполнен номер свидетельства о рождения
Номер свидетельства о рождении обязателен для заполнения
Серия и номер паспорта “pass_no”:
Только цифры
10 символов
Обязательно для детей старше 14 лет
Отсутствует у детей младше 14 лет
Возможные ошибки:
Неверно заполнены серия и номер паспорта
Серия и номер паспорта обязательны для заполнения
Серия и номер паспорта должны отсутствовать для ребенка младше 14 лет
Если ограничение не соблюдается, в ответе от системы мы должны получить ошибку для конкретного поля, например: “Пол может содержать только: символ m или f”. Если данные корректны: “Данные сохранены”.
Проблема первая. Тестовые данные не увеличивают вероятность обнаружения дефекта.
Давайте проанализируем поле «Фамилия» и ответим на вопросы: какие значения мы можем передать? Какие из них система должна пропустить, а какие нет?
Представим все возможные значения этого поля в виде множества. Так как тестируем мы API форму, то это множество практически ничем не ограничено, так что будем считать его стремящимся к бесконечности. Тестируемый сервис из всего этого множества должен пропустить строку, содержащую только буквы русского алфавита, римские цифры, дефис и апостроф, размером от 1 до 50 символов. Все возможные вариации этой строки назовем искомыми.
Наши тесты будут делиться на два типа: заведомо верные (мы передали значение, входящее в искомое подмножество) и заведомо ложные (мы передали значение, заведомо не входящее в подмножество). К заведомо ложным значениям отнесем еще и пустое множество, так как поле обязательно. Можем ли мы проверить ВСЕ заведомо верные значения? Подсчитаем:
Символьная группа:
33заглавные буквы+33строчные буквы+7римских цифр+2символа+1пустота = 76 возможных символов
Количество возможных значений:
7650+ 7649+ 7648+ ... +762+76 = i=15076i
Можете сами посчитать, если будет время ?, но что-то нам подсказывает, что у этого числа примерно 94 знака. Делаем вывод, что полностью протестировать это поле мы не можем, а это только одно множество заведомо верных значений! Но есть выход — прекратить искать дефектное значение и начать искать дефектное подмножество.
Меняя поиск дефектного значения на поиск дефектного множества, мы сокращаем время тестирования и экспоненциально увеличиваем вероятность обнаружения дефекта. С таким подходом, разбив задачу всего на 6 подмножеств, система будет протестирована намного качественнее.
Почему же это работает? Дефект появляется не по какому-то злому умыслу, а в результате ошибки в описании принимаемых значений, а это уже фактически подмножество.
Проблема вторая. Отсутствие структуры и громоздкость кода
Подсчитаем, сколько в нашем примере кейсов. Нам нужно проверить 17 ошибок и 7 полей, плюс необязательность для четырех из них. Итого — минимум 28 кейсов.
Через нас прошло много TMS, как самописных, так и уже готовых. И создается впечатление, что автотесты проекта делают для “галочки”. Не для каждого проекта выгодно использовать полюбившуюся вам систему тестирования, скорее всего однажды вы так перегрузите функциональность автотестами, что при значительных изменениях вам придется все писать заново.
Иногда на 28 кейсов тестировщики пишут 28 тестов, еще и дублируя логику каждого теста. Напомним, что пример максимально урезан, и в обычных задачах это не 28, а 84.
И это частая проблема. Наверняка вам хоть раз попадалась система-призрак, в которой описана функциональность годовой давности. Куча работы бедного тестировщика, который сидел и копипастил данные, описывая каждый тест. А в итоге, после парочки доработок автотесты забросили, и они так и крутятся где-то, выдавая ежедневный отчет с кучей “багов”.
Не так давно мы прекратили описывать тесты для форм, теперь описываем кейсы. Основа нашей тестовой системы — combidata. Эта библиотека позволяет блочно спроектировать тест по шагам, например для API формы :
Комбинация (библиотека комбинирует тестируемый кейс с другими)
Генерация (генерируются данные для каждого поля)
Экспорт (формируется тело запроса и отправляется по API)
Импорт (импортируется фактический результат)
Вывод (сравнивается ожидаемый и фактический результат и на его основе формируется вывод)
А результаты можно уже интегрировать куда угодно, хоть в pytest. В итоге мы сокращаем и структурируем код. Например, описание поля выглядит так:
child["cases"]["surname"] = {
"T": {
"gen_func": re_generate,
"value": r"[А-Яа-яёЁ\-']{1,50}",
"name": "Проверка поля surname"
},
"F": {
"gen_func": re_generate,
"value": r"[^А-Яа-яёЁ\-']{1,50}",
"type": "error",
"error": "Фамилия может содержать только: буквы русского алфавита, римские цифры, дефис и апостроф",
"name": "Проверка поля surname на наличие ошибки о некорректности символов"
},
"L": {
"gen_func": re_generate,
"value": r"[А-Яа-яёЁ\-']{51,60}",
"type": "error",
"error": "Фамилия не должна быть длиннее 50 символов",
"name": "Проверка поля surname на наличие ошибки длины"
},
"O": {
"value": None,
"type": "error",
"is_presented": False,
"error": "Фамилия обязательна для заполнения",
"name": "Проверка поля surname на наличие ошибки по отсутствию"
}}
И для того, чтобы протестировать одно поле нам теперь не нужно заботиться о других полях формы, библиотека сделает все за нас. Конечно, есть вопросы по структуре описания, но это уже гораздо удобнее, чем описывать тесты.
Проблема третья. Кейсы статичны и при изменении системы трудно редактируются
Теперь давайте подумаем, как мы будем описывать эти множества. Сразу уйдем от подхода с массивами в коде и дополнительными базами данных. Повторимся, такие тесты становятся мусором при любом изменении системы, отредактировать БД для новых значений почти также сложно, как ее и заполнить заново.
Предложим иной подход, генерация этих значений по определенным шаблонам, например по регулярными выражениями:
[А-Яа-яёЁ\-'IVXLCDM]{1,50} — Буквы русского алфавита, римские цифры, дефис и апостроф размером от 1 до 50 символов
[А-Яа-яёЁ\-'IVXLCDM]{50} — Строка длиной 50 символов (буквы русского алфавита, римские цифры, дефис и апостроф)
[А-ЯЁIVXLCDM]{40,50} — 33 заглавных буквы и 7 римских цифр
[А-Яа-яёЁ\-'IVXLCDM] — Любой символ из (буквы русского алфавита, римские цифры, дефис и апостроф)
[^А-Яа-яёЁ\-'IVXLCDM] — Любой символ кроме (буквы русского алфавита, римские цифры, дефис и апостроф)
[А-Яа-яёЁ\-'IVXLCDM]{51,60} — Строка длинной более 50 символов (буквы русского алфавита, римские цифры, дефис и апостроф)
[^А-Яа-яёЁ\-'IVXLCDM]{1,60} — Все возможные комбинации для поля
[а-яё\-']{40,50} — 33 строчные буквы, тире и дефис
Очень удобно генерировать случайные значения для таких множеств с помощью библиотек типа re-generate. Не для всего подходит такая генерация, например даты мы уже генерируем собственными функциями. Еще хороший вариант — Hypothesis.
Библиотека позволяет нам удобно генерировать данные практически любого типа, но у нее есть проблема — ее трудно освоить новичку, и она скорее заточена под UNIT тестирование, чем под интеграционное.
Проблема четвертая . Высокий порог вхождения
После всего прочитанного у вас скорее всего возникнет мнение, что мы ненавидим готовые TMS. А вот и нет, у них много плюсов:
Порог вхождения очень низок.
Идеально подходят для “простых” проектов или для smoke-регресс тестирования.
Для многих систем тест описывается без единой строки кода.
Но они недостаточно гибкие, а самописные TMS требуют квалификации у тех, кто проектирует, и у тех, кто систему поддерживает. Но комбинация библиотек Combidata, Re-generate, Hypothesis и Pytest решает эту проблему и позволяет нам создать максимально простой и гибкий скелет системы. С таким подходом можно блочно описать все этапы для тестов определенной функциональности, настроить и оставить для тестировщиков работу по описанию кейсов. А еще это позволяет нам не нагружать сотрудников с высокой квалификацией поддержкой автотестов.
Коротко о Combidata
Библиотека заполняет переданный ей шаблон данными, которые сама сгенерировала из доступных множеств. Вот так выглядит описание теста для этой библиотеки:
child = {"cases": {},
"workflow": (ST_COMBINE, ST_GENERATE, ST_FORM, EXPORT, ASK, COMPARE),
"tools": {
"UTILS": UTILS
},
"form": {"surname": "surname",
"name": "name",
"patronymic": "patronymic",
"birthdate": "birthdate",
"gender": "gender",
"birth_no": "birth_no",
"pass_no": "pass_no"
}}
В cases хранятся все наши поля и их возможные вариации (код приведен немного выше).
В workflow описан путь теста:
ST_COMBINE - комбинация
ST_GENERATE - генерация
ST_FORM - формирование по шаблону из “form”
EXPORT - отправка через “tools”,
ASK - запрос фактического результата
COMPARE - сравнение и вывод.
Выглядит очень просто, а главное масштабируемо.
Вывод
Избавившись от тестирования значений, систематизируя все по блокам, уйдя от статических значений и описания тестов в тестировании API форм, мы приобрели прозрачность в системе автоматизации, ее масштабируемость и возможность поддержания с меньшими расходами. Причем такой подход позволяет не только тестировать API, но и генерировать данные для фронтовых форм, эмулировать шаги взаимодействия пользователя. Нам очень хотелось бы узнать, а как вы тестируете свои API формы? Смогли бы внедрить такую структуру к себе, и если нет, то почему? Ведь каждая система уникальна, а нам очень важна универсальность. Будем рады обратной связи и спасибо за внимание!