JUnitTest Infected: Programmers Love Writing Tests A little test, a little code, a little test, a...

Post on 03-Apr-2015

144 views 0 download

Transcript of JUnitTest Infected: Programmers Love Writing Tests A little test, a little code, a little test, a...

JUnitTest Infected: Programmers Love Writing Tests

A little test, a little code, a little test, a little code…

Enoncé du problèmePortefeuille de devises

Opérations arithmétiques avec diverses devises

On ne peut pas simplement convertir une devise dans une autre

Il n’existe pas un seul taux de conversionOn peut vouloir comparer la valeur d’un portefeuille au taux d’hier avec celui au taux d’aujourd’hui

Public class Money {

private int fAmount;    

//ISO three letter abbreviation (USD, CHF, etc.). private String fCurrency; 

  

public Money(int amount, String currency){        fAmount= amount;         fCurrency= currency;   

  }     public int amount() {

return fAmount;     }    

public String currency() {         return fCurrency;     }

public Money add(Money m) {      return new Money(amount()+m.amount(),currency()); }

}

Addition d’instance de Money possédant la même devise, le montant de l’instance résultante de Money est la somme des deux montants

public class MoneyTest extends TestCase {     //…     public void testSimpleAdd() {         Money m12CHF= new Money(12, "CHF");  // (1)        Money m14CHF= new Money(14, "CHF");                 Money expected= new Money(26, "CHF");         Money result= m12CHF.add(m14CHF);    // (2)  

assertTrue(expected.equals(result)); // (3)     }}

1. Code qui crée les instance qui vont interagir pour le test. Le contexte du test est généralement appelé fixture.

2. Code qui active les instance de la fixture. 3. Code qui vérifie le résultat.

Deux instance de Money sont considérées égales si elles ont les mêmes montants et devises

public void testEquals() {

  Money m12CHF= new Money(12, "CHF"); Money m14CHF= new Money(14, "CHF");  

assertTrue(!m12CHF.equals(null));   

assertEquals(m12CHF, m12CHF);

   assertEquals(m12CHF, new Money(12, "CHF")); // (1)  

   assertTrue(!m12CHF.equals(m14CHF));}

assertTrueDéclenche un échec qui est loggé par JUnit lorsque l’argument n’est pas vrai.

assertEquals Test pour l’égalité à l’aide de la méthode « equals «  Log les valeurs « textuelles » (printString) de chaque instance si elles diffèrent.

La méthode equals de la classe Money

public boolean equals(Object anObject)

{  

if (anObject instanceof Money){

    Money aMoney= (Money)anObject;

     return aMoney.currency().equals(currency())  

           && amount() == aMoney.amount();

    }

   return false;

}

Common fixtureDuplication de code pour mettre en place les tests. La méthode setUp

Pour réutiliser le code commun de mise en place des tests. • Placer les objets de la fixture dans des variables d’instance de la sous-classe de TestCase

initialize them by overridding the setUp method.

La méthode tearDownOpération symétrique de la méthode setUpLa redéfinir pour nettoyer la fixture à la fin d’un test.

Chaque test s’exécute dans sa propre fixture JUnit invoque setUp et tearDown pour chaque test Pour ne pas avoir d’effet de bord entre les tests

Réécriture des tests – supprimer la duplication de code

public class MoneyTest extends TestCase {

   private Money f12CHF;    private Money f14CHF;

    protected void setUp() {         f12CHF= new Money(12, "CHF");        f14CHF= new Money(14, "CHF");      } public void testEquals() {

assertTrue(!f12CHF.equals(null));     assertEquals(f12CHF, f12CHF);     assertEquals(f12CHF, new Money(12, "CHF"));     assertTrue(!f12CHF.equals(f14CHF)); }

public void testSimpleAdd() {     Money expected= new Money(26, "CHF");     Money result= f12CHF.add(f14CHF);      assertTrue(expected.equals(result)); }

