Praspel, un langage de spécification par contrats

Logo du PHPTour

Ivan

Enregistrement

Qui sommes-nous ?

Ivan Enderlin :

ivan.enderlin@ { hoa-project.net atoum.org inria.fr

Qui sommes-nous ?

Julien Bianchi :

@jubianchi { twitter.com github.com
julien.bianchi@ { atoum.org hoa-project.net

Julien

DĂ©finitions

System Under Test (SUT) : systÚme qui sera exécuté

  • prĂ©ambule (given)
  • donnĂ©es (when)
  • oracle (then)
  • cas de test
  • place le SUT dans un Ă©tat particulier
  • test, exĂ©cute le SUT
  • Ă©tablit le verdict, est-ce que le produit de l’exĂ©cution et l’état du SUT aprĂšs son exĂ©cution sont ceux attendus ou non ?

Valeurs du verdict : succÚs, échec ou inconclusif

Suite de tests : ensemble de cas de tests

Ivan

Cartographie des tests

Taille du systÚme unitaire composant intégration systÚme Support de conception boßte noire boßte blanche Type de test fonctionnel robustesse performance utilisabilité

Nous : test unitaire, boüte noire et fonctionnel 📖

Design by Contract

Contrat :

  • inventĂ© par B. Meyer en 1992 dans le langage Eiffel 📖 📖
  • dĂ©crit un modĂšle en utilisant des annotations
  • exprime des contraintes formelles : prĂ©conditions, postconditions et invariants
  • caller : « je m'engage Ă  satisfaire ta prĂ©condition »
  • callee : « je m'engage Ă  Ă©tablir ma postcondition »
  • invariants satisfaits avant et aprĂšs l'exĂ©cution du callee

Langages de contrats

Chaque « grand » langage de programmation à un langage de contrat :

Pour PHP ? Rien !

Contract-based Testing

InventĂ© par B. K. Aichernig en 2003 📖

Principe :

  • utilise les prĂ©conditions pour gĂ©nĂ©rer des donnĂ©es de tests
  • utilise les postconditions et invariants pour Ă©tablir le verdict du test Ă  l'exĂ©cution, grĂące Ă  un Runtime Assertion Checker (RAC)

ProblÚmes :

  • contraintes souvent exprimĂ©es avec des formules logiques
  • difficile d'en gĂ©nĂ©rer des donnĂ©es

Solutions

Faire du Contract-based Testing en PHP en 3 étapes

  1. un (nouveau) langage de contrats : 📖
    • adapter Ă  PHP
    • pragmatique
    • facile Ă  utiliser pour le dĂ©veloppeur
    • assemblant plusieurs mĂ©thodes de test
  2. des gĂ©nĂ©rateurs de donnĂ©es : 📖 📖
    • pour tous les types usuels
  3. générer automatiquement des suites de tests unitaires

Agenda

  1. Praspel et les domaines réalistes
  2. Génération automatique de données
  3. Génération automatique de tests unitaires
  4. Extension atoum/praspel-extension
  5. Conclusion

1 Praspel et les domaines réalistes

Domaines réalistes

Naïvement, les domaines réalistes :

  • raffinent les types usuels, comme les entiers, les chaĂźnes de caractĂšres, les tableaux etc.
  • permettent d'exprimer des donnĂ©es plus complexes, comme des grammaires, des graphes etc.

Réaliste signifie qu'ils sont conçus pour spécifier des domaines de données pertinents pour un contexte spécifique

Exemple : adresse email

Caractéristiques

Ils ont les deux caractéristiques suivantes :

TrÚs important !

Hiérarchie

Une classification possible : verticale

Héritage permet de raffiner les caractéristiques :

Univers Urealdom :

couches undefined boolean integer float string array class bibliothĂšque standard bibliothĂšque utilisateur

Exemple

class        Boundinteger
  extends    Integer
  implements IRealdom\Interval,
             IRealdom\Finite,
             IRealdom\Nonconvex,
             IRealdom\Enumerable {

    protected $_arguments = [
        'Constinteger lower' => PHP_INT_MIN,
        'Constinteger upper' => PHP_INT_MAX
    ];

    public function predicate ( $q ) {

        return    parent::predicate($q)
               && $q >= $this['lower']->getConstantValue()
               && $q <= $this['upper']->getConstantValue();
    }

    public function sample ( $sampler ) {

        return $sampler->getInteger(
            $this['lower']->sample($sampler),
            $this['upper']->sample($sampler)
        );
    }

    // ...
}

Implémentation : un domaine réaliste = une classe

Hiérarchie : Boundinteger hérite de Integer

Classification : caractérisation du domaine réaliste

ParamĂštres : permet de reprĂ©senter des structures de donnĂ©es complexes (structures rĂ©cursives
)

Caractéristique de prédicabililité

Caractéristique de générabilité

BibliothĂšque standard

  1. Undefined
  2. Array
  3. Boolean
  4. Class
  5. Float
  6. Integer
  7. String
  8. Bag
  9. Boundfloat
  10. Boundinteger
  11. Color
  12. Constarray
  13. Constboolean
  14. Constfloat
  15. Constinteger
  16. Constnull
  17. Conststring
  18. Date
  19. Empty
  20. Even
  21. Grammar
  22. Natural
  23. Number
  24. Object
  25. Odd
  26. Regex
  27. Smallfloat
  28. Smallinteger
  29. Timestamp

Praspel

PHP Realistic Annotation and SPEcification Language :

Vue générale

class C {

    /**
     * @invariant I;
     */
    protected $a;

    /**
     * @requires P;
     * @behavior α {
     *     @description 'Apply the α process.';
     *     @requires P_α;
     *     @behavior ÎČ {
     *         @requires  P_α_ÎČ;
     *         @ensures   Q_α_ÎČ;
     *         @throwable T_α_ÎČ;
     *     }
     * }
     * @behavior Îł {
     *     @requires P_Îł;
     *     

     * }
     * @default {
     *     @description 'Apply the fallback process.';
     *     @ensures   Q_D;
     *     @throwable T_D;
     * }
     */
    public function f ( ) { }
}

@invariant exprime un invariant

@requires exprime une précondition

@ensures exprime une postcondition normale

@throwable exprime une postcondition exceptionnelle ; de la forme T_C with T_E

@behavior exprime un comportement

Les @behavior peuvent s'imbriquer

@default exprime un comportement par défaut, avec une clause @requires implicite

@description décrit informellement un comportement

Expressions

P, Q et T_E représentent une expression :

Déclaration :

  • associe un ou plusieurs domaines rĂ©alistes Ă  une variable du SUT (arguments) ou du modĂšle (let)
  • plusieurs = disjonction de domaines rĂ©alistes
  • \result reprĂ©sente le rĂ©sultat du SUT
  • \old(i) reprĂ©sente la valeur de i dans le prĂ©-Ă©tat (avant l'exĂ©cution du SUT)

Prédicat :

  • toutes les constructions de Praspel supportent la validation et la gĂ©nĂ©ration mais

  • 
 toutes les contraintes ne peuvent pas ĂȘtre exprimĂ©es
  • \pred(p) oĂč p est du code PHP pur (un prĂ©dicat en forme normale disjonctive, DNF)

Qualification :

  • qualifier une donnĂ©e avec un adjectif
  • « mot-clé » reprĂ©sentant des contraintes usuelles
  • facilite la vie, facile Ă  lire, Ă©vite les erreurs

Exemple

class Filesystem {

    const SIZE = 1024;

    /**
     * @invariant _usage: boundinteger(0, static::SIZE);
     */
    protected $_usage = 0;

    /**
     * @invariant _map: array([to class('File')], 0..0xffff);
     */
    protected $_map = array();

    /**
     * @ensures \result: this->_usage;
     */
    public function getUsage ( ) { /* 
 */ }

    /**
     * @requires file: class('File') and
     *           index: 0..0xffff or void;
     * @behavior full {
     *     @description 'The filesystem is full.';
     *     @requires  \pred('$this->getUsage() + $file->getSize()
     *                           > static::SIZE');
     *     @ensures   \none;
     *     @throwable AllocationException e with
     *                    file->isAttached(): false and
     *                    e->getFilesystem(): this;
     * }
     * @default {
     *     @ensures file->isAttached: true and
     *              \result: boolean();
     * }
     */
    public function store ( File $file, $index = null ) { /* 
 */ }
}

2 Génération automatique de données

Génération standard

Génération par défaut

Totalement pseudo-aléatoire

  1. Pour chaque variable :
    • sĂ©lection d'un domaine rĂ©aliste dans la disjonction alĂ©atoirement
    • appel de la mĂ©thode sample, puis predicate
  2. Évaluation des \pred(p)
@requires i: integer() or void and
          j: float() or boolean() and
          \pred('is_foo() && is_bar()');
@requires i: integer() or void and
          j: float() or boolean() and
          \pred('is_foo() && is_bar()');

SĂ©lection de la variable i

@requires i: integer() or void and
          j: float() or boolean() and
          \pred('is_foo() && is_bar()');

Choix du domaine réaliste integer ; i = 42

@requires i: integer() or void and
          j: float() or boolean() and
          \pred('is_foo() && is_bar()');

SĂ©lection de la variable j

@requires i: integer() or void and
          j: float() or boolean() and
          \pred('is_foo() && is_bar()');

Choix du domaine réaliste boolean ; j = true

@requires i: integer() or void and
          j: float() or boolean() and
          \pred('is_foo() && is_bar()');

Évaluation du prĂ©dicat

Génération de tableaux

Dans PHP :

Dans Praspel :

👉 Processus dĂ©diĂ© Ă  la gĂ©nĂ©ration de tableaux nĂ©cessaire

Approche

  1. étude des propriétés les plus populaires sur les tableaux (sur plus de 60 projets, plus 5 millions de LOC) ; retenues :

    • count
    • array_key_exists
    • in_array
  2. (syntaxe) extension de Praspel pour les inclure nativement
  3. (sémantique) transformation en propriétés ensemblistes
  4. solveur ensembliste

Syntaxe des propriétés

Déclaration de tableau : a: array(D, L)

Paire : a[K]: V

  • toute valeur dans K est la clĂ© d'une paire dans le tableau a
  • toute paire dont la clĂ© est dans K a sa valeur dans V

Exemple :

a[7..8]: true or 42

équivaut à :

   array_key_exists(7, $a)
&& array_key_exists(8, $a)
&& (in_array(true, $a) || in_array(42, $a))
&& ($a[7] === true     || $a[7] === 42)
&& ($a[8] === true     || $a[8] === 42)

Clé : a[K]: _

  • toute valeur de K doit ĂȘtre une clĂ© du tableau a

Exemple :

a[7..8]: _

équivaut à :

   array_key_exists(7, $a)
&& array_key_exists(8, $a)

Valeur : a[_]: V

  • toute valeur de V doit ĂȘtre une valeur du tableau a

Exemple :

a[_]: 'foo'

équivaut à :

in_array('foo', $a)

Unicité : a is unique

  • toutes les clĂ©s sont uniques (intrinsĂšque au tableau)
  • toutes les valeurs sont uniques

Exemple :

a is unique

équivaut à :

// for simple cases
count($a) === count(array_unique($a, SORT_REGULAR))

Mais aussi


a[K]!: V

  • toute valeur de K est la clĂ© d'une paire dans le tableau a
  • toute paire dont la clĂ© est dans K n'a pas sa valeur dans V

a[K]!: _

  • aucune paire du tableau a n'a sa clĂ© dans K

a[_]!: V

  • 


SĂ©mantique ensembliste

Un Constraint Satisfaction Problem et un triplet ( ÎŁ, Δ, Π ) oĂč


Les variables σ ∈ ÎŁ sont la taille S, les ensembles X et Y, la fonction totale H


Cardinalité : card ( X ) = S , S ≄ 0 , card ( X ) ≄ card ( Y ) 


Domaines et co-domaines : Y = H ^ ( X ) , X = âˆȘ 1 ≀ i ≀ j X i 


Paires : K ⊆ X , H ^ ( K ) ⊆ V , H ^ ( K ) ∩ V = ∅ 
 📖

censuré
Julien + démo

Solveur ensembliste

Fonctionne en deux étapes :

  1. propagation et consistance
  2. labelling

Exemple :

length: 0..5 or 10
a     : array([to string('a', 'e', 1)], length)
a[0]  : 'b' or 'c'
a is unique

a comme solution possible :

[
    0 => 'c',
    1 => 'd',
    2 => 'a',
    3 => 'e'
]

SynthĂšse

Solver-based Testing

Travaux publiĂ©s dans CSTVA 2013 📖

👉 Mission suivante ? Les chaünes de caractùres !

Ivan

Génération de chaßnes de caractÚres

Avec les tableaux, le type le plus utilisé

Représente des données vastes et complexes :

👉 Moyen rapide et simple de spĂ©cifier des chaĂźnes de caractĂšres (voire des langages) nĂ©cessaire

Grammaires

En 1956, N. Chomsky Ă©tablit une hiĂ©rarchie des langages formels📖 :

  1. grammaires générales (unrestricted grammars)
  2. grammaires contextuelles (context-sensitive grammars)
  3. grammaires algébriques (context-free grammars)
  4. grammaires réguliÚres (regular grammars)

Exemples de


Langage PP

Informellement : grammaire = lexÚmes + rÚgles

LexÚme (token) : plus petite unité lexicale composant un mot (donnée textuelle)

%token namespace_in:name value -> namespace_out

Exemple :

%token number \d+

RÚgle (rule) : décrit l'enchaßnement des lexÚmes les uns par rapport aux autres

  • rule() : pour invoquer une rĂšgle
  • <token> et ::token:: : pour dĂ©clarer un lexĂšme
  • | : pour une disjonction (un choix)
  • (
) : pour grouper
  • e?, e+, e* et e{x,y} pour rĂ©pĂ©ter
  • #node pour crĂ©er un nƓud
  • token[i] pour unifier

Exemple simplifié avec JSON

LexÚmes :

%skip   space          \s
// Scalars.
%token  true           true
%token  false          false
%token  null           null
// Strings.
%token  quote_         "        -> string
%token  string:string  [^"]+
%token  string:_quote  "        -> default
// Objects.
%token  brace_         {
%token _brace          }
// Arrays.
%token  bracket_       \[
%token _bracket        \]
// Rest.
%token  colon          :
%token  comma          ,
%token  number         \d+

RĂšgles :

value:
    <true> | <false> | <null> | <number> | string() | object() | array()

string:
    ::quote_:: <string> ::_quote::

#object:
    ::brace_:: pair() ( ::comma:: pair() )* ::_brace::

#pair:
    string() ::colon:: value()

#array:
    ::bracket_:: value() ( ::comma:: value() )* ::_bracket::

Reconnaissance de mots

Analyseur lexical (lexer) :

$ echo '[1, [1, [2, 3], 5], 8]' | hoa compiler:pp Json.pp 0 --token-sequence
  #  namespace     token name            token value           offset
----------------------------------------------------------------------
  0  default       bracket_              [                          0
  1  default       number                1                          1
  2  default       comma                 ,                          2
  3  default       bracket_              [                          4
  4  default       number                1                          5
  5  default       comma                 ,                          6
  6  default       bracket_              [                          8
  7  default       number                2                          9
  8  default       comma                 ,                         10
  9  default       number                3                         12
 10  default       _bracket              ]                         13
 11  default       comma                 ,                         14
 12  default       number                5                         16
 13  default       _bracket              ]                         17
 14  default       comma                 ,                         18
 15  default       number                8                         20
 16  default       _bracket              ]                         21
 17  default       EOF                                             23

Analyseur syntaxique (parser) :

$ echo '[1, [1, [2, 3], 5], 8]' | hoa compiler:pp Json.pp 0 --visitor dump
>  #array
>  >  token(number, 1)
>  >  #array
>  >  >  token(number, 1)
>  >  >  #array
>  >  >  >  token(number, 2)
>  >  >  >  token(number, 3)
>  >  >  token(number, 5)
>  >  token(number, 8)

Résultat sous forme d'arbre : Abstract Syntax Tree (AST)

Grammar-based Testing

Grammaires culturellement utilisées pour valider

IdĂ©e : les utiliser pour gĂ©nĂ©rer des donnĂ©es 📖

Principe : parcourir/explorer une grammaire et construire une séquence de lexÚmes

Trois algorithmes pour la génération (construction de la séquence)

Un algorithme pour la concrétisation

Algorithmes de génération

GĂ©nĂ©ration alĂ©atoire et uniforme, basĂ© sur les travaux de P. Flajolet📖 :

  • rapide pour des petites donnĂ©es, grande diversitĂ©, taille de la sĂ©quence n fixe
  • prĂ©-calcul exponentiel mais gĂ©nĂ©ration trĂšs rapide

Exemples (générations + concrétisations) avec n=5 :

[null, true]
[[false]]
[-2, "oD"]

GĂ©nĂ©ration exhaustive bornĂ©e : 📖 📖

  • rapide pour des petites donnĂ©es, exhaustivitĂ© (forme de preuve)
  • nombre exponentiel de donnĂ©es

Exemple (génération + concrétisation) avec la borne n=15 :

true
false
null
"y"
{"D/5-": true}
{"b": true, "A_=M": true}
{"A&<": true, "=8o^": false}
{"WA": true, "};2z": null}
{"r": true, "'*uT": "nRx0"}
{"FyD": true, "i>": [true]}



Au total : 356 327 données générées !

Génération basée sur la couverture :

  • rapide pour des donnĂ©es de taille moyenne ou grande, bonne diversitĂ©
  • ne considĂšre pas de taille

Exemple :

false
null
{"axIy#Y": [true, 0.31260e-87060470]}
{"x6: ": "!_`{", "yi*dj": [true], "2q": [true, true, true]}
{"YM!vgx": true, "DP)": true}

Toute la grammaire (lexÚmes et rÚgles) est couverte !

Concrétisation

LexÚmes exprimés avec des PCRE

Analyse les lexĂšmes avec la grammaire des PCRE (au format PP)

Algorithme de génération aléatoire isotropique :

Appliqué sur chaque élément d'une séquence

Julien + démo

SynthĂšse

Grammar-based Testing

Deux nouveaux domaines réalistes : grammar(F) et regex(R) (sucre syntaxique : /R/)

Travaux publiĂ©s dans A-MOST 2012 📖

👉 Mission suivante ? Les objets !

Ivan

Génération d'objets

Objet : aggrégation d'attributs et de méthodes, tous deux annotés par des contrats Praspel

Deux approches pour générer :

  1. appeler le constructeur puis les méthodes pour placer l'objet dans l'état désiré
  2. appeler le constructeur puis définir les attributs de l'objet directement (shunt the encapsulation)

Solution retenue : n°2

Principe

Bonus : détecter les références circulaires et réutiliser des objets déjà générés

SynthĂšse

Un nouveau domaine réaliste : class(C) (sucre syntaxique : \C tout simplement)

Nous savons générer efficacement tous les types de données usuels

👉 Mission suivante ? GĂ©nĂ©rer des tests unitaires !

3 Génération automatique de tests unitaires

Julien

Objectifs de tests

GĂ©nĂ©rer des tests mais
 pour tester quoi ?

Quels sont nos objectifs de tests ?

Couverture du code ?

Couverture des contrats ?

CritĂšre de couvertures sur les contrats

CritĂšre de Couverture de Clause

  • vise Ă  couvrir la structure d'un contrat

CritĂšre de Couverture de Domaine

  • raffine le CritĂšre de Couverture de Clause
  • vise Ă  couvrir tous les domaines rĂ©alistes

CritÚre de Couverture de Prédicat

  • similaire au CritĂšre de Couverture de Domaine
  • vise Ă  couvrir tous les prĂ©dicats (construction \pred(p))

Combinaisons (treillis)

Couverture Domaine Couverture Clause Couverture Prédicat Couverture Clause + Domaine Couverture Clause + Prédicat

Couverture All-G

Couverture All-G

Suite de tests unitaires

  1. choix, par l'utilisateur, d'un critĂšre de couverture de contrat
  2. génération, par l'outil, d'une suite de tests satisfaisants le critÚre

Comment l'exécuter ?

Idée : transformer les suites de tests Praspel en suites écrites avec l'API d'atoum

Pourquoi atoum ?

Les atouts :

Les manques :

4 Extension
atoum/praspel-extension

Fonctionnalités

Nouvelle extension dans atoum atoum/praspel-extension :

  1. exposer les domaines réalistes
  2. exposer les générateurs automatiques de données
  3. transformer des contrats Praspel en suites de tests atoum

Installer :

$ composer require atoum/praspel-extension *
$ composer install
// .atoum.php
$runner->addExtension(new \Atoum\PraspelExtension\Manifest());

Des nouveaux « asserters »

  • realdom pour crĂ©er des disjonctions de domaines rĂ©alistes
$interval = $this->realdom->boundinteger(7, 13)
                          ->or
                          ->boundinteger(42, 153);
  • realdom pour crĂ©er des disjonctions de domaines rĂ©alistes
  • sample pour gĂ©nĂ©rer une donnĂ©e
$interval = $this->realdom->boundinteger(7, 13)
                          ->or
                          ->boundinteger(42, 153);

var_dump(
    $this->sample($interval)
); // int(84)
  • realdom pour crĂ©er des disjonctions de domaines rĂ©alistes
  • sample pour gĂ©nĂ©rer une donnĂ©e
  • sampleMany pour gĂ©nĂ©rer plusieurs donnĂ©es
$interval = $this->realdom->boundinteger(7, 13)
                          ->or
                          ->boundinteger(42, 153);

foreach($this->sampleMany($interval, 1024) as $i)
    $this->integer($i)->isGreaterThan(7);
  • realdom pour crĂ©er des disjonctions de domaines rĂ©alistes
  • sample pour gĂ©nĂ©rer une donnĂ©e
  • sampleMany pour gĂ©nĂ©rer plusieurs donnĂ©es
  • predicate pour vĂ©rifier qu'une donnĂ©e appartient Ă  une disjonction de domaines rĂ©alistes
$interval = $this->realdom->boundinteger(7, 13)
                          ->or
                          ->boundinteger(42, 153);

var_dump($this->predicate(21, $interval)); // boolean(false)
var_dump($this->predicate(42, $interval)); // boolean(true)

Exemples

Générer une chaßne de caractÚres à partir d'une expression réguliÚre :

$realdom = $this->realdom->regex('/[\w\-_]+(\.[\w\-\_]+)*@\w+\.(net|org)/');
$this->string($this->sample($realdom))
     ->contains(
)->
;

Générer des dates relatives :

$realdom = $this->realdom->date(
    'd/m H:i',
    $this->realdom->boundinteger(
        $this->realdom->timestamp('yesterday'),
        $this->realdom->timestamp('next Monday')
    )
);

foreach($this->sampleMany($realdom, 10) as $date)
    var_dump($date);
DĂ©mo

Générer des suites de tests

Classe Project\HelloWorld

Générer la suite de tests :

$ praspel generate --class 'Project\HelloWorld' --test-root .

Exécuter la suite de tests :

$ atoum --files Path/To/Our/Generated/Test.php
> Total test duration: 0.01 second.
> Total test memory usage: 0.50 Mb.
> Code coverage value: 100.00%
> Running duration: 0.06 second.
> Success (1 test, 1/1 method, 0 void method,
           0 skipped method, 1 assertion)!

Exemples dans Hoa

Contient des contrats et des tests manuels

Commandes hoa test:* basées sur les API de l'extension atoum/praspel-extension

Pour générer les tests de la classe Hoa\Math\Util :

$ hoa test:generate --classes Hoa.Math.Util

puis :

$ hoa test:run --namespaces Hoa.Math

Et le tour est joué !

5 Conclusion

Ivan

Recapitulare

Implémenté dans Hoa :

  • un ensemble de bibliothĂšques PHP modulaires, extensibles et structurĂ©es
  • un pont entre le monde de la recherche et de l'industrie
  • gratuit et open-source

Domaines réalistes (Hoa\Realdom) :

  • des structures permettant de valider et gĂ©nĂ©rer des donnĂ©es
  • peuvent ĂȘtre emboĂźtĂ©s et Ă©tendus facilement

Praspel (Hoa\Praspel) :

  • nouveau langage de contrat pour PHP, simple, pragmatique et extensible
  • Contract-based Testing : gĂ©nĂ©ration automatique de donnĂ©es et de suites de tests unitaires
  • Random-, Solver- et Grammar-based Testing (Hoa\Compiler et Hoa\Regex), assemblĂ©s dans Praspel, plongĂ©s dans les domaines rĂ©alistes

Extension atoum (atoum/praspel-extension) :

  • pour utiliser les outils de Praspel dans atoum

Publications

Articles scientifiques en conférences internationales :

Un journal en attente (75% accepté)

ThĂšse soutenue trĂšs prochainement đŸ˜±

Et aprÚs ?

Apprentissage :

  • thĂšse = documentation : besoin d'une documentation « utilisateur »

Générateurs de données :

  • toutes les expĂ©rimentations valident nos algorithmes
  • matures
  • dĂ©jĂ  utilisables, mĂȘme dans atoum

Générateurs de tests unitaires :

  • plusieurs expĂ©rimentations trĂšs encourageantes
  • besoin de maturitĂ©
  • besoin d'amĂ©liorer/complĂ©ter les outils

Merci ! â˜ș

hoa-project.net
@hoaproject