Persistance

Depuis notre précédente livraison, notre logiciel peut être considéré comme acceptable pour une utilisation basique : lister le contenu d'un podcast audio, sélectionner un morceau et l'écouter. C'est là une des forces de l'agilité : fournir rapidement un système ayant une réelle utilité tout en garantissant que ce système va pouvoir aisément évoluer par rajout (sans douleur) de nouvelle fonctionnalités.
C'est ce que nous allons faire aujourd'hui pour continuer notre aventure de conception émergente. Le problème récurrent de notre utilisateur est qu'il doive sans cesse saisir l'URL de son podcast à chaque lancement du logiciel. Cela devient vite laborieux et son besoin le plus prioritaire est donc que le logiciel mémorise cette URL.

Première étape : décrire le scénario souhaité. Je ne vais même pas prendre la peine de le représenter graphiquement tellement il est simple :

  1. lors de la saisie d'une URL, sa valeur est envoyée à un composant qui la mémorise de manière persistante
  2. lors du lancement du logiciel, si une valeur d'URL est disponible dans le composant de mémorisation persistante, le podcast correspondant est souscrit comme s'il avait été saisi par l'utilisateur.

En ce qui concerne l'étape 1, elle est plus une étape à rajouter dans un scénario existant, celui ou l'utilisateur saisit une URL, qu'un scénario à part entière. Insérons-là donc dans TestSelectSourceAndRetrieveContent.MainScenario en déclarant l'appel à nouveau composant d'interaction externe, celui qui gère la persistance de notre URL :

[Test]
public void MainScenario()
{
  Expect.Once.On(mockSettings).SetProperty("Source").To("http://someserver/podcast.content.xml");
  Expect.Once.On(mockFetcher).SetProperty("Source").To("http://someserver/podcast.content.xml");
  Expect.Once.On(mockView).SetProperty("Items").To(Is.NotNull);
  newSourceEvent.Raise();
  //...
}

Ce composant, matérialisé dans le test par mockSettings doit être déclaré dans l'initialisation de notre test :

protected IApplicationSettings mockSettings;
//...
mockSettings = mocks.NewMock<IApplicationSettings>();

Pour cette première étape, l'interface IApplicationSettings est relativement simple :

public interface IApplicationSettings
{
  string Source { set; }
}

Le test échoue mais il n'en faut pas beaucoup pour le faire fonctionner. On rajoute l'appel à la sauvegarde dans l'évènement de saisie de l'URL :

public PodcastSubscription(IPodcastSubscriptionView view,IXmlFetcher fetcher,IAudioPlayer audio,IApplicationSettings settings)
{
  //...
  view_.NewSource += delegate
  {
    settings_.Source = view_.Source;
    fetcher_.Source = view_.Source;
  };
  //...
}

Le test réussit... mais son voisin échoue. Dans le scénario TestSelectSourceAndRetrieveContent.FailedToRetrieve, la sauvegarde est également réalisée alors que l'on ne l'attendait pas.

On commence à entrevoir ici les bienfaits du développement dirigé par les tests. Ce test qui échoue pose une question importante quant au comportement de l'application : lorsque la récupération du XML échoue, faut-il tout de même sauvegarder l'URL saisie ?
Seul notre utilisateur peut faire ce choix. En l'occurrence, il décide que la sauvegarde de l'URL n'a d'intérêt que lorsque l'on a effectivement quelque chose à écouter.

Pour nos tests, cela signifie qu'ils modélisent correctement le comportement attendu pour l'application : sauvegarde de l'URL quand la récupération des données XML réussit, pas de sauvegarde quand elle échoue.
Il n'en reste pas moins que nos tests, eux, échouent encore. Il nous faut modifier le code de telle sorte que TestSelectSourceAndRetrieveContent.FailedToRetrieve réussisse sans pour autant faire échouer TestSelectSourceAndRetrieveContent.MainScenario qui réussit déjà.
Pour cela, nous réalisons la sauvegarde de l'URL non pas lors de l'évènement de saisie mais lors de la récupération du XML :

