Présentation de l’outil de génération de jeux de données Benerator
Afin qu'une campagne de tirs de charges soit la plus utile possible, il est souvent nécessaire d'avoir un jeu de données conséquent et réaliste. Pour cela plusieurs solutions existent :
- importation des données de la base de données de production ;
- création du jeu de données à l'aide d'outils maison ;
- utilisation d'ETL ;
- utilisation de l’outil de test de charge et/ou de test fonctionnel afin d’exécuter des scripts qui vont remplir la base ;
- utilisation d'outil de création de données.
Nous allons nous focaliser sur la dernière solution avec l'utilisation de l'outil Benerator qui couvre largement ce besoin.
1. Un peu de théorie
Pour la partie théorique et quelques exemples, je vous laisse aller sur le site officiel et sur mon précédent article sur developpez.com.
2. Passons à la pratique
2.1 Présentation du cas de test
Dans cet article nous allons nous pencher sur la création de plusieurs jeux de données pour l'application démo PlantsByWebSphere livré avec IBM WebSphere 8. Nous utiliserons une base de données MySQL en remplacement de Derby.
PlantsByWebSphere est une démonstration de boutique en ligne comme vous pouvez le voir sur cette capture d'écran.
On remarque que :
- les produits sont divisés en 4 catégories (Flowers, Fruits & Vegetables, Trees et Accessories) ;
- l'application gère des comptes clients ;
- l'application gère un panier d'achats.
Regardons d'un peu plus près le schéma de la base de données.
Et son code SQL.
CREATE TABLE CUSTOMER ( CUSTOMERID VARCHAR(250) NOT NULL, PASSWORD VARCHAR(250), FIRSTNAME VARCHAR(250), LASTNAME VARCHAR(250), ADDR1 VARCHAR(250), ADDR2 VARCHAR(250), ADDRCITY VARCHAR(250), ADDRSTATE VARCHAR(250), ADDRZIP VARCHAR(250), PHONE VARCHAR(250)); ALTER TABLE CUSTOMER ADD CONSTRAINT PK_CUSTOMER PRIMARY KEY (CUSTOMERID); CREATE TABLE INVENTORY ( INVENTORYID VARCHAR(250) NOT NULL, NAME VARCHAR(250), HEADING VARCHAR(250), DESCRIPTION VARCHAR(250), PKGINFO VARCHAR(250), IMAGE VARCHAR(250), IMGBYTES LONG BIT VARYING, PRICE REAL, COST REAL, CATEGORY INTEGER, QUANTITY INTEGER, NOTES VARCHAR(250), ISPUBLIC INTEGER, MINTHRESHOLD INTEGER NOT NULL, MAXTHRESHOLD INTEGER NOT NULL); ALTER TABLE INVENTORY ADD CONSTRAINT PK_INVENTORY PRIMARY KEY (INVENTORYID); CREATE TABLE ORDER1 ( ORDERID VARCHAR(250) NOT NULL, SELLDATE VARCHAR(250), BILLNAME VARCHAR(250), BILLADDR1 VARCHAR(250), BILLADDR2 VARCHAR(250), BILLCITY VARCHAR(250), BILLSTATE VARCHAR(250), BILLZIP VARCHAR(250), BILLPHONE VARCHAR(250), SHIPNAME VARCHAR(250), SHIPADDR1 VARCHAR(250), SHIPADDR2 VARCHAR(250), SHIPCITY VARCHAR(250), SHIPSTATE VARCHAR(250), SHIPZIP VARCHAR(250), SHIPPHONE VARCHAR(250), CREDITCARD VARCHAR(250), CCNUM VARCHAR(250), CCEXPIREMONTH VARCHAR(250), CCEXPIREYEAR VARCHAR(250), CARDHOLDER VARCHAR(250), SHIPPINGMETHOD INTEGER NOT NULL, PROFIT REAL NOT NULL, CUSTOMERID VARCHAR(250)); ALTER TABLE ORDER1 ADD CONSTRAINT PK_ORDER1 PRIMARY KEY (ORDERID); CREATE TABLE ORDERITEM ( INVENTORYID VARCHAR(250) NOT NULL, NAME VARCHAR(250), PKGINFO VARCHAR(250), PRICE REAL NOT NULL, COST REAL NOT NULL, CATEGORY INTEGER NOT NULL, QUANTITY INTEGER NOT NULL, SELLDATE VARCHAR(250), ORDER_ORDERID VARCHAR(250) NOT NULL); ALTER TABLE ORDERITEM ADD CONSTRAINT PK_ORDERITEM PRIMARY KEY (INVENTORYID, ORDER_ORDERID); CREATE TABLE IDGENERATOR ( IDNAME VARCHAR(250) NOT NULL, IDVALUE INTEGER NOT NULL); ALTER TABLE IDGENERATOR ADD CONSTRAINT PK_IDGENERATOR PRIMARY KEY (IDNAME); CREATE TABLE BACKORDER ( BACKORDERID VARCHAR(250) NOT NULL, INVENTORYID VARCHAR(250), QUANTITY INTEGER NOT NULL, STATUS VARCHAR(250), LOWDATE BIGINT NOT NULL, ORDERDATE BIGINT NOT NULL, SUPPLIERORDERID VARCHAR(250) NULL); ALTER TABLE BACKORDER ADD CONSTRAINT PK_BACKORDER PRIMARY KEY (BACKORDERID); CREATE TABLE SUPPLIER ( SUPPLIERID VARCHAR(250) NOT NULL, NAME VARCHAR(250), STREET VARCHAR(250), CITY VARCHAR(250), USSTATE VARCHAR(250), ZIP VARCHAR(250), PHONE VARCHAR(250), URL VARCHAR(250)); ALTER TABLE SUPPLIER ADD CONSTRAINT PK_SUPPLIER PRIMARY KEY (SUPPLIERID);
Comme on peut le voir, le schéma de la base de données n'est pas ce qui se fait de mieux mais nous permettra d'avoir une bonne vision de l'utilisation de Benerator.
Après la découverte du schéma de la base de données et du fonctionnement Benerator, regardons comment créer le fichier XML de description du projet.
2.2 Paramétrage de la base de données cible et de la volumétrie du jeu de données
Mais avant cela, nous allons créer deux fichiers properties afin de regrouper les informations sur :
- la base de données ;
- la volumétrie cible.
Les informations sur la base de données seront dans le fichier mysql\PlantsByWebSphere.mysql.properties
bddUrl=jdbc:mysql://localhost:3306/test bddDriver=com.mysql.jdbc.Driver bddCatalog=test bddUser=root bddPassword=mysql
Les informations sur les volumétries cibles seront dans les fichiers de type PlantsByWebSphere.{volumetrie}.properties
Par exemple pour un poste de développement, le fichier se nommera PlantsByWebSphere.development.properties
taillePaquet=10 customer_count=10 inventory_cat0_count=10 inventory_cat1_count=3 inventory_cat2_count=2 inventory_cat3_count=5 supplier_count=1 order_count=60 items_per_order=3
2.3 Création du fichier de description de Benerator
- Commençons par l'entête du fichier.
<?xml version="1.0" encoding="iso-8859-1"?> <setup xmlns="http://databene.org/benerator/0.7.5" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://databene.org/benerator/0.7.5 http://databene.org/benerator-0.7.5.xsd"> </setup>
- Importons les domaines nécessaires et la bonne plate-forme.
<import domains = "person,net,address,finance" /> <import platforms = "db,csv"/>
- Définissons des valeurs par défaut pour le type de volumétrie et le type de base de données cible.
<comment>Valeur par défaut</comment> <setting name="volumetrie" default="development" /> <setting name="database" default="mysql" />
- Chargeons les deux fichiers properties que l'on a créés.
<comment>Récupération des valeurs pour la volumétrie et la base de données cible</comment> <include uri="{ftl:${database}/PlantsByWebSphere.${database}.properties}" /> <include uri="{ftl:PlantsByWebSphere.${volumetrie}.properties}" />
- Définissons l'URL de la base de données (ne pas oublier d'ajouter les drivers dans le classpath si nécessaire).
<database id="db" url="{bddUrl}" driver="{bddDriver}" user="{bddUser}" password="{bddPassword}" catalog="{bddCatalog}" batch="true" fetchSize="1000" />
- Afin d'éviter les mauvaises surprises, partons d'une base vide (drop + create) à l'aide de 2 scripts SQL.
<comment>Drop des tables avant leurs créations</comment> <execute uri="{ftl:${database}/drop_tables_${database}.sql}" target="db" onError="ignore" /> <execute uri="{ftl:${database}/create_tables_${database}.sql}" target="db" />
- Créons deux générateurs de nombre auto incrémenté pour les utiliser comme clé primaire.
<bean id="idGen" spec="new IncrementGenerator(1)" /> <bean id="idOrderGen" spec="new IncrementGenerator(1)" />
Maintenant, en fonction de ce que l'on veut tester, il nous reste plusieurs choix possibles comme :
- simuler des tests avec un catalogue de produits plus grand que celui défini par défaut ;
- simuler des tests à l'ouverture de la boutique en ligne ;
- simuler des tests après plusieurs jours de fonctionnement (ce qui correspond à avoir déjà des clients et des commandes en base de données) ;
- simuler plusieurs combinaisons des cas précédents.
Ces choix impacteront le volume et la répartition des données en base et donc les résultats des tests.
Dans notre cas, nous allons regarder comment créer des données pour chaque table.
- Commençons par ajouter des articles en base (table INVENTORY).
Chargeons les images des articles en mémoire afin de pouvoir remplir le champ IMGBYTES.
L'objectif du jeu de données étant une campagne de test de charge, nous ne nous soucierons pas de la correspondance entre l'image et le produit.
<bean id="pictures_files" class="org.databene.benerator.file.BinaryFileContentGenerator"> <property name="uri" value="images" /> <property name="filter" value=".*\.jpg" /> </bean>
Afin de contrôler le plus finement possible la répartition des articles, nous les traiterons catégorie par catégorie (champ CATEGORY)
Chaque catégorie sera générée de cette façon (ici pour la catégorie des Flowers).
generate type="INVENTORY" count="{inventory_cat0_count}" consumer="db" pageSize="{taillePaquet}" >
<id name="INVENTORYID" generator="idGen" />
<attribute name="NAME" values="'Petunia','African Orchid','Baby Breath','Black-eyed Susan','Coleus','Yellow Shasta Daisy','Perennial Foxglove','Geranium','Goodnight Moon Iris','Impatiens','Lily','Pansy','Primrose','Red Poinsettia','Red Rose','Sparkler Celosia','Tulip','White Poinsettia','White Rose','Zinnia'" />
<attribute name="HEADING" values="'Striped Brightness','Rare Delicate Beauty','Colorful Accent','Charming Simple Beauty','Tangerine Dream','Autumn Mix','Seasonal Beauty','Always in Bloom','Seasonal Simplicity','A Classic Beauty'" />
<attribute name="DESCRIPTION" />
<attribute name="PKGINFO" values="'4 plants'" />
<attribute name="IMAGE" values="'flower_petunias.jpg','flower_african_orchid.jpg','flower_bbreath.jpg','flower_black-eyed_susan.jpg','flower_coleus.jpg','flower_daisies.jpg','flower_foxglove.jpg','flower_geranium.jpg','flower_goodnight_moon_iris.jpg','flower_impatiens.jpg','flower_lily.jpg','flower_pansies.jpg','flower_primrose.jpg','flower_red_poinsettia.jpg','flower_red_rose.jpg','flower_sparkler_celosia.jpg','flower_tulips.jpg','flower_white_poinsettia.jpg','flower_white_rose.jpg','flower_zinnia.jpg'" />
<attribute name="IMGBYTES" generator="pictures_files" />
<attribute name="PRICE" min="10" max="45" />
<attribute name="COST" min="1" max="9" />
<attribute name="CATEGORY" values="0" />
<attribute name="QUANTITY" values="10000" />
<attribute name="NOTES" values="'NOTES and stuff'" />
<attribute name="ISPUBLIC" values="1" />
<attribute name="MINTHRESHOLD" values="50" />
<attribute name="MAXTHRESHOLD" values="200" />
</generate>En fonction de la catégorie et de l'objectif du test, on pourra modifier les paramètres suivants :
- count ;
- QUANTITY ;
- ISPUBLIC.
Je vous laisse créer les autres articles pour les 3 catégories restantes (CATEGORY aura pour valeurs : 1, 2 et 3).
- Créons à l'identique de l'original la table SUPPLIER.
<generate type="SUPPLIER" count="{supplier_count}" consumer="db" pageSize="{taillePaquet}" > <variable name="individu" generator="org.databene.domain.person.PersonGenerator" dataset="US" locale="en"/> <variable name="adresse" generator="org.databene.domain.address.AddressGenerator" dataset="US" locale="en"/> <attribute name="SUPPLIERID" script="individu.email" unique="true" /> <attribute name="NAME" script="individu.givenName" /> <attribute name="STREET" script="adresse.houseNumber + ' ' + adresse.street" /> <attribute name="CITY" script="adresse.city" /> <attribute name="USSTATE" script="adresse.state.id" /> <attribute name="ZIP" script="adresse.postalCode" /> <attribute name="PHONE" script="adresse.mobilePhone" /> <attribute name="URL" values="'http://localhost:9080/OrderProcessorEJB/services/FrontGate?wsdl'" /> </generate>
- Créons des comptes clients (table CUSTOMER).
<generate type="CUSTOMER" count="{customer_count}" consumer="db" pageSize="{taillePaquet}" > <variable name="individu" generator="org.databene.domain.person.PersonGenerator" dataset="US" locale="en"/> <variable name="adresse" generator="org.databene.domain.address.AddressGenerator" dataset="US" locale="en"/> <attribute name="CUSTOMERID" script="individu.email" unique="true" /> <attribute name="PASSWORD" minLength="6" maxLength="10" /> <attribute name="FIRSTNAME" script="individu.givenName" /> <attribute name="LASTNAME" script="individu.familyName" /> <attribute name="ADDR1" script="adresse.houseNumber + ' ' + adresse.street" /> <attribute name="ADDR2" maxLength="20" /> <attribute name="ADDRCITY" script="adresse.city" /> <attribute name="ADDRSTATE" script="adresse.state.id" /> <attribute name="ADDRZIP" script="adresse.postalCode" /> <attribute name="PHONE" script="adresse.privatePhone" /> </generate>
Comme on peut le voir, on utilise les domaines Person et Address afin d'avoir des données réalistes.
Il nous reste à créer des commandes clients (tables ORDER1, ORDERITEM et IDGENERATOR).
- Remplissons la table ORDER1.
<generate type="ORDER1" count="{order_count}" consumer="db" pageSize="{taillePaquet}" > <id name="ORDERID" generator="idOrderGen" /> <attribute name="SELLDATE" type="date" min="2009-01-01" max="2011-08-01" converter="org.databene.commons.converter.ToStringConverter" /> <attribute name="CUSTOMERID" source="db" selector="select CUSTOMERID from CUSTOMER" cyclic="true" /> <attribute name="BILLNAME" source="db" subSelector="{{'select LASTNAME from CUSTOMER where CUSTOMERID = \'' + this.CUSTOMERID + '\''}}" /> <attribute name="BILLADDR1" source="db" subSelector="{{'select ADDR1 from CUSTOMER where CUSTOMERID = \'' + this.CUSTOMERID + '\''}}" /> <attribute name="BILLADDR2" source="db" subSelector="{{'select ADDR2 from CUSTOMER where CUSTOMERID = \'' + this.CUSTOMERID + '\''}}" /> <attribute name="BILLCITY" source="db" subSelector="{{'select ADDRCITY from CUSTOMER where CUSTOMERID = \'' + this.CUSTOMERID + '\''}}" /> <attribute name="BILLSTATE" source="db" subSelector="{{'select ADDRSTATE from CUSTOMER where CUSTOMERID = \'' + this.CUSTOMERID + '\''}}" /> <attribute name="BILLZIP" source="db" subSelector="{{'select ADDRZIP from CUSTOMER where CUSTOMERID = \'' + this.CUSTOMERID + '\''}}" /> <attribute name="BILLPHONE" source="db" subSelector="{{'select PHONE from CUSTOMER where CUSTOMERID = \'' + this.CUSTOMERID + '\''}}" /> <attribute name="SHIPNAME" script="this.BILLNAME" /> <attribute name="SHIPADDR1" script="this.BILLADDR1" /> <attribute name="SHIPADDR2" script="this.BILLADDR2" /> <attribute name="SHIPCITY" script="this.BILLCITY" /> <attribute name="SHIPSTATE" script="this.BILLSTATE" /> <attribute name="SHIPZIP" script="this.BILLZIP" /> <attribute name="SHIPPHONE" script="this.BILLPHONE" /> <attribute name="CREDITCARD" pattern="(Visa|Master Card|American Express)" /> <attribute name="CCNUM" generator="org.databene.domain.finance.CreditCardNumberGenerator" /> <attribute name="CCEXPIREMONTH" type="int" min="01" max="12" converter="org.databene.commons.converter.ToStringConverter" /> <attribute name="CCEXPIREYEAR" type="int" min="2013" max="2016" converter="org.databene.commons.converter.ToStringConverter" /> <attribute name="SHIPPINGMETHOD" min="0" max="2" /> <attribute name="CARDHOLDER" script="this.BILLNAME" /> <attribute name="PROFIT" type="double" values="1.0" /> </generate>
Nous utilisons ici les commandes this et subSelector nous permettant d'être cohérent sur les données de l'acheteur par rapport à la table CUSTOMER.
- Puis la table ORDERITEM.
<generate type="dummy" count="{order_count}" pageSize="{taillePaquet}" > <variable name="x" source="db" selector="select ORDERID from ORDER1" distribution="increment" unique="true" /> <generate type="ORDERITEM" count="{items_per_order}" consumer="db"> <variable name="y" source="db" selector="select INVENTORYID from INVENTORY" distribution="random" unique="true" /> <attribute name="INVENTORYID" script="y"/> <attribute name="ORDER_ORDERID" script="x"/> <attribute name="NAME" source="db" subSelector="{{'select NAME from INVENTORY where INVENTORYID = \'' + this.INVENTORYID + '\''}}" /> <attribute name="PKGINFO" source="db" subSelector="{{'select PKGINFO from INVENTORY where INVENTORYID = \'' + this.INVENTORYID + '\''}}" /> <attribute name="PRICE" source="db" subSelector="{{'select PRICE from INVENTORY where INVENTORYID = \'' + this.INVENTORYID + '\''}}" /> <attribute name="COST" source="db" subSelector="{{'select COST from INVENTORY where INVENTORYID = \'' + this.INVENTORYID + '\''}}" /> <attribute name="CATEGORY" source="db" subSelector="{{'select CATEGORY from INVENTORY where INVENTORYID = \'' + this.INVENTORYID + '\''}}" /> <attribute name="QUANTITY" min="1" max="20" /> <attribute name="SELLDATE" source="db" subSelector="{{'select SELLDATE from ORDER1 where ORDERID = \'' + this.ORDER_ORDERID + '\''}}" /> </generate> </generate>
Ici on doit utiliser deux boucles imbriquées afin de garantir l'unicité de la clé primaire composée de deux champs (INVENTORYID,ORDERITEM).
Mettons à jour le prix de chaque commande (champ PROFIT de la table ORDER1).
<iterate type="ORDER1" source="db" consumer="db.updater()"> <attribute name="PROFIT" source="db" selector="{{ftl:select IFNULL(SUM((PRICE - COST)*QUANTITY), 0) from ORDERITEM where ORDER_ORDERID = '${ORDER1.ORDERID}' }}" cyclic="true"/> </iterate>
On utilise la fonction SQL IFNULL de MySQL afin d'être sûr de mettre une valeur non null dans le champ PROFIT.
Une autre solution est d'utiliser un subSelector à la place de la combinaison de selector et cyclic="true".
<iterate type="ORDER1" source="db" consumer="db.updater()"> <attribute name="PROFIT" source="db" subSelector="{{ftl:select IFNULL(SUM((PRICE - COST)*QUANTITY), 0) from test.ORDERITEM where ORDER_ORDERID = '${ORDER1.ORDERID}' }}" /> </iterate>
- Pour la dernière table IDGENERATOR, on va utiliser les capacités de Benerator a importer des fichiers CSV.
Importons le fichier CSV idgenerator.import.csv.
<iterate type="IDGENERATOR" source="idgenerator.import.csv" encoding="utf-8" consumer="db" />Puis mettons à jour la valeur du champ IDVALUE.
<iterate type="IDGENERATOR" source="db" consumer="db.updater()"> <attribute name="IDVALUE" source="db" selector="{{ftl:select 1+${order_count}*50 from IDGENERATOR where IDNAME = 'ORDER' }}" cyclic="true"/> </iterate>
Une autre solution est de tout faire lors de l'importation du fichier csv à l'aide du mot clé condition.
<iterate type="IDGENERATOR" name="idg" source="idgenerator.import.csv" encoding="utf-8" consumer="db" > <attribute name="IDVALUE" condition="idg.IDNAME == 'ORDER'" script="1+order_count*50"/> </iterate>
Il ne reste plus qu'à exécuter Benerator pour générer notre jeu de données.
Notre fichier XML de description est prêt et je vous laisse le paramétrer au mieux pour l'objectif de vos tests.
Ma conclusion sur cet outil est toujours la même :
- il est pratique ;
- pilotable en ligne de commande et donc très flexible pour son intégration dans un processus ;
- larges possibilitées :
- générer un jeu de données à partir de rien ;
- anonymiser un jeu de données ;
- traitement de données existantes comme pour un ETL ;
- création de fichier d'entrée pour un test de charge (création de login/password, ...) ;
- possibilité d'utiliser le même fichier XML de configuration de Benerator pour divers environnements (test, développement, pré production...) à l'aide de fichiers properties;
- documentation complète ;
- développement actif ;
- gratuit ;
- open source ;
- support par le développeur ;
- extensible.




