В предыдущей статье я рассказал о подготовке данных для тестирования. Теперь стоит подробно разобрать, как их генерировать. Есть несколько подходов к генерации данных. У всех их них есть свои плюсы, минусы и особенности, которые стоит учитывать.
Сверху это выглядит как супер-параметризированная структура, где я говорю, что мне нужно по таким-то шаблонам сгенерировать столько-то тысяч записей и запускаю цикл.
Функции-обертки с параметрами генерации данных для сценариев, например, функция load_fill_database_change_password для сценария change_password
Эти количества и шаблоны, конечно, беру из методики, которую заранее подготовили. Параметры передаются в функцию, которая уже на основе этих шаблонов готовит данные для конкретного сценария. В ней цикл от 0 до 10000, чтобы делать INSERT INTO, INSERT INTO и различные вычисления.
Параметризируемая функция генерации данных с циклами генерации данных
Но такой подход реализуем:
Цикл генерации значений, где каждое значение зависит от счетчика цикла
Мы бежим в цикле и на основе индекса вычисляем, например, имя и фамилию пользователя 1. У него будет индекс №1. Для индекса №2 используются аналогичные вычисления. Конечно, данные получатся не такими случайными, как в реальной жизни, но они будут корректными.
Я знаю такие подходы:
timeDiffMinutes = i % 80000
nick_name = format( 'user%sname', i::text )
Пример результата:
user123name
Для 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('users' || test_name || lpad(i::text, 12, '0') )::uuid
Пример результата:
faf154aa-54d1-0b07-3c8e-fe8921f96c45
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;
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;
Я рассказал про подходы, но я бы не был нагрузочником, если бы не сказал, как эти подходы ускорить.
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.
Его использовал, чтобы генерировать большое количество псевдоперсональных данных для тестирования зарплатных проектов. Вот пример алгоритма генерации сотрудниц.
В Elizabeth создается случайная персона и для нее можно получить фамилию и имя
В цикле формируется 600000 строк, и чтобы Mimesis сгенерировал русские имена и фамилии, мы в строке 16 задаем персону для русского языка. В классе person у нас появляются все псевдослучайные персональные данные. Если мы возьмем, например, поле surname или name и вставим их в наши поля Pandas, то получим женскую фамилию и женское имя. Несложным алгоритмом можно получить отчество.
Подбираем отца перебором, пока имя отца не будет заканчиваться на к, н, г, з, ф, в, п, р, л, д, ч, с, м, т или б. Для Павла и Льва - особые условия формирования отчества
Для этого нужно сгенерировать уже мужское имя и посмотреть на какую букву алфавита оно заканчивается. Если на простую букву, например, имя Кирилл заканчивается на букву «Л», то чтобы сгенерировать женское отчество от имени Кирилл, нужно добавить «овна» — Кирилловна. Есть несколько исключений, для которых надо добавлять дополнительные правила. Например, имя Павел тоже заканчивается на простую букву «Л», но Павеловна — некорректное отчество.
Аналогично генерируются мужские отчества — Иван - «ович».
На Python можно генерировать другие данные, выполнять транслитерацию и генерировать номера.
Генерация имени для пластиковой карты из 23 символов
Ещё Pandas предоставляет удобный интерфейс для того, чтобы удалить дубликаты в одну строку.
df = pd.DataFrame(rowList)
df = pd.drop_duplicates(keep="first")
Или для того, чтобы массово обновить все поля всех строк, например, сказать, что в этом датасете все родились 25 ноября:
Массовое добавление полей
Я использовал Pandas для того, чтобы не разбираться с форматами Excel, а сохранить готовые данные в CSV или XLS файлы.
Выгрузка в csv
Хороший путь — это пойти к разработчикам или скачать исходники, взять Data Transfer Object, которые уже написаны, версионируются и поддерживаются вместе с вашим сервисом, где есть специальные методы, чтобы только имя заполнить, а все остальное сгенерировать, проверить и рассчитать.
И самое главное — Data Transfer Object в одну строку превращаются в XML-файл или JSON-файл. Вам не нужно для этого делать конкатенацию. Возможно, это будет чуть дольше, но вы сэкономите много времени на отладке.
Генерация данных с PostgreSQL
Чаще всего я работаю с Postgres. Я пришел к структуре генератора тестовых данных, которой хочу с вами поделиться. Она представляет из себя набор хранимых процедур, где есть корневая хранимая процедура, в ней есть параметры. Она вызывает служебные хранимые процедуры, которые вызывают служебные хранимые процедуры для сценариев, которые вызывают уже супер служебные процедуры, которые выполняют атомарные действия: получи пароль, получи имя, заполни какую-то запись.Сверху это выглядит как супер-параметризированная структура, где я говорю, что мне нужно по таким-то шаблонам сгенерировать столько-то тысяч записей и запускаю цикл.
Эти количества и шаблоны, конечно, беру из методики, которую заранее подготовили. Параметры передаются в функцию, которая уже на основе этих шаблонов готовит данные для конкретного сценария. В ней цикл от 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'
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:Его использовал, чтобы генерировать большое количество псевдоперсональных данных для тестирования зарплатных проектов. Вот пример алгоритма генерации сотрудниц.
В цикле формируется 600000 строк, и чтобы Mimesis сгенерировал русские имена и фамилии, мы в строке 16 задаем персону для русского языка. В классе person у нас появляются все псевдослучайные персональные данные. Если мы возьмем, например, поле surname или name и вставим их в наши поля Pandas, то получим женскую фамилию и женское имя. Несложным алгоритмом можно получить отчество.
Для этого нужно сгенерировать уже мужское имя и посмотреть на какую букву алфавита оно заканчивается. Если на простую букву, например, имя Кирилл заканчивается на букву «Л», то чтобы сгенерировать женское отчество от имени Кирилл, нужно добавить «овна» — Кирилловна. Есть несколько исключений, для которых надо добавлять дополнительные правила. Например, имя Павел тоже заканчивается на простую букву «Л», но Павеловна — некорректное отчество.
Аналогично генерируются мужские отчества — Иван - «ович».
На Python можно генерировать другие данные, выполнять транслитерацию и генерировать номера.
Ещё Pandas предоставляет удобный интерфейс для того, чтобы удалить дубликаты в одну строку.
df = pd.DataFrame(rowList)
df = pd.drop_duplicates(keep="first")
Или для того, чтобы массово обновить все поля всех строк, например, сказать, что в этом датасете все родились 25 ноября:
Я использовал Pandas для того, чтобы не разбираться с форматами Excel, а сохранить готовые данные в CSV или XLS файлы.
Генерация данных с сериализацией классов
Если вы генерируете данные со стороны API, и эти данные представляют структуры данных сервиса, то переиспользуйте их. Например, вам нужно отправить много JSON файлов в вашу систему. Часто тестировщики соединяют строки и вставляют параметры, чтобы в результате получился валидный JSON. Это плохой путь.Хороший путь — это пойти к разработчикам или скачать исходники, взять Data Transfer Object, которые уже написаны, версионируются и поддерживаются вместе с вашим сервисом, где есть специальные методы, чтобы только имя заполнить, а все остальное сгенерировать, проверить и рассчитать.
И самое главное — Data Transfer Object в одну строку превращаются в XML-файл или JSON-файл. Вам не нужно для этого делать конкатенацию. Возможно, это будет чуть дольше, но вы сэкономите много времени на отладке.
Промежуточные итоги
- генерация данных на SQL - наиболее производительный способ генерации, который работает максимально быстро, если предварительно настроить базу данных на быструю вставку
- генерацию данных в формате Excel/CSV удобно делать с Pandas, а для псевдоперсональных данных удобно использовать Mimesis из python
- для формирования XML/JSON, при генерации данных через API, сериализация объектов системы дает более стабильный результат, чем формирование XML/JSON через конкатенацию строк
Атака не клонов, или Генерация и анализ тестовых данных для нагрузки. Часть 2
В предыдущей статье я рассказал о подготовке данных для тестирования, что данные лучше генерировать, а не клонировать. Теперь стоит подробно разобрать, как их генерировать. Есть несколько подходов к...
habr.com