Генерация тестовых данных с SQL, Python и сериализацией, или Генерация и анализ тестовых данных для нагрузки. Часть 2

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

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

Генерация данных с PostgreSQL

Чаще всего я работаю с Postgres. Я пришел к структуре генератора тестовых данных, которой хочу с вами поделиться. Она представляет из себя набор хранимых процедур, где есть корневая хранимая процедура, в ней есть параметры. Она вызывает служебные хранимые процедуры, которые вызывают служебные хранимые процедуры для сценариев, которые вызывают уже супер служебные процедуры, которые выполняют атомарные действия: получи пароль, получи имя, заполни какую-то запись.

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

Функции-обертки с параметрами генерации данных для сценариев, например, функция load_fill_database_change_password для сценария change_password
Функции-обертки с параметрами генерации данных для сценариев, например, функция load_fill_database_change_password для сценария change_password

Эти количества и шаблоны, конечно, беру из методики, которую заранее подготовили. Параметры передаются в функцию, которая уже на основе этих шаблонов готовит данные для конкретного сценария. В ней цикл от 0 до 10000, чтобы делать INSERT INTO, INSERT INTO и различные вычисления. 

Параметризируемая функция генерации данных с циклами генерации данных
Параметризируемая функция генерации данных с циклами генерации данных

Подходы для генерации значений

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

Но такой подход реализуем:

Цикл генерации значений, где каждое значение зависит от счетчика цикла
Цикл генерации значений, где каждое значение зависит от счетчика цикла

Мы бежим в цикле и на основе индекса вычисляем, например, имя и фамилию пользователя 1. У него будет индекс №1. Для индекса №2 используются аналогичные вычисления. Конечно, данные получатся не такими случайными, как в реальной жизни, но они будут корректными. 

Я знаю такие подходы:

Число из интервала

Взять остаток от деления на N: это даст число в интервале от 0 до N-1.

timeDiffMinutes = i % 80000

Логин пользователя

Или использовать функцию, которая в SQL для Postgres называется format. Там есть шаблонные строки и параметры.

nick_name = format( 'user%sname', i::text )

Пример результата:

user123name

IP-адрес

Можно объединить, например, format и остаток от деления для того, чтобы вычислять IP-адреса в журнал входов в систему или таблицу аудита.

Для IPv4

format(  '106.%s.%s.%s', 
         (((ip_index/250)/250)%250)::text,  --- 0..249
         ((ip_index/250)%250)::text,        --- 0..249
         (1+ip_index%250)::text             --- 1..250
      )

Пример результата:

106.0.1.2

А всего 15 625 000 значений

GUID через md5

Часто идентификаторами или ключами в таблицах являются GUID’ы. Я придумал способ, чтобы вычислить GUID для теста. Например, если нужен GUID в таблицу users для пользователя №175000 для сценария login, то можно взять

  • число 175000, превратить его в строку фиксированной длины через lpad

  • конкатенировать с именем таблицы 'users'

  • и конкатенировать с названием теста 'login'

Взять от результата md5 хэш и привести его к типу uuid. Так мы получим уникальный GUID:

guid = md5('users' || test_name || lpad(i::text, 12, '0') )::uuid

Пример результата:

faf154aa-54d1-0b07-3c8e-fe8921f96c45

GUID через конкатенацию строк

Я знаю, что на специально подготовленных данных в md5 бывают проблемы — коллизии. Но при использовании имени таблицы, имени теста и номера строки в тесте, у меня за всю практику ни разу коллизий не было. Поэтому я рекомендую вам md5 для генерации предсказуемых, но при этом уникальных GUID. Если сомневаетесь, можете сформировать md5 с префиксами: идентификатор теста замените на другую подстроку и используйте номер вашей записи. Получится GUID для записи 1, например, префикс + 000000000001:

guid = ('22-33-44-55' || 'aa-bb-cc-dd-ee-ff' || lpad(i::text, 12, '0'))::uuid

Пример результата:

22334455-aabb-ccdd-eeff-000000000001

Выбор констант из массива

Можно выбирать данные из массивов:

array[ 1 + mod( abs("userIndex"), arrayLenght ) ]

Предел на размер массива данных - 1 ГБайт, тестировал на массиве в 10 МБайт. Использую 64 КБайт (примерный размер сценария ниже - 64 КБайт):

-- Function: public.load_get_name(integer)
-- DROP FUNCTION public.load_get_name(integer);

