Привет, Хабр!

Если ваш проект вырос, в нем бэкенд с фронтендом, различные точки входа API, интеграции с внешними системами, сложные алгоритмы, проверки введенных данных пользователем на валидность, диаграммы бизнес-процессов имеют тысячи ветвей, то скорее всего регрессионное тестирование занимает кучу времени, и проводить его вручную уже невыгодно. Проще эту работу поручить машине и тестировать продукт автоматически.  Первый вопрос, который возникает — «Как генерировать данные?», а конкретнее, как генерировать то, что может ввести пользователь. Этот вопрос мы и разберем в данной статье.

Обычно введенные строки сравнивают с некими шаблонами, среди которых наиболее распространенный– RegExr (регулярное выражение). Самый логичный способ — тестировать шаблон шаблоном.

Что такое и из чего состоит RegExr:

RegExr — шаблон, в котором представлена совокупность правил для конкретных позиций в строке.

Для единичных символов понятно, на этой позиции может быть только он. А в квадратных скобках указаны возможные варианты символов, например [1-9#] на этой позиции возможны только символы — 123456789#, а для правила [^@\.]  возможны любые символы кроме @ и . (напомню, что символ ^ после открывающей скобки означает «кроме»)

Квантификатор даёт нам информацию о том, на сколько позиций распространяется символ или символьная группа перед ним.

Квантификатор

Использование

{n}

Ровно n повторений

{m,n}

От m до n повторений

{m,}

Не менее m повторений

{,n}

Не более n повторений

?

Ноль или одно повторение

То же, что и {0,1}

*

Ноль или более повторений

То же, что и {0,}

+

Одно или более повторений

То же, что и {1,}

 

Все как в алгебре, скобки группируют и в основном работают с квантификаторами. В примере выражение в скобках может повторяться один и более раз:

Логика “или” позволяет сделать наше выражение более эластичным. В примере строка может состоять либо из 6 цифр, либо из 10. Выражений разделенных элементом | может быть  сколько угодно, но на этом месте может быть только одно из них.

Проблематика:

Например, поле email не принимает случайный набор символов, естественно там присутствует проверка через регулярное выражение, и в самом простом виде это [^@]+@([^@\.]+\.)+[^@\.]+. Соответственно, нам нужно либо захардкодить значение на каждое поле, либо при каждом тесте генерировать строку, которая подойдет под это выражение.
Чем обернется хардкод: 

1. Вы не сможете поддерживать свои тесты, так как ваш продукт меняется, и искать часами, где именно прошлый разработчик указал данные для этого поля долго и невыгодно.

2. Возникает риск ошибки, ведь по основному принципу тестирования мы тестируем черный ящик и не знаем какое там регулярное выражение, так как программист мог спокойно указать [^@]+@[^@\.]+\.[^@\.]+, и misha@mail.ru пройдет, а diana@inform.bigdata.ru нет.

3. Банально под ваши тесты могут подогнать регулярное выражение :)

Соответственно, правильно будет каждый раз генерировать новую случайную строку. Рекомендую писать свое регулярное выражение для генерации, основываясь на ТЗ, а не таскать их из кода.  Для того, чтобы сгенерировать случайную строку, нужно решить две основные проблемы:

1. Какие рамки поставить для квантификаторов +, {m,}, *. Ведь бесконечное количество символов мы сгенерировать не можем, а строка из тысячи символов только нагрузит интерфейс и особо никак не повлияет на тестирование. Тут нужно найти баланс и случайно не написать стресс-тест.

2. Откуда брать символы для генерации такого выражения — [^@\.]? Из таблицы ASCI? Но там довольно много специальных символов, и в итоге мы получим непонятную кашу.

Ответив на эти вопросы, мы сможем реализовать генерацию. Первый вопрос решается выбором верхней планки длины строки или квантификатора, например 50 символов. А второй — собственной библиотекой символов для проекта.

Задача:

Для генерации строки из выражения нужно разложить его на такие части, которые будет принимать конечный  автомат. То есть нужно привести к такому виду, что некая функция будет принимать его части на вход (множество символов B), выбирать такой символ a, который принадлежит этому множеству и выдавать этот символ.

Метод генерации строки из регулярного выражения

Приведу свою блок-схему алгоритма генерации включающую в себя 5 основных этапов:

1 Этап — декомпозиция основного выражения на подгруппы:

2 Этап — выбор между возможными вариантами:

Для оптимизации времени выполнения алгоритма, этот этап выполняется сразу после декомпозиции и позволяет не тратить времени на генерацию ненужных символов.

3 Этап — обработка символьных групп:

На этапе происходит замена шаблонов и формирование множеств для конечных автоматов.

4 Этап — обработка квантификаторов:

