Nouveau scénario

La première version de notre logiciel étant livrée, nous allons attaquer la deuxième en rajoutant un scénario de sélection et écoute de podcast. Nous sommes face au premier vrai défi. Le secret de l'agilité, c'est d'écrire du code facile à modifier.
Ce nouveau scénario va nous donner l'occasion de vérifier cette affirmation et de voir, du moins l'espère-t-on, évoluer notre conception en conséquence.

Que nous dit le 2ème scénario ?
"L'utilisateur sélectionne un élément de podcast dans la liste et l'écoute."
Au premier abord, cette description est un peu courte. Pour la sélection, pas de problème étant donné que nos élements sont présentés sous une forme de liste. Pour l'écoute, de multiples options sont possibles :

  • possibilité d'interrompre l'écoute en cours
  • possibilité de reprendre l'écoute interrompue
  • affichage de la progression de l'écoute (temps passé, temps restant, ...)
  • ...

Notre client peut avoir une idée arrêtée sur la question tout comme il peut être plus indécis et penser "cela dépend de ce que couterait chacune de ces fonctionnalités". Nous n'allons pas entrer ici dans des considérations de planification agile, mais il est essentiel, au delà de l'exercice de développement itératif, de garder à l'esprit que, en amont, se posent de véritables problèmes de gestion de projet qui sont autant de raisons de tirer parti d'une évolution par petites étapes.

En l'occurrence, nous allons supposer que le client ne sait pas encore très bien ce qu'il veut comme comportement détaillé. Nous allons donc commencer par réaliser ce scénario dans sa forme la plus simple possible : l'utilisateur sélectionne un élément, aussitôt l'écoute débute et elle se termine lorsque le morceau est terminé ou lorsque un nouvel élément est sélectionné (auquel cas le morceau nouvellement sélectionné est joué).
On introduit donc ici une nouvelle interaction externe pour notre application : la possibilité de jouer un élément audio.
Pour ce qui est de la logique applicative, par laquelle nous allons, comme toujours, commencer, nous pouvons continuer avec le même composant de gestion de souscription. En effet, c'est lui qui possède la liste de nos éléments, il est donc bien placé pour lancer une écoute sur reception d'un évènement de sélection.

En première approche, nous pouvons modéliser le scénario de la façon suivante : S2a.png

Nous écrivons un test correspondant à ce scénario. On veillera à conserver toutes les déclarations d'évènements précedemment définies. En effet, même si ces évènements n'entrent pas en jeu dans notre scénario, les souscriptions sont présentes dans PodcastSubscription que l'on est en train de tester. C'est la principale contrainte d'un TDD à base de mocks par rapport à une pratique plus "classique" à base de stubs.

[SetUp]
public void SetUp()
{
  mocks = new Mockery();

  mockView = mocks.NewMock<IPodcastSubscriptionView>();
  selectItemEvent = new MockEvent();
  Stub.On(mockView).EventAdd("NewSource", Is.Anything);
  Stub.On(mockView).EventAdd("SelectItem", Is.Anything).Will(MockEvent.Hookup(selectItemEvent));
  PodcastItem selectedItem = new PodcastItem("title2","http://someserver/podcast2.mp3");
  Stub.On(mockView).GetProperty("SelectedItem").Will(Return.Value(selectedItem));

  mockFetcher = mocks.NewMock<IXmlFetcher>();
  Stub.On(mockFetcher).EventAdd("Retrieved", Is.Anything);
  Stub.On(mockFetcher).EventAdd("FailedToRetrieve", Is.Anything);

  mockAudio = mocks.NewMock<IAudioPlayer>();

  subscription = new PodcastSubscription(mockView,mockFetcher,mockAudio);
}

[Test]
public void SelectItemAndPlayAudio()
{
  Expect.Once.On(mockAudio).Method("Play").With("http://someserver/podcast2.mp3");
  selectItemEvent.Raise();
  mocks.VerifyAllExpectationsHaveBeenMet();
}

Première erreur de compilation : l'interface IAudioPlayer n'existe pas. Rajoutons-là.

public interface IAudioPlayer
{
}