CREATE OR REPLACE FUNCTION public.load_get_name("userIndex" integer DEFAULT 0)
RETURNS text AS 
$BODY$
    DECLARE
        namearray text[] := array[
            'Авдей',
            'Авдий',
            'Авенир',
            'Аверкий',
            'Авксентий',
            'Агафон',
            --- '…',
            'Фёдор',
            'Харитон',
            'Христофор',
            'Эдуард',
            'Эраст',
            'Юлиан',
            'Юлий',
            'Юрий',
            'Юстин',
            'Яков',
            'Якун',
            'Ярослав'
        ];
        nameArrayLenght integer := array_lenght(nameArray, 1);
    BEGIN
        RETURN nameArray[ 1 + mod(abs("userIndex"), nameArrayLenght) ];
    END
$BODY$
LANGUAGE plpgsql VOLATILE
COST 100;

ALTER FUNCTION public.load_get_name(integer) OWNER TO postgres;

Выбор констант из справочной таблицы

Или выбирать данные из целых таблиц с помощью SELECT, которые вы загрузили из CSV файла.

CREATE FUNCTION public.load_get_password("userLogin")
    RETURNS text AS
$BODY$
    DECLARE
        password varchar(255);
    BEGIN
        password = (select password_hash
            from test_passwords
            where user_name="userLogin"
        limit 1);
        RETURN password;
    END
$BODY$
LANGUAGE plpgsql VOLATILE;

Я рассказал про подходы, но я бы не был нагрузочником, если бы не сказал, как эти подходы ускорить.

Ускорение вставки значений

Чтобы ускорить вставку значений не только за счет использования хранимых процедур, а еще за счет оптимального процесса, я рекомендую до генерирования данных сделать еще 4 предварительных шага:

1) Сохранить схему БД в виде файла — вдруг что-то поломается;

2) Отключить индексы запросом для Postgres:

UPDATE pg_index
SET indisready=false
WHERE indexrelid = (
      SELECT oid
      FROM pg_class
      WHERE
      relname in (
          'таблица1'
         ,'таблица2'
      )
);

Здесь мы говорим, что нужные индексы у нас теперь (indisready=false) отключены.

3) Отключить триггеры, если они есть:

ALTER TABLE public.таблица1
DISABLE TRIGGER ALL;

ALTER TABLE public.таблица2
DISABLE TRIGGER ALL;

4) Удалить ключи:

ALTER TABLE public.таблица1
DROP CONSTRAINT таблица1_pkey;

ALTER TABLE public.таблица2
DROP CONSTRAINT таблица2_pkey;

Ключи являются индексами, но отключить их нельзя. Поэтому если вы хотите ускорить тестирование и отказаться от ключей, их можно только удалить. Для этого мы и сохраняли схему БД на первом шаге, чтобы потом проверить, что у нас все хорошо.

5) Сгенерировать данные «хранимками»:

SELECT load_fill_database_test1(1,       1000000);
SELECT load_fill_database_test2(1000001, 2000000);

Далее нужно выполнить шаги обратные шагам 4, 3, 2.

5) Создать ключи (ADD CONSTRAINT):

ALTER TABLE public.таблица1
ADD CONSTRAINT PRIMARY KEY(id);

ALTER TABLE public.таблица2
ADD CONSTRAINT PRIMARY KEY(id);

6) Включить триггеры, если мы их отключали:

ALTER TABLE public.таблица1
ENABLE TRIGGER ALL;

ALTER TABLE public.таблица2
ENABLE TRIGGER ALL;

7) Включить индексы:

UPDATE pg_index 
SET indisready=true
WHERE indexrelid=(
    SELECT oid
    FROM pg_class
    WHERE
    relname in
        ('таблица1',
         'таблица2')
);

Если отключить индексы, а потом вставить данные и включить индексы, индексы автоматически не обновятся. Нужно выполнить переиндексацию.

8) Переиндексировать:

REINDEX public.таблица1;

REINDEX public.таблица2;

Теперь Postgres будет знать о всех новых записях, индексы перестроятся и можно сравнить сохраненную схему, которая была на шаге 1, и схему, которая получилась после того, как мы выполнили все шаги.

9) Сравнить схему до и после.

Но данные можно генерировать не только с Postgres. Иногда нужно генерировать сложные данные, например, списки сотрудников, которые получают зарплату, в виде документа Excel. Тогда мне на помощь приходит Python и проект под названием Pandas.

Генерация данных с Python

Я работал с Pandas и проектом, который раньше назывался Elizabeth, а сейчас Mimesis:

  • Генерация фиктивных данных с Mimesis: Часть I

  • Генерация фиктивных данных с Mimesis: Часть II

Его использовал, чтобы генерировать большое количество псевдоперсональных данных для тестирования зарплатных проектов. Вот пример алгоритма генерации сотрудниц.

В Elizabeth создается случайная персона и для нее можно получить фамилию и имя
В Elizabeth создается случайная персона и для нее можно получить фамилию и имя

