Tests Greenpepper en PHP
Voici un retour d'expérience d'une mission (réussie) que j'ai effectuée, qui consistait à faire exécuter des tests fonctionnels en PHP à Greenpepper.
Disclaimer : les versions des logiciels exposés dans cet article sont vieilles, mais se retrouvent encore en entreprise. Cet article se veut un guide pour ceux qui ont encore ces versions et ne peuvent pas, pour diverses raisons, faire autrement que les utiliser.
Malgré son âge, Greenpepper reste très présent en entreprise, où il sert à exécuter des spécification fonctionnelles en Java ou en .NET
Malgré la concurrence, PHP reste un langage bien implanté en entreprise et avec lequel il est facile et rapide de développer des applications web.
Il est donc logique de vouloir utiliser l'un avec l'autre.
C'est là où ça se complique : demandez à Google, ou autour de vous, vous obtiendrez peu de réponses pertinentes, ou des grimaces, ou des "moi je suis passé à Fitnesse / Behat"
Solitude ...
Pourtant, vous allez voir que c'est parfaitement faisable !
PHPSud
Nous sommes parti de Confluence 2.10, équipé de Greenpepper 2.6 (rappelons que Greenpepper est un plugin de Confluence).
Pour faire exécuter les tests fonctionnels d'une page, Greenpepper dispose de 'Runner'. Ce sont des lignes de commande qu'il va lancer avec certains paramètres (templatisable) et dont il va attendre certains résultats. C'est ici que tout va se jouer (et ce fut notre principal champs de bataille)
PHPSud (développé par un Octo, d'ailleurs) est un jar qui répond à cette contrainte ; il va exécuter du code PHP (vos fixtures) et remonter le résultat à Greenpepper. Pour la démonstration j'ai pris la version 2.7m1, ainsi que pour le core. Mais cela fonctionne avec la 2.6
Nos postes sont des desktop (physiques et réels) sous Windows, avec Wamp installé et fonctionnel.
Pour configurer un runner de Greenpepper, connectez-vous en tant qu'administrateur de Confluence, allez dans 'Browse', en haut à droite, puis validez sur 'Confluence Admin'
Dans le panneau de gauche, cliquez sur 'Plugins'.
Dans la colonne 'Installed Plugins' vous devriez avoir 'GreenPepper Toolbox'. Cliquez dessus.
Puis sur 'Configure plugin'.
Puis sur 'Runner'. Cliquez sur 'Ajouter un nouveau runner'
Entrez le nom que vous souhaitez, puis la ligne de commande adéquat dans le champs 'Command Line'
Chez nous, c'est celle-ci : C:\wamp\bin\php\php5.4.3\php.exe C:/tmp/phpi.php java -cp ${classpaths} ${mainClass} C:\wamp\bin\php\php5.4.3\php.exe c:\wamp/www\ gp.php ${inputPath} ${outputPath} --xml --debug -r ${repository}
Évidemment, les chemins sont à adapter à votre cas.
Voici les autres paramètres à fournir :
Main Class: com.greenpepper.phpsud.Runner
Environment: JAVA
Server Name: None
Server Port: None
Secured mode: Décoché
Validez, puis cliquez sur 'Edit' de la ligne 'Classpath', et entrez :
Classpaths:
- C:/GP-Trial/tomcat/webapps/confluence/WEB-INF/lib/xmlrpc-2.0+xmlrpc61.1+sbfix.jar
- C:/GP-Trial/tomcat/webapps/confluence/WEB-INF/lib/greenpepper-core-2.7m1-fixtures.jar
- C:/GP-Trial/tomcat/webapps/confluence/WEB-INF/lib/greenpepper-core-2.7m1-tests.jar
- C:/GP-Trial/tomcat/webapps/confluence/WEB-INF/lib/greenpepper-extensions-php-2.7m1-jar-with-dependencies.jar
- C:/GP-Trial/tomcat/webapps/confluence/WEB-INF/lib/greenpepper-core-2.7m1.jar
- C:/GP-Trial/tomcat/webapps/confluence/WEB-INF/lib/greenpepper-extensions-java-2.7m1.jar
- C:/GP-Trial/tomcat/webapps/confluence/WEB-INF/lib/commons-codec-1.3.jar
- C:/GP-Trial/tomcat/webapps/confluence/WEB-INF/lib/greenpepper-confluence-plugin-2.7m1-complete.jar
Là aussi, il faut adapter les chemins à votre environnement. Vous remarquerez que nous avons été 'optimiste' dans la liste des jar à inclure ...
Décortiquons la ligne de commande : il y a les paramètres ${classpaths} ${mainClass} ${inputPath} ${outputPath} ${repository}. Vous en trouverez la description ici
"classPaths" et "mainClass" ont les valeurs que vous indiquez dans la configuration du runner. Les autres variables sont remplacées par Greenpepper à la volée. Comme vous êtes attentif, vous avez vu que ce n'est pas un jar qui est exécuté, mais un script php :
C:\wamp\bin\php\php5.4.3\php.exe C:/tmp/phpi.php
Ce script a pour but de faire le ménage dans les paramètres passés à PHPSud, ainsi qu'espionner ce qu'il se trame, à des fins de débuggage :
<?php
error_log(PHP_EOL . '---------------------------------------------' . PHP_EOL, 3, 'c:\tmp\phpi.log');
error_log(date('d/m/Y H:i:s') . PHP_EOL, 3, 'c:\tmp\phpi.log');
error_log(implode(' ', $argv) . PHP_EOL, 3, 'c:\tmp\phpi.log');
error_log(var_export($argv,true) . PHP_EOL, 3, 'c:\tmp\phpi.log');
foreach ($argv as $i => $arg) {
$argv[$i] = str_replace('Program Files (x86)', 'progra~2', $argv[$i]);
$argv[$i] = str_replace('&includeStyle=false', '', $argv[$i]);
$argv[$i] = str_replace('127.0.0.1', '10.0.2.15', $argv[$i]);
}
array_shift($argv);
$command = implode(' ', $argv);
error_log("system($command);" . PHP_EOL, 3, 'c:\tmp\phpi.log');
$ret = shell_exec($command);
echo $ret
Le premier str_replace (ligne 8) est à adapter à votre cas. Le troisième (ligne 10) sert à ... exécuter les specs à distance ! En l'occurrence ici l'adresse ip est celle du serveur (VM ou machine physique) sur laquelle tourne Confluence.
Le script php exécute ce qui lui est donné sur sa ligne de commande (en modifiant les paramètres à la volée) ; ici ce n'est autre qu'une ligne de commande qui a pour but de lancer PHPSud avec ces paramètres :
java -cp ${classpaths} ${mainClass} C:\wamp\bin\php\php5.4.3\php.exe c:\wamp\www\ gp.php ${inputPath} ${outputPath} --xml --debug -r ${repository}
qui sont :
- le chemin vers l’exécutable php,
- le répertoire où résident les fixtures,
- le script php de bootstrap,
- l'input path (la page de specs greenpepperisée)
- l'output path (le fichier généré par le plugin)
- la génération du rapport en xml
- un peu de débug
- et le dépôt où aller chercher les specs à exécuter. Par défaut c'est un fichier, mais si vous laissez ce défaut ça ne fonctionne pas car PHPSud a besoin de récupérer les specs via xmlrpc.
Voilà pour la configuration du runner.
Les specs et les fixtures
Vous avez sans doutes noté que j'ai omis de parler du script php de bootstrap. Je vais le faire maintenant. C'est lui qui va (faire) exécuter les fixtures.
Voici la page de specs que nous avons utilisé :
|| Setup | GroupOfPeople ||
| Gender | FirstName | FamilyName |
| M | Gabriel | Guillon |
| M | Lionel | Ruhier \\ |
|| Rule for || calculator ||
|| x || y || sum? || product? || quotient? ||
| 0 | 0 | 0 | 0 | error |
| 1 | 0 | 1 | 0 | error |
| 0 | 1 | 1 | 0 | 0 |
| 1 | 1 | 2 | 1 | 1 |
| 10 | 2 | 12 | 20 | 5 |
|| Rule for \\ || EnregistrerParametre \\ ||
| Parametre | IdentifiantReferentiel | LibelleReferentiel | CodeReferentiel | Enregistrer ? |
| Langue | 2 | Italien | IT | OK |
| Pays | 4 | Bénin | BEN | OK |
| Type programme | 3 | Série | 3 | OK |
| CSA | 2 | Grande Diffusion | DIF | OK |
| Ayant-Droit | 3 | A.S.S.E | 3 | OK |
|| List of || ListerParametre || Parametre || Langue ||
| IdReferentiel | LibelleReferentiel | CodeReferentiel |
| 1 | Français | FR |
| 2 | Italien | IT |
| 3 | Espagnol | ES |
| 4 | Anglais | GB |
| 5 | Allemand | D |
| 6 | Japonais | JP |
| 7 | Coréen | COR |
| 8 | Chine | CN |
| 9 | Norvégien | NO |
| 10 | Roumain | RO |
|| Set of || SeterParametre || Parametre || Langue ||
| IdReferentiel | LibelleReferentiel | CodeReferentiel |
| 1 | Francais | FR |
| 2 | Italien | IT |
| 3 | Espagnol | ES |
|| do with | Bank ||
| open checking account | 12345-67890 | under the name of | Spongebob | | Squarepants |
|| accept | withdraw | $25 \\ | from account | 12345-67890 ||
|| check | that balance of account | 12345-67890 | is | $0.00 ||
|| reject | withdraw | $25 | from account | 123 ||
|| display | balance ||
| end |
Et le php de bootstrap correspondant. En vérité il ne bootstrap que lui-même, mais rien n'empêche de l'adapter pour qu'il exécute des fixtures situées dans d'autres répertoires :
<?php
// Setup
class GroupOfPeople {
public $gender;
public $firstName;
public $familyName;
public function GroupOfPeople()
{
}
// Appelé par GP pour chaque ligne
public function enterRow() {
}
public function setGender($gender) {
$this->gender=$gender;
}
public function setFirstName($fn) {
$this->firstName=$fn;
}
public function setFamilyName($fmn) {
$this->familyName=$fmn;
}
}
class Calculator {
private $x;
private $y;
// Appel le seter pour chaque ligne/colonne, et la fonction pour obtenir le résultat
function setX($x) {
$this->x = $x;
}
function setY($y) {
$this->y = $y;
}
function Sum() {
return $this->x + $this->y;
}
function Product() {
return $this->x * $this->y;
}
function Quotient() {
if ($this->y == 0) {
throw new Exception("Divide by zero");
}
return $this->x / $this->y;
}
}
class EnregistrerParametre {
protected $parametre;
protected $identifiantReferentiel;
protected $libelleReferentiel;
protected $codeReferentiel;
public function setParametre($value) {
$this->Parametre = $value;
}
public function setIdentifiantReferentiel($value) {
$this->identifiantReferentiel = $value;
}
public function setLibelleReferentiel($value) {
$this->libelleReferentiel = $value;
}
public function setCodeReferentiel($value) {
$this->codeReferentiel = $value;
}
public function Enregistrer() {
// TODO
return "OK";
}
}
// List of
class ListerParametre {
protected $parametre;
private $langue;
public function __construct($parametre) {
$this->parametre = $parametre;
}
public function query() {
return call_user_func(array($this, $this->parametre));
}
// Retourne un tableau d'objets ordonnées
// Le premier n-uplet renvoyé doit correspondre un premier n-uplet des specs
protected function Langue() {
$test = new LangueFixture();
$test->idReferentiel = 2;
$test->libelleReferentiel = 'Italien';
$test->codeReferentiel = 'IT';
return array(
$test,
$test,
$test,$test,$test,$test,$test,$test,$test,$test);
}
}
class LangueFixture {
public $idReferentiel;
public $libelleReferentiel;
public $codeReferentiel;
public function getCodeReferentiel() {
return $this->codeReferentiel;
}
public function getLibelleReferentiel() {
return $this->libelleReferentiel;
}
public function getIdReferentiel() {
return $this->idReferentiel;
}
}
class SeterParametre {
protected $parametre;
private $langue;
public function __construct($parametre) {
$this->parametre = $parametre;
}
public function query() {
return call_user_func(array($this, $this->parametre));
}
// Retourne une liste d'objets non ordonnées.
// GP affiche vert si un n-uplet ici correspond à un n-uplet sur la page de specs
protected function Langue() {
$it = new LangueFixture();
$it->idReferentiel = '2';
$it->libelleReferentiel = 'Italien';
$it->codeReferentiel = 'IT';
$fr = new LangueFixture();
$fr->idReferentiel = '1';
$fr->libelleReferentiel = 'Francais';
$fr->codeReferentiel = 'FR';
return array($fr,$it);
}
}
// Do with
class Bank {
public function OpenCheckingAccountUnderTheNameOf($a,$b,$c) {
}
// Gére l'accept et le reject
public function WithdrawFromAccount($a,$account) {
if ('12345-67890' == $account) {
return true;
}
return false;
}
// Check
public function ThatBalanceOfAccountIs($account) {
if ($account == '12345-67890') {
return "$0.00";
}
}
public function Balance() {
return 'Taureau';
}
public function End() {
}
}
Cette fixture implémente normalement tout ce dont vous avez besoin : Setup, doWith, setOf, listOf et ruleFor.
Conclusion
J'ai passé sous silence dans cet article toutes les ornières et culs-de-sac dans lequel nous nous sommes perdu afin d'aller à l'essentiel : faire faire des tests fonctionnels en PHP à Greenpepper.
En espérant vous avoir fait gagner un temps précieux ; si vous avez suivi cet article dans le but d'implémenter les TF en PHP sous Greenpepper, et que ça vous avez réussi, ou pas, laissez-moi un message dans les commentaires ; j’enrichirai l'article de vos expériences.
Pour finir, quelques ...
Tips
- Faites attention aux variables d'environnement Windows, notamment PATH et JAVA_HOME.
- Ne mettez pas d'echo dans vos fixtures : ça perturbe grandement Greenpepper à l'arrivée