На этом этапе нам требуется выбрать конкретные значения для квантификаторов. Тут мы сталкиваемся с проблемой описанной ранее, квантификаторы +, *, {n,} представляют диапазоны чисел от n до бесконечности, с такими диапазонами мы работать не можем, поэтому требуется ограничение, например от n до n + 50. Потом для диапазонов нам нужно выбрать конкретное значение, оно и будет означать количество запусков конечных автоматов.  Далее мы «разворачиваем» выражения в круглых скобках в зависимости от значения их квантификатора.

Отмечу, что мы должны рекурсивно запускать алгоритм для каждой группы столько раз, сколько указано в её квантификаторе.

Приведу пример работы квантификатора, который должен запускать именно то количество конечных автоматов, которое указано для него, а не «умножать» результат работы.

5 Этап — Запуск конечных автоматов.

После всех этих этапов мы получаем общность конечных автоматов, которые и требуется запустить:

Зачем изобретать велосипед?

Увы, но нет универсальных решений, и под свои задачи нужно либо модифицировать, либо писать что-то самостоятельно. Например, на нашем проекте для автоматического тестирования API был выбран язык Python. Он простой, удобный, и обучение специалиста, который сможет поддерживать фреймворк — дело буквально месяца-двух.  У нас несколько требований к библиотеке генерации случайных строк:

  1. Быстрая;

  2. Удобная;

  3. Должна поддерживать основную лексику регулярных выражений;

  4. В начале и конце сгенерированной строки не должно быть пробелов.

В своих поисках мы нашли 4 библиотеки: xeger, strgen, exrex, rstr.

Strgen – самая быстрая, практически идеальна, но не поддерживает многие квантификаторы.

Xeger — реализована не самая удобная работа с библиотеками символов и первоначальными настройками.

Rstr — Не самая стабильная библиотека, в некоторых моментах работает в 5 раз медленнее других.

Exrex — генерирует 1000 случайных строк в среднем за 2 секунды, не критично, но можно лучше.

Хотелось бы получить универсальную, быструю библиотеку, подходящую под наши требования. Вот так и появилась на свет библиотека re_generate (pypi.org/project/re-generate). Получилось даже лучше, чем мы думали, уже до оптимизации алгоритм выдает хорошие показатели в сравнительных бенчмарках:

Итог:

Если вы не нашли уже готового решения для вашего проекта, рекомендуем реализовать этот алгоритм, таким образом вы получите практику и прекрасный инструмент для работы. Забирайте в свои проекты, пользуйтесь, реализуйте. Спасибо за внимание!

 

Комментарии (7)


  1. nin-jin
    16.01.2023 13:23

    Так и не понял зачем генерировать случайную строку, которая гарантированно проходит проверку по регулярке. Это вы так тестовые данные готовите, чтобы тесты никогда не падали?


    1. PPR Автор
      18.01.2023 13:54

      RegExr очень прост для описания того, что должно быть в строке и очень удобен для кода. Разработчик и тестировщик получают техническую документацию на функционал, где дано не регулярное выражение, а описание требования к полю.  После этого разработчик описывает эти требования регуляркой, а тестировщик описывает возможные кейсы своими RegExr. Реальный пример:
      Требования к одному из полей — Только русские буквы и любое число в конце

      Разработчик повесил на поле паттерн ^[А-Яа-яёЁ]+\d$

      Тестировщик описал положительный кейс: ^[А-Яа-яёЁ]+([1-9]\d*|0)$  (Так как там должно быть именно число в конце. Числа не начинаются с 0, кроме 0)

      и отрицательные кейсы:

      ^[^А-Яа-яёЁ][1-9]\d*$ (проверит можно ли ввести что-то кроме русских букв)

      ^[А-Яа-яёЁ]+0[1-9]+$ (проверит пропустит ли набор цифр)

      ^[А-Яа-яёЁ]+[^0-9]$ (проверит можно ли что-то передать кроме числа)

      В итоге после тестирования стало понятно, что задача будет отправлена на доработку. 


      1. nin-jin
        18.01.2023 14:40
        -1

        Если у вас даже тестировщик пишет регулярки ввиду того, что ими крайне просто описывать , то зачем их вообще тестировать? Или вы сам движок регекспов тестируете?


  1. estet
    16.01.2023 13:41
    +1

    Почему не использование библиотеки Hypothesis со стратегией from_regex?


    1. PPR Автор
      18.01.2023 13:54

      Очень крутая библиотека, влюблены в нее, но она увы про другое и конкретно нам не подходит, так как используем свой фреймворк для запуска тестов. Стратегии генерируют не просто данные, а удобную композицию для тестовой функции, которую можно прогнать через PyTest. Можно конечно использовать стратегию как генератор, но это будет дольше и усложнит код.


  1. SH33011
    18.01.2023 13:55

    (напомню, что символ ^ после открывающей скобки означает «кроме»)

    Разве этот символ не означает что "строка начинается с " символа или последовательности после ^


    1. PPR Автор
      18.01.2023 13:56

      Верно, но только в начале регулярного выражения, а после "[" любой символ "кроме".