public PodcastSubscription(IPodcastSubscriptionView view,IXmlFetcher fetcher,IAudioPlayer audio,IApplicationSettings settings)
{
  //...
  fetcher_.Retrieved += delegate
  {
    settings_.Source = view_.Source;
    ParseXMLResponseIntoItemList(fetcher_.Response);
    view_.Items = items_;
  };
  //...
}

A présent, tout irait presque pour le mieux s'il n'y avait notre TestSelectItemAndPlay récemment défini.
Même s'il n'a pas de rapport direct avec la sauvegarde/récupération de l'URL, les modifications visant à introduire IApplicationSettings ont un impact non négligeable : PodcastSubscription n'est plus correctement initialisé.

On touche là un point sensible de l'urbanisation des tests. Quand une même classe est testée dans plusieurs TestFixtures, on a un problème d'initialisation du contexte de test.
Plusieurs solutions s'offrent à nous. On pourrait rassembler tous les tests dans le même TestFixture. On n'y gagnerait pas forcément en clarté, surtout que tous les tests en question n'ont pas besoin de contextes strictement identiques. TestSelectItemAndPlay, par exemple, se place dans un contexte où un PodcastItem est déja sélectionné.

Une autre solution est de mettre en commun la partie d'initialisation partagée. Pour cela on encapsule cette initialisation partagée dans une méthode dédiée
Pour une vision plus globable des diverses stratégies d'urbanisation, on peut se référer au catalogue de patterns de Gerard Meszaros qui, entre autres, décrit diverses possibilités pour organiser des TestFixtures. Dans le cas qui nous intéresse, nous sommes en présence d'une Creation Method.

Où allons nous mettre cette méthode ? Une possibilité est de définir une classe de base pour tous les TestFixtures portant sur la classe PodcastSubscription :

public class DeclareTestMocks
{
  protected Mockery mocks;
  protected IPodcastSubscriptionView mockView;
  protected MockEvent newSourceEvent, selectItemEvent;
  protected IXmlFetcher mockFetcher;
  protected MockEvent retrievedEvent, failedToRetrieveEvent;
  protected IAudioPlayer mockAudio;
  protected IApplicationSettings mockSettings;
  protected MockEvent loadSourceEvent;
  
  protected void SetUpMocks()
  {
    mocks = new Mockery();
    
    mockView = mocks.NewMock<IPodcastSubscriptionView>();
    newSourceEvent = new MockEvent();
    Stub.On(mockView).EventAdd("NewSource", Is.Anything).Will(MockEvent.Hookup(newSourceEvent));
    selectItemEvent = new MockEvent();
    Stub.On(mockView).EventAdd("SelectItem", Is.Anything).Will(MockEvent.Hookup(selectItemEvent));

    mockFetcher = mocks.NewMock<IXmlFetcher>();
    retrievedEvent = new MockEvent();
    Stub.On(mockFetcher).EventAdd("Retrieved", Is.Anything).Will(MockEvent.Hookup(retrievedEvent));
    failedToRetrieveEvent = new MockEvent();
    Stub.On(mockFetcher).EventAdd("FailedToRetrieve", Is.Anything).Will(MockEvent.Hookup(failedToRetrieveEvent));

    mockAudio = mocks.NewMock<IAudioPlayer>();

    mockSettings = mocks.NewMock<IApplicationSettings>();
    loadSourceEvent = new MockEvent();
    Stub.On(mockSettings).EventAdd("LoadSource", Is.Anything).Will(MockEvent.Hookup(loadSourceEvent));
  }

Nos TestSelectSourceAndRetrieveContent et TestSelectItemAndPlay n'ont ainsi qu'à dériver de cette classe et appeler la méthode de création de contexte dans leur setup :

[TestFixture]
public class TestSelectSourceAndRetrieveContent : DeclareTestMocks
{
  [SetUp]
  public void SetUp()
  {
    base.SetUpMocks();
    //...
  }
  
  //...
}

Ainsi nous arrivons à compiler et faire réussir tous nos tests.
Nous n'avons pas encore le scénario complet de suvegarde/récupération de l'URL mais nous en avons une partie conçue, codée et testée tout en ayant laissé l'ensemble dans un état stable : nous pouvons nous arrêter ici et continuer une fois prochaine.