JUnitTest Infected: Programmers Love Writing Tests A little test, a little code, a little test, a little code…
Enoncé du problème Portefeuille de devises Opérations arithmétiques avec diverses devises On ne peut pas simplement convertir une devise dans une autre Il nexiste pas un seul taux de conversion On peut vouloir comparer la valeur dun portefeuille au taux dhier avec celui au taux daujourdhui
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 dinstance de Money possédant la même devise, le montant de linstance 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.Code 2.Code qui active les instance de la fixture.Code 3.Code qui vérifie le résultat.Code
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)); } assertTrue Déclenche un échec qui est loggé par JUnit lorsque largument nest pas vrai. assertEquals Test pour légalité à laide 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 fixture Duplication 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 dinstance de la sous- classe de TestCase initialize them by overridding the setUp method. La méthode tearDown Opération symétrique de la méthode setUp La redéfinir pour nettoyer la fixture à la fin dun test. Chaque test sexécute dans sa propre fixture JUnit invoque setUp et tearDown pour chaque test Pour ne pas avoir deffet 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 dun test Statique 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(); } }; Dynamique Utiliser la réflexivité définie dans la méthode runTest TestCase test= new MoneyTest("testSimpleAdd");
Création dune instance de tests Anonymous inner class public void runTest( ){ testSimpleAdd(); } fName = «Simple add » TestCase public void runTest( ){ …… } runTest() MoneyTest
Création dune 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 suite TestSuite 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 suite public 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érentes Il nexiste pas un taux de change unique Pour 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 Money s Soccupe de consolider les Money s 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 laddition de IMoney public 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 dun MoneyBag à un Money simple testSimpleBagAdd - addition dun Money simple à un MoneyBag testBagBagAdd – addition de deux MoneyBags
Suite de tests pour MoneyBag public 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 laddition Le 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). Lidée derrière le double dispatch est dutiliser 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; }
Conclusion Un peu de tests, un peu de code, un peu de tests, un peu de code… Capturer lintention dans les tests Le code des tests est comme le code du modèle Il fonctionne mieux sil est bien factorisé. Garder les vieux tests fonctionnels est aussi important que décrire de nouveaux tests qui fonctionnent. Lorsque lon 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 investissement Plus 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 testing.htm