    protected void tearDown() {         f12CHF= null;        f14CHF= null;      }}

Exécution d’un testStatique

Redéfinir la méthode runTest héritée de TestCase Invoquer le test unitaire désiré

TestCase test= new MoneyTest("simple add"){     public void runTest() {

        testSimpleAdd();     } };

DynamiqueUtiliser la réflexivité définie dans la méthode runTest

TestCase test= new MoneyTest("testSimpleAdd");

Création d’une instance de tests

Anonymous inner class

--------------------------public void runTest( ){

testSimpleAdd();

}

fName = «Simple add » 

TestCase

---------------------public void runTest( ){

……

}

runTest()

MoneyTest

------------------------

Création d’une instance de tests

fName = «testSimpleAdd » 

TestCase

-----------------------------------------public void runTest( ){

trouver la méthode de sélecteur fName

exécuter cette méthode

}

runTest()

MoneyTest

------------------------

Test suiteTestSuite est un patron de conception Composite (Design Pattern)

Un TestSuite peut exécuter une collection de tests. TestSuite et TestCase implémentent l’ interface Test qui définit les méthodes permettant d’éxécuter un test. Permet la création de suite de tests en composant arbitrairement les TestCases et les TestSuites.

public static Test suite() {     TestSuite suite= new TestSuite();    suite.addTest(new MoneyTest("testEquals"));    suite.addTest(new MoneyTest("testSimpleAdd"));    return suite; }

Static test suitepublic static Test suite() {    TestSuite suite= new TestSuite();     suite.addTest(new MoneyTest("money equals") {

            protected void runTest() {

testEquals(); }         }     );

suite.addTest( new MoneyTest("simple add") {              protected void runTest() {

testSimpleAdd(); }         }     );return suite;

}

Création dynamique de suites de tests

Transmettre simplement la classe à tester à la classe TestSuite TestSuite extrait les méthodes de tests automatiquement

public static Test suite() {

return new TestSuite(MoneyTest.class);

}

Opérations arithmétiques sur des devises différentesIl n’existe pas un taux de change uniquePour contourner ce problème, introduction de la classe MoneyBag qui diffère les conversions en fonction des taux de change.

class MoneyBag {     private Vector fMonies= new Vector();

    MoneyBag(Money m1, Money m2) {         appendMoney(m1);         appendMoney(m2);    

}     MoneyBag(Money bag[]) {

        for (int i= 0; i < bag.length; i++)             appendMoney(bag[i]);  

   }}

appendMoney Ajoute une instance de Money à la liste des Moneys S’occupe de consolider les Moneys possédant la même devise.

Tester MoneyBag

protected void setUp() {     f12CHF= new Money(12, "CHF");     f14CHF= new Money(14, "CHF");     f7USD=  new Money( 7, "USD");     f21USD= new Money(21, "USD");     fMB1= new MoneyBag(f12CHF, f7USD);     fMB2= new MoneyBag(f14CHF, f21USD); }

public void testBagEquals() {     assertTrue(!fMB1.equals(null));     assertEquals(fMB1, fMB1);     assertTrue(!fMB1.equals(f12CHF));    assertTrue(!f12CHF.equals(fMB1));    assertTrue(!fMB1.equals(fMB2)); }

public Money add(Money m) {     if (m.currency().equals(currency()) )         return new Money( amount()+m.amount(),

currency());     return new MoneyBag(this, m); }

Il existe maintenant deux représentations pour les Moneys : Money et MoneyBag

Les cacher au niveau du code client. Introduire une interface IMoney implémentée par les deux représentations.

interface IMoney {     public abstract IMoney add(IMoney aMoney);    

//… }

Tests de l’addition de IMoneypublic void testMixedSimpleAdd() { 

    // [12 CHF] + [7 USD] == {[12 CHF][7 USD]} 

    Money bag[]= { f12CHF, f7USD }; 

    MoneyBag expected= new MoneyBag(bag);   

   assertEquals(expected, f12CHF.add(f7USD)); 

}

Les autres tests suivent le même patron:

testBagSimpleAdd – addition d’un MoneyBag à un Money simple

testSimpleBagAdd - addition d’un Money simple à un MoneyBag

testBagBagAdd – addition de deux MoneyBags

Suite de tests pour MoneyBagpublic static Test suite() {

    TestSuite suite= new TestSuite();

    suite.addTest(new MoneyTest("testMoneyEquals"));

    suite.addTest(new MoneyTest("testBagEquals"));

    suite.addTest(new MoneyTest("testSimpleAdd"));

    suite.addTest(new MoneyTest("testMixedSimpleAdd"));

    suite.addTest(new MoneyTest("testBagSimpleAdd"));  

   suite.addTest(new MoneyTest("testSimpleBagAdd"));

    suite.addTest(new MoneyTest("testBagBagAdd"));

   return suite;

}

Implémentation de l’additionLe défi de cette implémentation est de gérer les diverses combinaisons de Money et MoneyBag.

Le patron de conception « Double dispatch » solution élégante à ce problème (Visitor DP). L’idée derrière le double dispatch est d’utiliser un appel supplémentaire pour découvrir le type des arguments impliqués. Transmettre au paramètre un message dont le (nouveau) nom est

• le nom de la méthode originale • suivi du nom de la classe du destinataire original.

class Money implements IMoney {     public IMoney add(IMoney m) {         return m.addMoney(this);    

}     //… }class MoneyBag implements IMoney {     public IMoney add(IMoney m) {         return m.addMoneyBag(this);     }    

//… }

Implémentation du double dispatch dans Money

class Money implements IMoney {

