- PVSM.RU - https://www.pvsm.ru -
Сейчас появляется очень много материала про юнит и нагрузочное тестирования. Все поголовно пишут тесты, код создают исключительно через TDD, используют jmeter/ab. Однако, все тестирование очень тесно связано с тестовыми данными. А их нужно генерировать/писать. Проблема не стоит остро для юнит тестирования — накидал mock, погонял его и забыл. Но как быть с нагрузочным тестированием? Когда мне нужно не 1-2-5-10 объектов, а миллионы?
Большинство (php) разработчиков, которых я встречал, сталкиваясь с задачей нагрузочного тестирования своего кода, создают несколько фикстур руками и насилуют их (ab/jmeter). Полученный результат тестирования не является достоверным, но они об этом не думают. Более продвинутые пишут скрипты для генерации данных, закидывают в БД и после этого уже играются. Похвально, но таких значительно меньше, а сам способ мне не кажется идеальным — другой программист может не разобраться в говнокоде генерилки фикстур (ведь создатель писал это быстро и для утилитарных целей) и рано или поздно все либо пойдут по первому пути, либо начнут писать новую генерилку.
Ценность правильного составления фикстур сейчас недооценена, многие просто на это забивают из-за трудоемкости такой работы (представим 15-25 связанных таблиц, писать скрипт генерации фикстур будет весьма, кхм, интересно). Я прекрасно понимаю почему разработчики так поступают, и, когда появилась такая же задача, то решил не биться головой об стену, а поискать инструментарий для нормальной генерации связанных данных.
Я был очень удивлен, но ничего вразумительного не было найдено, сложилось ощущение, что никого этот вопрос просто не интересует и мне всю жизнь придется писать кривые скрипты с кучей циклов. Тем не менее, подходящий инструмент был найден, мы успешно опробовали его в работе, и теперь я хочу представить его вам.
Бенератор (да, смешное название) служит для 2х целей: генерация данных и их анонимизация. Последнее выходит за рамки этой статьи, но тоже очень правильное и полезное дело (модификация дампа бд с продакшена, с целью порезать пользовательские личные данные и номера их кредиток).
Тулза использует написанный вами XML сценарий для генерации CSV/XML или экспорта прямо в базу. Работает вообщем-то везде и поддерживает следующие БД:
Сценарий представляет собою череду тэгов generate, в которых вы описываете какие сущности вы будете создавать и каким способом. Звучит просто, но есть ньюансы.
Это обзорная статья, я не буду пытаться описать все возможности этого инструмента, не собираюсь переводить всю многостраничную документацию, моя задача — показать способ решения классических кейсов и, тем самым, заинтересовать тех кому это надо.
Сейчас я попробую пошагово описать процесс от скачивания до получения данных в постгресе. Предполагается, что у вас уже установлен постгрес :)
Распаковываем архив продукта [1] и добавляем в конец ~/.bash_profile:
export BENERATOR_HOME=/path/to/unpacked/benerator
export PATH=$PATH:$BENERATOR_HOME/bin
Выполняем:
chmod a+x $BENERATOR_HOME/bin/*.sh
Сперва нам необходимо познакомиться с базовыми конструкциями сценария, то, как он строится и из чего состоит. Давайте сгенерируем 5 пользователей с несколькими полями и отдадим их в консоль.
Создаем произвольную папку, сохраняем в нее benerator.xml со следующим содержимым:
<?xml version="1.0" encoding="UTF-8"?>
<setup xmlns="http://databene.org/benerator/0.7.6"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://databene.org/benerator/0.7.6 benerator-0.7.6.xsd"
defaultEncoding="UTF-8"
defaultDataset="US"
defaultLocale="us"
defaultLineSeparator="n">
<bean id="dtGen" class="DateTimeGenerator">
<property name='minDate' value='2013-01-01'/>
<property name='maxDate' value='2013-01-31'/>
<property name='dateGranularity' value='00-00-02' />
<property name='dateDistribution' value='random' />
<property name='minTime' value='08:00:00' />
<property name='maxTime' value='17:00:00' />
<property name='timeGranularity' value='00:00:01' />
<property name='timeDistribution' value='random' />
</bean>
<import domains="person"/>
<generate type="user" count="5" consumer="ConsoleExporter">
<variable name="person" generator="PersonGenerator"/>
<attribute name="first_name" script="person.givenName"/>
<attribute name="last_name" script="person.familyName"/>
<attribute name="birthdate" script="person.birthDate"/>
<attribute name="email" script="person.email"/>
<attribute name="gender" script="person.gender" map="'MALE'->'true','FEMALE'->'false'"/>
<attribute name="created_at" type="timestamp" generator="dtGen"/>
</generate>
</setup>
Запустив в этой папке benerator.sh ./benerator.xml вы должны увидеть выдачу полученных объектов в консоль. А теперь давайте внимательно изучим benerator.xml и разберемся как это произошло.
<setup> — аттрибуты тэга несут настройки локали и т.д. Не интересно.<bean> — создает генератор класса DateTimeGenerator и идентификатором dtGen. По этому идентификатору мы можем далее использовать созданный генератор. Обилие вложенных тэгов property выставляют настройки генератора, названия их вполне говорящие.<import> — подгружает один из встроенных генераторов. Я не могу объяснить, почему PersonGenerator нужно подгружать через import, а DateTimeGenerator не требует ничего.<generate> — создает цикл объектов user с выдачей через потребителя ConsoleExporter (потребителем можно указать подключение к БД, или CSVExporter). Цикл содержит 5 итераций.<variable> — создает переменную равную экземплятру генератора, все просто. Ньюанс в том, что аттрибуты тэга могут выступать в качестве конфигурации генератора. Обратите внимание, что область видимости этой переменной ограничена нашим циклом generate. Переменную нельзя объявить вне цикла.<attribute> — заполнение свойств объекта. Как видите, в аттрибуте script используется переменная person, вызываются различные свойства специфичные для PersonGenerator. Названия этих свойств, равно как и сами классы описаны в документации. В одном из случаев используется еще один аттрибут map. Т.к. я решил хранить пол пользователя в булине, мне нужно «объяснить» бенератору этот момент, чтобы не получилось ситуации, что я пытаюсь в булинь запихнуть строку.
То что вы увидили выше, это все конечно замечательно и красиво, но давайте задумаем себе дополнительные приключения. Скажем, у нас, помимо users должна быть таблица tags, а так же user_refs_tag. Соответственно, между сущностями users и tags будет связь n к n.
Нам необходимо связать каждого пользователя с произвольным (но управляемым!) количеством тэгов. Сами тэги подготовили нам менеджеры и прислали csv, нам нужно заполнить таблицу из этого файла.
Сперва, созданим наши таблички в БД:
create table users (id serial primary key, first_name varchar not null, last_name varchar not null, birthdate date not null, email varchar not null, gender boolean not null, created_at timestamp not null);
create table tags (id serial primary key, name varchar not null, weight numeric not null, active boolean not null);
create table user_refs_tag (user_id integer not null references users (id), tag_id integer not null references tags (id), primary key (user_id, tag_id));
Подготовим tags.ent.csv, который, якобы прислали нам менеджеры:
name,weight,active
Tag 1,1.0,true
Tag 2,1.05,true
Tag 3,0.95,true
Tag 4,1.0,true
Tag 5,1.06,true
Tag 6,1.04,true
Tag 7,1.05,true
Tag 8,1.1,true
Tag 9,1.01,true
Tag 10,0.8,true
Обновляем benerator.xml:
<?xml version="1.0" encoding="UTF-8"?>
<setup xmlns="http://databene.org/benerator/0.7.6"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://databene.org/benerator/0.7.6 benerator-0.7.6.xsd"
defaultEncoding="UTF-8"
defaultDataset="US"
defaultLocale="us"
defaultLineSeparator="n">
<import domains="person"/>
<import platforms="db" />
<database id="db" url="jdbc:postgresql://127.0.0.1:6432/benerator" driver="org.postgresql.Driver" user="benerator" password="123" schema="public" catalog="benerator" />
<memstore id="memstore"/>
<setting name="min_tags_per_user" value="1"/>
<setting name="users_count" value="5"/>
<execute target="db" type="sql" onError="warn">
truncate users cascade;
truncate tags cascade;
</execute>
<bean id="dtGen" class="DateTimeGenerator">
<property name='minDate' value='2013-01-01'/>
<property name='maxDate' value='2013-01-31'/>
<property name='dateGranularity' value='00-00-02' />
<property name='dateDistribution' value='random' />
<property name='minTime' value='08:00:00' />
<property name='maxTime' value='17:00:00' />
<property name='timeGranularity' value='00:00:01' />
<property name='timeDistribution' value='random' />
</bean>
<bean id="tags_seq" spec="new DBSequenceGenerator('tags_id_seq', db)" />
<bean id="users_seq" spec="new DBSequenceGenerator('users_id_seq', db)" />
<bean id="tags_counter" spec="new IncrementalIdGenerator(1)" />
<iterate type="tags" source="tags.ent.csv" consumer="db,memstore,ConsoleExporter">
<id name="id" type="long" generator="tags_seq" />
<variable name="tags_count" generator="tags_counter" />
<setting name="max_tags_per_user" value="{tags_count}"/>
</iterate>
<echo>{ftl:Total tags count: ${max_tags_per_user}}</echo>
<generate type="users" count="{users_count}" consumer="db,ConsoleExporter">
<variable name="person" generator="PersonGenerator"/>
<id name="id" type="long" generator="users_seq" />
<attribute name="first_name" script="person.givenName"/>
<attribute name="last_name" script="person.familyName"/>
<attribute name="birthdate" script="person.birthDate"/>
<attribute name="email" script="person.email"/>
<attribute name="gender" script="person.gender" map="'MALE'->'true','FEMALE'->'false'"/>
<attribute name="created_at" type="timestamp" generator="dtGen"/>
<variable name="tags_per_user_count" type="int" min="{min_tags_per_user}" max="{max_tags_per_user}" distribution="random" />
<generate type="user_refs_tag" count="{tags_per_user_count}" consumer="db,ConsoleExporter">
<variable name="tag" source="memstore" type="tags" distribution="random" unique="true" />
<attribute name="tag_id" script="tag.id"/>
<attribute name="user_id" script="{users.id}"/>
</generate>
</generate>
</setup>
После запуска сценария вы получите выдачу в консоль и вместе с этим экспорт в БД. Рассмотрим, что случилось.
<database> — Создание коннекшена к БД. Есть ньюанс, что вам необходимо указать аттрибут catalog, равный названию БД.<memstore id="memstore"/> — Создает пул в памяти, куда мы будем складывать некие промежуточные данные, увидите позже. Пул доступен по идентификатору memstore<setting> — Это просто переменные, объявленные вне контекста циклов и доступные глобально.<bean> с классом DBSequenceGenerator. Этому классу передается название секвенса из базы и коннекшен, класс читает последнее значение секвенса и позволяет вам использовать его.<iterate> — А вот это уже новый тип циклов. Проходится по всему csv, создает сущности tags, создает свойства каждой сущности, согласно загаловку csv и наполняет эти самые свойства. Наполнение каждого из свойств можно переопределять.
Обратите внимание на consumer — там и db для экспорта в БД и memstore для сохранения в оперативной памяти. Это пригодиться мне позже.
<echo> — Вывод строки в консоль. Ньюанс в том, что мне нужно вывести строку с переменной, поэтому необходимо заключить всю строку в {ftl:}. Подробнее об этом уже в документации, я не разобрался до конца.<generate> для users я добавил новую локальную переменную, которая рандомно получается (distribution) в соответствии с аттрибутами min и max. А вот в этих аттрибутах используются ранее созданные мною глобальные переменные. Таким образом, в локальной переменной tags_per_user_count будет лежать рандомное число, которое я использую в качестве count для нового цикла <generate> внутри текущего.На выходе мы получаем следующую картину в БД:
benerator=> select * from tags;
id | name | weight | active
----+--------+--------+--------
1 | Tag 1 | 1 | t
2 | Tag 2 | 1.05 | t
3 | Tag 3 | 0.95 | t
4 | Tag 4 | 1 | t
5 | Tag 5 | 1.06 | t
6 | Tag 6 | 1.04 | t
6 | Tag 7 | 1.05 | t
8 | Tag 8 | 1.1 | t
9 | Tag 9 | 1.01 | t
10 | Tag 10 | 0.8 | t
(10 rows)
benerator=> select * from users;
id | first_name | last_name | birthdate | email | gender | created_at
---+------------+-----------+------------+------------------------------------+--------+---------------------
1 | Francis | Gardner | 1946-08-22 | francis_gardner@hotmail.com | t | 2013-01-01 09:46:57
2 | Todd | Robinson | 1911-07-24 | todd_robinson@william-thompson.org | t | 2013-01-21 14:42:54
3 | Jamie | Lyons | 1933-08-14 | jamielyons@owwybni.net | f | 2013-01-29 11:23:07
4 | Ronald | West | 1989-03-24 | ronald_west@yahoo.com | t | 2013-01-11 15:43:42
5 | Vanessa | Pope | 1942-05-27 | vanessapope@apc.de | f | 2013-01-05 12:28:43
(5 rows)
benerator=> select * from user_refs_tag;
user_id | tag_id
---------+--------
1 | 4
1 | 10
1 | 6
1 | 7
1 | 5
1 | 2
1 | 3
1 | 1
1 | 9
1 | 8
2 | 5
2 | 8
3 | 7
3 | 10
3 | 3
3 | 2
3 | 4
3 | 1
3 | 5
3 | 8
3 | 6
4 | 1
4 | 9
4 | 4
5 | 6
(25 rows)
Надеюсь, этот пример был показателен и не требуют дополнительного описания. Сейчас вы увидели лишь малую часть функционала бенератора, приведу несколько примеров из документации:
<bean id="special" class="com.my.SpecialGenerator" />
<attribute name="ean_code" source="db" selector="{{ftl:select ean_code from db_product where country='${shop.country}'}}"/>
<generate type="db_role" count="10" consumer="db" />
<generate type="db_user" count="100" consumer="db">
<reference name="role_fk" targetType="db_role" source="db" distribution="random"/>
</generate>
name,population
New York,8274527
Los Angeles,3834340
San Francisco,764976
<generate type="address" count="100" consumer="ConsoleExporter">
<variable name="city_data" source="cities.ent.csv" distribution="weighted[population]"/>
<id name="id" type="long" />
<attribute name="city" script="city_data.name"/>
</generate>
Все это весьма детально расписано в официальном мануале [2].
Так же, существует форум [3], на случай, если вы зашли в тупик со своей проблемой.
Я убежден, что если вы хотите действительно посмотреть, что получится из вашего проекта при больших объемах данных, а не оставлять это на авось, то этот инструмент просто незаменим.
Автор: madesst
Источник [4]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/testirovanie/27355
Ссылки в тексте:
[1] архив продукта: http://databene.org/databene-benerator/
[2] официальном мануале: http://databene.org/download/databene-benerator-manual-0.7.6.pdf
[3] форум: http://databene.org/phpBB3/index.php
[4] Источник: http://habrahabr.ru/post/169713/
Нажмите здесь для печати.