В цикле формируется 600000 строк, и чтобы Mimesis сгенерировал русские имена и фамилии, мы в строке 16 задаем персону для русского языка. В классе person у нас появляются все псевдослучайные персональные данные. Если мы возьмем, например, поле surname или name и вставим их в наши поля Pandas, то получим женскую фамилию и женское имя. Несложным алгоритмом можно получить отчество.

Подбираем отца перебором, пока имя отца не будет заканчиваться на к, н, г, з, ф, в, п, р, л, д, ч, с, м, т или б. Для Павла и Льва - особые условия формирования отчества
Подбираем отца перебором, пока имя отца не будет заканчиваться на к, н, г, з, ф, в, п, р, л, д, ч, с, м, т или б. Для Павла и Льва - особые условия формирования отчества

Для этого нужно сгенерировать уже мужское имя и посмотреть на какую букву алфавита оно заканчивается. Если на простую букву, например, имя Кирилл заканчивается на букву «Л», то чтобы сгенерировать женское отчество от имени Кирилл, нужно добавить «овна» — Кирилловна. Есть несколько исключений, для которых надо добавлять дополнительные правила. Например, имя Павел тоже заканчивается на простую букву «Л», но Павеловна — некорректное отчество.

Аналогично генерируются мужские отчества — Иван - «ович».

На Python можно генерировать другие данные, выполнять транслитерацию и генерировать номера. 

Генерация имени для пластиковой карты из 23 символов
Генерация имени для пластиковой карты из 23 символов

Ещё Pandas предоставляет удобный интерфейс для того, чтобы удалить дубликаты в одну строку.

df = pd.DataFrame(rowList)
df = pd.drop_duplicates(keep="first")

Или для того, чтобы массово обновить все поля всех строк, например, сказать, что в этом датасете все родились 25 ноября:

Массовое добавление полей
Массовое добавление полей

Я использовал Pandas для того, чтобы не разбираться с форматами Excel, а сохранить готовые данные в CSV или XLS файлы.

Выгрузка в csv
Выгрузка в csv

Генерация данных с сериализацией классов

Если вы генерируете данные со стороны API, и эти данные представляют структуры данных сервиса, то переиспользуйте их. Например, вам нужно отправить много JSON файлов в вашу систему. Часто тестировщики соединяют строки и вставляют параметры, чтобы в результате получился валидный JSON. Это плохой путь.

И самое главное — Data Transfer Object в одну строку превращаются в XML-файл или JSON-файл. Вам не нужно для этого делать конкатенацию. Возможно, это будет чуть дольше, но вы сэкономите много времени на отладке.

Промежуточные итоги

  • генерация данных на SQL - наиболее производительный способ генерации, который работает максимально быстро, если предварительно настроить базу данных на быструю вставку

  • генерацию данных в формате Excel/CSV удобно делать с Pandas, а для псевдоперсональных данных удобно использовать Mimesis из python

  • для формирования XML/JSON, при генерации данных через API, сериализация объектов системы дает более стабильный результат, чем формирование XML/JSON через конкатенацию строк

В следующей части я расскажу об анализе данных и логировании запросов PostgreSQL.

Хороший путь — это пойти к разработчикам или скачать исходники, взять Data Transfer Object, которые уже написаны, версионируются и поддерживаются вместе с вашим сервисом, где есть специальные методы, чтобы только имя заполнить, а все остальное сгенерировать, проверить и рассчитать.

Конференция об автоматизации тестирования TestDriven Conf 2022 пройдёт в Москве, 28-29 апреля 2022 года. Если вы хотите выступить на конференции, то до окончания приема заявок осталось 8 дней.

Кроме хардкора об автоматизации и разработки в тестировании, будут и вещи, полезные в обычной работе. Принятые доклады можно посмотреть здесь, а купить билет — здесь.

Источник: https://habr.com/ru/company/oleg-bunin/blog/589543/


Интересные статьи

Интересные статьи

Всем привет! Благодарен всем за замечания и комментарии к предыдущей статье. Благодаря всем нам мы наполняем интернет доступными знаниями и это действительно круто.Сегодня продолжаем разбираться с шей...
В предыдущей статье мы познакомились с проектом Meshtastic. Узнали, что можно построить собственный радиочат на основе Mesh-сети, обычного смартфона и радиомодема. Следую...
Здравствуйте. Меня зовут Андрей, я frontend-разработчик и я хочу поговорить с вами на такую тему как нейросети. Дело в том, что ML технологии все глубже проникают в нашу ...
Вы наверняка слышали про игровую приставку Xbox 360, и что она «прошивается». Под «прошивкой» здесь имеется в виду обход встроенных механизмов защиты для запуска копий игр и самописного софта. И ...
Привет всем! Сегодня я начну серию статей-лекций посвященных теме проектирования беспилотных летательных аппаратов космического назначения (ракет) =). Да-да, — вы не ослышались, самых настоящих р...