  public IMoney add(IMoney m) {       return m.addMoney(this);     }

public IMoney addMoney(Money m) {  if (m.currency().equals(currency()) )     

return new Money( amount()+m.amount(), currency());  

return new MoneyBag(this, m); }

public IMoney addMoneyBag(MoneyBag s) {     return s.addMoney(this); }}

Implémentation du double dispatch dans MoneyBag

class MoneyBag implements IMoney {

    public IMoney add(IMoney m) {

        return m.addMoneyBag(this);

    }   

 

public IMoney addMoney(Money m) {   

  return new MoneyBag(m, this); }

public IMoney addMoneyBag(MoneyBag s) {  

   return new MoneyBag(s, this);

}

}

Test de la simplification de MoneyBag

public void testSimplify() {   

  // {[12 CHF][7 USD]} + [-12 CHF] == [7 USD]    

Money expected= new Money(7, "USD");

   assertEquals( expected,

fMB1.add(new Money(-12, "CHF")));

}

// … Test fails

Implémentation de la simplification dans MoneyBag

class MoneyBag implements IMoney {     public IMoney add(IMoney m) {         return m.addMoneyBag(this);     }   

public IMoney addMoney(Money m) {      return new MoneyBag(m, this).simplify(); }

public IMoney addMoneyBag(MoneyBag s) {      return new MoneyBag(s, this).simplify(); }

private IMoney simplify() {     if (fMonies.size() == 1)          return (IMoney)fMonies.firstElement();     return this; } }

ConclusionUn peu de tests, un peu de code, un peu de tests, un peu de code…

Capturer l’intention dans les tests

Le code des tests est comme le code du modèleIl fonctionne mieux s’il est bien factorisé.

Garder les vieux tests fonctionnels est aussi important que d’écrire de nouveaux tests qui fonctionnent.

Lorsque l’on a envie d’écrire un énoncé « print » ou une instruction de déverminage, écrire un test à la place

JUnit : une manière de tester qui demande un faible investissementPlus rapide, plus productif, plus prévisible et moins de stress.

Il devient possible de refactoriser plus agressivement une fois que les tests sont disponibles.

Interface commune pour les classes Money et MoneyBag

interface IMoney {public abstract IMoney add(IMoney m);

/** * implementing double dispatch */IMoney addMoney(Money m);IMoney addMoneyBag(MoneyBag s);

public abstract boolean isNull();public abstract IMoney multiply(int factor);public abstract IMoney negate();public abstract IMoney subtract(IMoney m);

}

Références

Erich Gamma and Kent Beck, JUnitTest Infected: Programmers Love Writing Tests, Java Report, July 1998, Volume 3, Number 7

http://junit.sourceforge.net/doc/testinfected/testing.htm