Deuxième erreur de compilation : du fait de l'ajout de l'audio, le constructeur de PodcastSubscription doit évoluer. Faisons le évoluer.

public PodcastSubscription(IPodcastSubscriptionView view,IXmlFetcher fetcher,IAudioPlayer audio)

Du coup, il faut rajouter ce paramètre dans nos tests précédents qui, eux aussi, ne compilent plus. Grace à NMock, cela se fait sans grande modification sur le code existant :

subscription = new PodcastSubscription(mockView,mockFetcher,mocks.NewMock<IAudioPlayer>());

Notre programme principal rencontre également la même erreur. A ce niveau, 3 solutions se présentent :

  • Mettre le programme principal dans un projet séparé dont on ne se préoccupera que lorsque les tests unitaires sur les composants seront terminés.
  • Créer d'ores et déjà un composant qui implémente IAudioPlayer pour notre programme principal.
  • Utiliser un mock.

Je n'aime pas la première option car elle ne fait que repousser les problèmes. Si plusieurs modifications interviennent, je passerai certainement beaucoup (trop) de temps sur l'intégration à venir.
Je n'aime pas non plus la 2ème option car elle va m'obliger à modifier le composant en question au fur et à mesure des évolutions de l'interface. Certains seraient tentés de dire "mais c'est très bien, de toutes façons, il le faudra ce composant". Moi je réponds que si je me mets à tout modifier en même temps, je ne fais plus vraiment de test unitaire, ce qui en soi n'est pas forcément grave, mais, surtout, j'augmente mes risques de ne plus penser de manière suffisamment découplée et de diminuer la puissance de mes interfaces. Cela serait mettre une hypothèque sur mon agilité future.
Je prends donc la 3ème option :

public MainForm()
{
  //...
  NMock2.Mockery mocks = new NMock2.Mockery();
  subscription_ = new PodcastSubscription(
    TheSubscriptionView,
    new XmlFetcher(),
    mocks.NewMock<IAudioPlayer>()
  );
}

Les problèmes de compilation étant résolus, passons à l'exécution du test.
Notre test déclare un évènement SelectItem sur IPodcastSubscriptionView qui n'existe pas encore. Il faut le rajouter. Dans la foulée, il faut aussi le rajouter dans PodcastSubscriptionView.
Il faut faire de même avec la propriété SelectedItem.

L'erreur suivante se produit sur nos rajouts de PodcastItem pour simuler un PodcastSubscription déjà initialisé. On se rend compte que la liste d'éléments n'est actuellement pas initialisée lors du constructeur de PodcastSubscription mais seulement lors de la réception d'un évènement Retrieved. Ce n'est pas un problème : rajoutons une initialisation dans le constructeur.

La vérification suivante porte sur l'existence d'une méthode Play de IAudioPlayer : il faut déclarer cette méthode.

public interface IAudioPlayer
{
  void Play(string audioUrl);
}

Et enfin, la dernière erreur porte sur l'appel de cette méthode. En rajoutant le traitement d'évènement qui va bien, le test réussit.

public PodcastSubscription(IPodcastSubscriptionView view,IXmlFetcher fetcher,IAudioPlayer audio)
{
  audio_ = audio;
  view_.SelectItem += delegate
  {
    audio_.Play(view_.SelectedItem.URL);
  };
}

Mais... Nos précédents tests échouent !
En fait, l'abonnement à l'évènement SelectItem n'était pas prévu dans nos précédents tests. Il suffit de le rajouter pour tenir compte de la nouvelle réalité.

Nous avons donc réussi à faire fonctionner la logique applicative de notre deuxième scénario. On pourrait croire que l'on n'a pas fait grand chose : rajouter un handler d'évènement qui appelle une méthode, la belle affaire... En fait, nous avons fait bien plus que cela :

  • Nous avons pu faire évoluer notre conception existante pour y rajouter une logique applicative non prévue initialement.
  • La partie manquante du scénario a été repoussée dans un composant d'interaction externe avec le système audio dont le développement est entièrement découplé du coeur de l'application.
  • Protégés par nos tests unitaires, nous avons fait tout cela en conservant les fonctionnalités existantes.

D2.png

La prochaine fois, nous essaierons de livrer la 2ème version de notre logiciel.