Architecture applicative minimum pour tester unitairement

le 23/04/2010 par Marc Bojoly
Tags: Software Engineering

L'un des points fondamentaux pour réaliser un test automatisé est de le rendre reproductible. L'un des critères pour qu'un test soit unitaire est qu'une seule méthode soit testée de façon isolée sans dépendre d'une base de données ou de tout autre système externe. Le moyen le plus efficace pour assurer ces deux caractéristiques est d'utiliser des mocks. Trop souvent, lorsqu'on prononce ces mots devant un client, des réactions de méfiance apparaissent : on a besoin de la base de données dans l'équipe, avec notre code ça n'est pas possible, c'est compliqué... L'objectif de ce billet est de montrer une architecture applicative minimale pour répondre à ce besoin. Sur mon projet actuel la majorité de l'équipe .NET ne connaissait pas la problématique de tests unitaires. La majorité de leur code se présentait sous forme de méthodes statiques appelant au final des procédures stockées, cas somme tout assez fréquent dans les projets d'application de gestion. Le premier refactoring doit assurer un découpage en couche minimal : une classe métier MyService contenant le code à tester, une classe MyDAO contenant le minimum de code permettant d'appeler une procédure stockée car il ne sera pas testé unitairement. Voici quelques points simples à vérifier par MyDAO pour s'assurer d'un découpage effectif :

  • Chaque méthode de MyDAO prend les valeurs de ses paramètres (si possible des POCOs) les transforme en paramètres SQL. Elle appelle la procédure stockée et renvoie le résultat. Ce sont les seules responsabilités que ces méthodes sont autorisées à prendre.
  • Les seules structures conditionnelles doivent concerner des cas d'erreur ou du paramétrage SQL. Déporter toute structure if dans la classe MyService en créant au besoin deux méthodes séparées dans la classe MyDAO .

Voici un exemple simplifié de la classe MyService qui sera testée :

public class MyService {
  public static Customer CreateCustomer(string name, string surname) {
    //Code métier
   MyDAO.createcustomer(name, surname, technicalReference);
   //Code métier
  }
}

Afin d'introduire les premiers tests unitaires sur la classe MyService, nous avons indiqué à l'équipe de suivre désormais les quelques règles suivantes:

  • Première règle : utiliser des singletons pour transformer les appels statiques en appel d'instance à moindre coût.
  • Deuxième règle : définir une interface pour chaque classe.
  • Troisième règle : placer une référence à MyDAO dans MyService et appeler les méthodes sur cette référence à la place des méthodes statiques.
  • Quatrième règle : dans le constructeur de mon service, initialiser la référence à MyDAO avec le singleton

Voici le résultat sur notre exemple :

public class MyService {
  private IMyDAO _MyDAO;
  
  public static readonly IMyService Instance = new MyService();

  private MyService() {
    _MyDAO = MyDAO.Instance();
  }
  
  public Customer CreateCustomer(string name, string surname) {
    //Code métier
   _MyDAO.Createcustomer(name, surname, technicalReference);
   //Code métier
  }
}

Ce premier changement a été suffisamment léger pour être facilement appréhendé par les développeurs. Maintenant voyons l'utilité qu'on l'on peut en tirer :

  • Cinquième règle : Ajouter un constructeur public prenant en argument IMyDAO.

Comme ceci :

public class MyService
    {
        private IMyDAO _MyDAO;

        public static readonly IMyService Instance = new MyService();

        private MyService()
        {
            _MyDAO = MyDAO.Instance();
        }

        /// <summary>
        /// Ce constructeur est uniquement dédié aux tests unitaires et n’est pas conçu pour être utilisé en temps normal
        /// Il pourra être accédé depuis les tests unitaires en ajoutant [assembly: InternalsVisibleTo("UnitTests")] sur l'assembly
        /// </summary>
        /// <param name="MyDAO"></param>
        internal MyService(IMyDAO MyDAO)
        {
            _MyDAO = MyDAO;
        }

        public Customer CreateCustomer(string name, string surname)
        {
            //Code métier
            _MyDAO.CreateCustomer(name, surname, technicalReference);
            //Code métier
        }
    }

Il est maintenant possible d'écrire un test unitaire de la façon suivante : entièrement mocké, entièrement isolé. L'environnement est parfaitement maîtrisé à chaque exécution du test.

//Mock
class DaoMock : IMyDAO {
	public Customer CreateCustomer(string name, string surname, int technicalReference) {
	  Assert.AreEqual("TestName", name);
	  Assert.AreEqual("TestSurname", surname);
	  return new Customer("TestName", "TestSurname");
	}
}

[Test]
public void TestCreateCustomerOK() {
  
  IMyDAO daoMock = new DaoMock();
  
  IMyService serviceToTest = new MyService(daoMock);
  Customer result = serviceToTest()
  Assert.AreEqual("TestName", result.Name);
  Assert.AreEqual("TestSurname", result.Surname);
}

Très rapidement dans ce projet, nous avons introduit le framework Rhino Mock pour diminuer la quantité de code nécessaire pour les mocks. Mais l'infrastructure sous-jacente étant déjà présente cela a permis d'appréhender le framework plus facilement. L'exemple suivant met en regard les nouveaux concepts introduits par rapport au code précédent :

[Test]
public void TestCreateCustomerOK() {
  
  MockRepository mockRepository = new MockRepository();
  IMyDAO daoMock = mockRepository.StrictMock<IMyDAO>();
  Customer sample = new Customer("TestName", "TestSurname");
  Expect.Call(daoMock.CreateCustomer("TestName", "TestSurname")).Return(sample);
  mockRepository.ReplayAll();
  
  IMyDAO daoMock = new DaoMock();
  
  IMyService serviceToTest = new MyService(daoMock);
  Customer result = serviceToTest()
  Assert.AreEqual("TestName", result.Name);
  Assert.AreEqual("TestSurname", result.Surname);
}

Cette solution est minimaliste : il ne s'agit pas d'inversion de contrôle car le lien vers l'implémentation est dispersé dans les différentes classes. Cependant, elle permet de réaliser le découplage minimal pour garantir des tests réellement unitaires tout en minimisant l'impact sur un code existant. Les classes appelant préalablement MonService.CreateCustomer(...) devront ainsi simplement appeler MonService.Instance.CreateCustomer(...). Tous ces exemples ont été réalisés en C#/.NET, technologie de mon projet actuel. Notez cependant qu'ils sont transposables quasi en l'état pour un projet Java. Si un framework comme Spring, souvent cité dans le monde Java lorsqu'on réalise des tests unitaires, n'est pas un besoin immédiat, une telle architecture applicative peut être une alternative plus légère pour permettre l'introduction des premiers tests unitaires. C'est donc une raison de plus pour commencer dès aujourd'hui à écrire des tests sur votre code!