Exemples en C# pour 'Les simulacres ne sont pas des bouchons'Date de publication : 9/9/2007 , Date de mise à jour : 24/10/2007
Par
Bruno Orsier (Site Web de Bruno Orsier)
Le code source du projet peut être téléchargé
ici
Les tests traditionnels
Réalisation détaillée
Code final
Exemple avec objet simulacre
Mise en place
Code final
Exemple avec TypeMock
Mise en place
Code final
Remerciements
Les tests traditionnels
Réalisation détaillée
Ici nous appliquons directement ce qui a déjà été vu dans le tutoriel sur les
pentaminos, et mettons donc en place un projet Visual C# Express avec Nunit,
comme indiqué précédemment.
Puis nous nous laissons guider par les tests, ce qui est facile puisqu'ils sont
déjà écrits en Java. Nous obtenons ainsi la classe de test suivante :
[TestFixture]
public class TestsSelonApprocheTraditionnelle
{
private static String TALISKER = " Talisker " ;
private static String HIGHLAND_PARK = " Highland Park " ;
private Warehouse warehouse;
[SetUp]
public void setUp ()
{
warehouse = new Warehouse () ;
warehouse.add (TALISKER, 50 );
warehouse.add (HIGHLAND_PARK, 25 );
}
[Test]
public void testOrderIsFilledIfEnoughInWarehouse ()
{
Order order = new Order (TALISKER, 50 );
order.fill (warehouse);
Assert.IsTrue (order.isFilled ());
Assert.AreEqual (0 , warehouse.getInventory (TALISKER));
}
}
|
Pour parvenir à compiler, ces tests nous conduisent
au code suivant :
public class Warehouse
{
public void add (string itemIdentification, int orderAmount)
{
}
public int getInventory (string itemIdentification)
{
return - 1 ;
}
}
public class Order
{
public Order (string itemIdentification, int orderAmount)
{
}
public void fill (Warehouse warehouse)
{
}
public Boolean isFilled ()
{
return false ;
}
}
|
Et de plus nous obtenons la barre rouge :
si bien qu'il ne reste plus qu'à écrire le code suivant (implémentation évidente) pour passer au vert :
public class Warehouse
{
private Hashtable AvailableItems = new Hashtable ();
public void add (string itemIdentification, int orderAmount)
{
AvailableItems.Add (itemIdentification, orderAmount);
}
public void remove (string itemIdentification, int orderAmount)
{
AvailableItems[itemIdentification] = (int ) AvailableItems[itemIdentification] - orderAmount;
}
public int getInventory (string itemIdentification)
{
return (int ) AvailableItems[itemIdentification]; ;
}
}
public class Order
{
private string Identification;
private int Amount;
private Boolean Filled;
public Order (string itemIdentification, int orderAmount)
{
Identification = itemIdentification;
Amount = orderAmount;
}
public void fill (Warehouse warehouse)
{
warehouse.remove (Identification, Amount);
}
public Boolean isFilled ()
{
return true ;
}
}
|
C'est une implémentation minimum pour passer au vert : par exemple il n'y a pas
de gestion d'exception, et c'est donc une implémentation très optimiste. Mais
dans une optique TDD, aucun test ne nous conduit à une telle gestion, donc
nous n'écrivons pas de tel code. Par contre nous n'avons pas encore programmé
toute la logique de gestion de l'entrepôt, et pour cela il nous faut le
deuxième test :
[Test]
public void testOrderDoesNotRemoveIfNotEnough ()
{
Order order = new Order (TALISKER, 51 );
order.fill (warehouse);
Assert.IsFalse (order.isFilled ());
Assert.AreEqual (50 , warehouse.getInventory (TALISKER));
}
|
Ce code compile, et nous obtenons la barre rouge comme prévu. Pour avoir la barre verte
il faut principalement modifier la méthode fill :
public void fill (Warehouse warehouse)
{
if (warehouse.getInventory (Identification) >= Amount)
{
warehouse.remove (Identification, Amount);
Filled = true ;
}
}
|
Code final
Voici le code complet ainsi obtenu. Tout d'abord le code de test :
[TestFixture]
public class TestsSelonApprocheTraditionnelle
{
private static String TALISKER = " Talisker " ;
private static String HIGHLAND_PARK = " Highland Park " ;
private Warehouse warehouse;
[SetUp]
public void setUp ()
{
warehouse = new Warehouse () ;
warehouse.add (TALISKER, 50 );
warehouse.add (HIGHLAND_PARK, 25 );
}
[Test]
public void testOrderIsFilledIfEnoughInWarehouse ()
{
Order order = new Order (TALISKER, 50 );
order.fill (warehouse);
Assert.IsTrue (order.isFilled ());
Assert.AreEqual (0 , warehouse.getInventory (TALISKER));
}
[Test]
public void testOrderDoesNotRemoveIfNotEnough ()
{
Order order = new Order (TALISKER, 51 );
order.fill (warehouse);
Assert.IsFalse (order.isFilled ());
Assert.AreEqual (50 , warehouse.getInventory (TALISKER));
}
}
|
Puis le code nécessaire à l'exécution des tests :
public class Warehouse
{
private Hashtable AvailableItems = new Hashtable ();
public void add (string itemIdentification, int orderAmount)
{
AvailableItems.Add (itemIdentification, orderAmount);
}
public void remove (string itemIdentification, int orderAmount)
{
AvailableItems[itemIdentification] = (int ) AvailableItems[itemIdentification] - orderAmount;
}
public int getInventory (string itemIdentification)
{
return (int ) AvailableItems[itemIdentification]; ;
}
}
public class Order
{
private string Identification;
private int Amount;
private Boolean Filled;
public Order (string itemIdentification, int orderAmount)
{
Identification = itemIdentification;
Amount = orderAmount;
Filled = false ;
}
public void fill (Warehouse warehouse)
{
if (warehouse.getInventory (Identification) >= Amount)
{
warehouse.remove (Identification, Amount);
Filled = true ;
}
}
public Boolean isFilled ()
{
return Filled;
}
}
|
Exemple avec objet simulacre
Mise en place
Pour réaliser cet exemple, nous avons besoin d'un outil permettant de
définir automatiquement nos objets simulacres (il est bien sûr possible
de les définir manuellement, sans outil, mais cela est assez fastidieux).
Nous allons utiliser ici NMock version 2,
car elle est inspirée de jMock que Martin Fowler utilise dans son exemple.
Après l'avoir téléchargée (prendre la version pour .net 2.0) et installée
(pour la version Release Candidate 1 utilisée ici, il n'y a pas d'installateur
et il faut copier le dossier bin à un endroit approprié, par exemple
C:\program files\nmock-2.0.0.RC1-net-2.0), il faut ajouter la référence NMock2
au projet Visual C# Express :
et penser à importer l'espace de noms correspondant dans les
fichiers sources :
L'utilisation de NMock2 va ensuite nous obliger à définir une interface, ce qui
est assez normal car il nous faut un mécanisme pour pouvoir utiliser à la fois
notre véritable objet (en production) et notre simulacre (en test). Donc voici
notre interface :
public interface IWarehouse
{
void add (string itemIdentification, int orderAmount);
void remove (string itemIdentification, int orderAmount);
int getInventory (string itemIdentification);
}
public class Warehouse : IWarehouse
{
...
}
|
Le test Java se transcrit alors assez directement en C#, avec toutefois
deux particularités. D'une part pour exprimer l'ordre dans lequel nous
attendons les méthodes il faut utiliser using(mocks.Ordered) { ... } ;
d'autre part la création de l'objet simulacre est un peu différente car
il faut utiliser cette fabrique de simulacres appellée Mockery :
[Test]
public void testFillingRemovesInventoryIfInStock ()
{
Order order = new Order (TALISKER, 50 );
Mockery mocks = new Mockery ();
IWarehouse warehouseMock = mocks.NewMock< IWarehouse> ();
using (mocks.Ordered)
{
Expect.Once.On (warehouseMock)
.Method (" hasInventory " )
.With (TALISKER,50 )
.Will (Return.Value (true ));
Expect.Once.On (warehouseMock)
.Method (" remove " )
.With (TALISKER, 50 );
}
order.fill (warehouseMock);
mocks.VerifyAllExpectationsHaveBeenMet ();
}
}
|
Le test échoue toutefois, car nous n'avons pas défini
de telle méthode hasInventory (aucun test ne nous
avait conduit à réaliser une telle méthode jusqu'à présent) :
MocksArentStubs.OrderInteractionTester.testFillingRemovesInventoryIfInStock :
System.ArgumentException :
mock object warehouse does not have a method matching hasInventory
|
Définir cette méthode ne suffit toujours pas, nous échouons cette
fois pour une autre raison, plus intéressante car justement le
test montre que nous n'avons pas encore utilisé cette méthode :
MocksArentStubs.OrderInteractionTester.testFillingRemovesInventoryIfInStock :
NMock2.Internal.ExpectationException :
unexpected invocation of warehouse.remove("Talisker", <50>)
Expected:
Ordered:
1 time: warehouse.hasInventory(equal to "Talisker", equal to <50>), will return <True> [called 0 times]
1 time: warehouse.remove(equal to "Talisker", equal to <50>) [called 0 times]
|
Il reste en effet à modifier la méthode fill pour qu'elle utilise
hasInventory :
public void fill (IWarehouse warehouse)
{
if (warehouse.hasInventory (Identification,Amount))
{
warehouse.remove (Identification, Amount);
Filled = true ;
}
}
|
Cela permet d'obtenir la barre verte. Comme NMock2 possède une méthode
WithAnyArguments, le deuxième test est facile à écrire,
et est présenté ci-dessous.
Code final
Voici le code complet correspondant à l'exemple de Martin Fowler :
[TestFixture]
public class OrderInteractionTester
{
private static String TALISKER = " Talisker " ;
[Test]
public void testFillingRemovesInventoryIfInStock ()
{
Order order = new Order (TALISKER, 50 );
Mockery mocks = new Mockery ();
IWarehouse warehouseMock = mocks.NewMock< IWarehouse> ();
using (mocks.Ordered)
{
Expect.Once.On (warehouseMock)
.Method (" hasInventory " )
.With (TALISKER,50 )
.Will (Return.Value (true ));
Expect.Once.On (warehouseMock)
.Method (" remove " )
.With (TALISKER, 50 );
}
order.fill (warehouseMock);
mocks.VerifyAllExpectationsHaveBeenMet ();
}
[Test]
public void testFillingDoesNotRemoveIfNotEnoughInStock ()
{
Order order = new Order (TALISKER, 50 );
Mockery mocks = new Mockery ();
IWarehouse warehouseMock = mocks.NewMock< IWarehouse> ();
Expect.Once.On (warehouseMock)
.Method (" hasInventory " )
.WithAnyArguments ()
.Will (Return.Value (false ));
order.fill (warehouseMock);
mocks.VerifyAllExpectationsHaveBeenMet ();
}
|
Exemple avec TypeMock
Ici j'ai choisi de ne pas utiliser easymock.net car
son développement semble être arrêté. A la place j'utilise TypeMock.NET,
qui offre une version gratuite ainsi qu'une version commerciale. TypeMock va nous permettre
d'illustrer la métaphore enregistrer/rejouer que Martin Fowler avait illustrée avec EasyMock.
Mise en place
Après avoir téléchargé et installé TypeMock, il suffit d'ajouter la référence au projet
(sélectionner TypeMock.NET for .NET 2.0 dans la liste des références), et d'inclure
l'espace de noms correspondant dans le fichier source :
Le code de test est alors particulièrement simple et direct à écrire, puisqu'on indique
directement les appels souhaités pour définir les attentes :
[Test]
public void testFillingRemovesInventoryIfInStock ()
{
Order order = new Order (TALISKER, 50 );
Warehouse warehouseMock = new Warehouse ();
using (RecordExpectations recorder = RecorderManager.StartRecording ())
{
warehouseMock.hasInventory (TALISKER, 50 );
recorder.Return (true );
warehouseMock.remove (TALISKER, 50 );
}
order.fill (warehouseMock);
MockManager.Verify ();
}
|
Le code est même beaucoup plus simple qu'avec EasyMock car il n'est pas nécessaire
de manipuler des objets "contrôle".
 |
Attention, on ne peut pas lancer NUnit directement, il faut le lancer
par l'intermédiaire d'un programme fourni par TypeMock, comme suit (à
taper en ligne de commande) :
|
"C:\Program Files\TypeMock\TypeMock.NET\TMockRunner.exe" "C:\Program Files\NUnit 2.4.3\bin\nunit.exe"
|
On constate alors que nous obtenons la barre verte pour tous les tests que nous avons écrits
jusque là :
Code final
Voici le code de test final :
[TestFixture]
public class OrderEasyTester
{
private static String TALISKER = " Talisker " ;
[Test]
public void testFillingRemovesInventoryIfInStock ()
{
Order order = new Order (TALISKER, 50 );
Warehouse warehouseMock = new Warehouse ();
using (RecordExpectations recorder = RecorderManager.StartRecording ())
{
warehouseMock.hasInventory (TALISKER, 50 );
recorder.Return (true );
warehouseMock.remove (TALISKER, 50 );
}
order.fill (warehouseMock);
MockManager.Verify ();
}
}
|
Remerciements
Merci à Alp pour sa relecture de cet article.
Ce document est issu de http://www.developpez.com et reste la propriété exclusive de son auteur.
La copie, modification et/ou distribution par quelque moyen que ce soit est soumise à l'obtention préalable de l'autorisation de l'auteur.
|