• Contenu
  • Bas de page
logo ouidoulogo ouidoulogo ouidoulogo ouidou
  • Qui sommes-nous ?
  • Offres
    • 💻 Applications métier
    • 🤝 Collaboration des équipes
    • 🛡️ Sécurisation et optimisation du système d’information
    • 🔗 Transformation numérique
  • Expertises
    • 🖥️ Développement logiciel
    • ♾️ DevSecOps
    • ⚙️ Intégration de logiciels et négoce de licences
      • Atlassian : Jira, Confluence, Bitbucket…
      • Plateforme monday.com
      • GitLab
      • SonarQube
    • 📚​ Logiciel de CRM et de gestion
    • 🎨 UX/UI design
    • 🌐 Accessibilité Numérique
    • 🗂️​ Démarches simplifiées
    • 📝 Formations Atlassian
  • Références
  • Carrières
    • 🧐 Pourquoi rejoindre Ouidou ?
    • ✍🏻 Nous rejoindre
    • 👨‍💻 Rencontrer nos collaborateurs
    • 🚀 Grandir chez Ouidou
  • RSE
  • Ressources
    • 🗞️ Actualités
    • 🔍 Articles techniques
    • 📖 Livres blancs
    • 🎙️ Interviews Clients
Nous contacter
✕
Vous reprendrez bien un morceau ?
Vous reprendrez bien un morceau ?
4 juin 2020
Retour sur la conférence ElixirConf Europe 2020
Retour sur la conférence ElixirConf Europe 2020
1 juillet 2020
Ressources > Articles techniques > Pilotez vos tests Elixir avec des scénarios

Pilotez vos tests Elixir avec des scénarios

Article écrit par Ludovic de Luna

Clap de démarrage pour les tests scénarisés en Elixir

Photo par Avel Chuklanov depuis Unsplash

À la fois documentation et validateur de l’implémentation, le test est une composante essentielle à la qualité logicielle. Elixir, tout comme son écosystème, intègre le test dans son ADN. Mon premier projet Elixir remonte à 2018. Nous découvrions chez Synbioz les approches possibles et la manière d’utiliser cette technologie.

Bien que nous ayons organisé avec soin nos tests, j’ai constaté que leur qualité s’était dégradée au fil des évolutions de notre base de code. Je souhaite partager avec vous une approche que j’ai récemment expérimentée pour mieux contrôler les tests au travers d’un modèle plus déclaratif. Ce n’est pas une solution exhaustive, mais elle reste simple et centrée sur ExUnit, le framework de test embarqué dans Elixir.

Gargantua est passé par là !

Nous étions confiants lors de l’implémentation initiale : reproductibilité du test via des générateurs pour le jeu de données, séparation des tests unitaires et d’intégration, un peu de factorisation et de macros pour éviter la répétition… C’était déjà correct.

Mais en revenant sur les tests après plus d’un an d’exploitation, je constate qu’ils perdent en lisibilité pour certains cas – la factorisation n’apportant pas le résultat attendu sur ce point. Nos tests prennent invariablement de l’embonpoint.

Voici un exemple de départ très simpliste de ce que nous avons :

describe "MyApp.hello/2" do   test "Get a simple greeting when the reg number is not 1000" do      # Build objects     users = %{       1 => build(         :user,         %{name: "Baker", regnum: 1}       )     }      # Assertions     assert "Hello Baker !" = MyApp.hello(users, 1)   end end 

Cet exemple ne pose en soi aucun problème. Mais parfois, on souhaite valider le résultat d’une même action de façon répétitive en fonction du jeu de données.

Ne voyant pas de solution immédiate, j’ai continué en ce sens tout en sachant qu’il faudrait tôt ou tard trouver une autre approche.

Et ce moment est arrivé récemment pour un cas qui nécessitait d’ajouter plusieurs scénarios pour valider la correction d’un bogue assez vicieux. C’est le point de départ qui m’a fait dire que nous perdrions durablement en qualité si nous poursuivions ainsi.

Scénariser le test

Je voulais modifier notre approche pour obtenir :

  • un jeu de tests déclaratif pour lequel il serait simple de désactiver ou d’ajouter un élément dans les phases de construction ;
  • l’entête me donne de visu les conditions du banc d’essai sans besoin d’aller trop loin dans la lecture du code ;
  • le corps du test devait contenir les assertions et rien d’autre.

Et pour y arriver, il fallait utiliser un composant que j’ai trop sous-estimé : l’objet context. Couplé à la fonction de rappel via le setup et à l’annotation tag, j’avais tout ce qu’il me fallait.

Mais avant d’aller plus loin, résumons le rôle de chacun.

Le context est un tableau associatif (Map en Elixir) mis à disposition par le framework ExUnit et partagé auprès de l’ensemble des tests tout comme des phases de préparation. N’oubliez pas qu’en Elixir, la donnée est immuable. Il n’est donc pas possible de modifier son contenu en dehors des mécanismes prévus à cet effet par ExUnit.

Le setup est une phase de préparation qui définit les conditions dans lesquelles le test va se dérouler. Il s’agit d’actions faites avant chaque test au travers d’une (ou plusieurs) fonction de rappel. Cette phase a également pour objectif d’alimenter le context en données par fusion avec les éléments renvoyés par la fonction de rappel.

Le tag permet d’alimenter le context avec les données du test avant même sa phase de préparation, ici aussi par fusion.

Réfléchissez quelques instants à l’usage du contexte pour orienter les tests par fonction et par scénario, le tout adossé à des fonctions de support (ou helpers) pour préserver la lisibilité de l’ensemble.

Voici comment procéder en 3 étapes.

Étape 1 – externaliser la préparation

L’idée est d’avoir une fonction qui recevra en argument le context (un Map) et retournera les modifications à appliquer à ce dernier (par fusion). Souvenez-vous que le context contiendra déjà les éléments déclarés en entête de chaque test.

Voici un exemple de fonction de préparation d’un test (méthode privé) :

defp prepare_users(context) do   # ... do something with the context ...   # return changes to apply on the context:   %{name: "Baker"} end 

Une fonction de préparation doit toujours renvoyer un résultat. Et ce résultat est standardisé en Elixir.

Ici, on retourne un Map qui sera fusionné avec le context. A minima, il faut retourner un indicateur de réussite (l’atome « :ok ») si on ne veut pas modifier le context. Mais on peut aussi cumuler l’indicateur de réussite avec les éléments à fusionner via une liste de mots-clés.

Exemple :

{:ok, [name: "Baker"]} 

En dernière position dans un atome, les crochets sont optionnels pour la liste de mots-clés :

{:ok, name: "Baker"} 

Le corps de notre fonction contient ce que nous aurions normalement placé dans la déclaration du setup, que nous allons utiliser pour la suite un peu différemment.

Vos fonctions de préparation peuvent se placer dans la même section que vos tests, regroupées soit en début soit en fin.

Étape 2 – chaîner les étapes de préparation

Nous avions pour habitude d’avoir un seul appel à la fonction setup. Elle était très générique à l’ensemble du fichier de test. Il est possible d’avoir une fonction setup en plus par section de description (« describe »), et c’est ce que nous allons faire.

À l’inverse de la classique déclaration comme ci-après :

setup(context) do   # all actions of setup ...   # ... and finally the success indicator   :ok end 

Je vous propose d’utiliser une autre syntaxe, plus courte, et qui va appeler en cascade les fonctions créées à l’étape précédente :

setup [:prepare_users, :prepare_other_thing, :prepare_commons] 

La liste ci-dessus contient le nom des fonctions à appeler. Le setup leur donnera en argument le context et traitera le retour comme nous l’avons vu à l’étape précédente.

Quelle est la conséquence de l’échec d’une fonction de rappel ? Le test associé sera marqué en échec et les fonctions de rappel qui suivent ne seront pas exécutées. Tout comme pour un test, le rapport final mentionnera les détails de l’erreur.

Étape 3 – mise en œuvre du « tag »

Pour alimenter notre contexte avec de nouvelles entrées, nous allons utiliser les tags. Voici un exemple :

setup [:prepare_users]  @tag user_reg_number: 10 test "Get a simple greeting when the reg number is not 1000" do   # Assertions here... end 

Ici, on ajoute au tableau associatif context une entrée reg_number. Nous pourrons l’utiliser aussi bien dans une fonction en phase de préparation que dans le test en lui-même. L’une des phases de préparations pourrait être :

defp prepare_users(%{user_reg_number: reg_number} = _context),   do: build(:user, reg_number: reg_number)  defp prepare_users(_context),   do: :ok 

Ici, on va créer un utilisateur qui aura le numéro de registre fournis dans le contexte via la clé user_reg_number. Si cette information est manquante, l’utilisateur ne sera pas créé (le cas de la seconde fonction).

Exemple complet

J’ai créé un petit dépôt GitHub pour jouer avec ce principe. Il vous faudra un Elixir opérationnel (ou l’utiliser via une image Docker). Le projet Elixir s’appelle « ElixirUnitTests » et ne contient véritablement qu’un seul module. J’avoue avoir été peu inspiré pour lui trouver un nom (le mal de tous les développeurs).

Voici l’organisation générale du fichier de test. Nous verrons les 3 points qui composent le test de façon séparée :

  1. Setup
  2. Tests
  3. Helpers

Un module de test reprend le nom du module qu’il teste suivit de « Test ».

defmodule ElixirUnitTestsTest do # ... prepare test env    describe "ElixirUnitTests.hello/2" do      # 1. Setup by calling functions before start each test     # (see the article bellow)      # 2. Tests     # (see the article bellow)      # 3. Helpers for ElixirUnitTests.hello/2     # (see the article bellow)    end  end 

C’est assez générique. La phase de préparation via le setup, puis les tests (assertions) et quelques fonctions de support ou helpers.

1 — Voici le setup :

# Setup by calling functions before start each test setup [:load_fixtures] 

2 — Vient ensuite les tests :

# Tests  @tag user: %{name: "Baker"} @tag congratulate: false test "Simple hello with the name", %{users: users, id: id} = _context do   assert "Hello Baker !" = ElixirUnitTests.hello(users, id) end  @tag user: %{name: "Alan"} @tag congratulate: true test "Hello with congratulations", %{users: users, id: id} = _context do   assert "Hello Alan ! Congratulations !" = ElixirUnitTests.hello(users, id) end 

3 — Et pour finir, les helpers :

# Helpers for ElixirUnitTests.hello/2  defp load_fixtures(%{user: user, congratulate: true} = _context),   do: build_users(user, 1000)  defp load_fixtures(%{user: user} = _context),   do: build_users(user, 1)  def build_users(attributes, id) do   [     users: %{id => build(:user, put_in(attributes[:regnum], id))},     id: id   ] end 

La fonction que vous ne pouvez pas voir est build. Elle fait partie des fonctions de support pour l’ensemble des tests et a en charge la création d’une structure de modèle (Ecto) pour le stockage en base de données.

Résumé

Nous avons vu comment exploiter le tag pour piloter le banc de test et conserver dans le corps du test uniquement les assertions. C’est une autre approche pour créer des tests en fonction de scénarios en limitant la surcharge sur chaque test.

C’est toujours difficile d’extraire un exemple utile depuis un code métier pour un article. J’espère que l’intérêt de la solution est intact et que ça pourra vous aider dans vos tests en Elixir.

Si vous souhaitez jouer avec l’exemple donné plus haut dans l’article, je vous ai concocté un dépôt sous GitHub qui contient le projet Elixir entier.

Je vous souhaite de prendre plaisir dans vos développements Elixir !

À lire aussi

Fresque numérique miniature image
16 avril 2025

Fresque du Numérique

Lire la suite

intelligence artificielle Ouicommit miniature image
17 mars 2025

Ouicommit – L’intelligence artificielle en entreprise, on y est ! 

Lire la suite

Image miniature Hackathon Women in Tech
13 mars 2025

Hackathon Women in Tech :  un engagement pour une tech plus inclusive 

Lire la suite

image miniature les nouveautés Atlassian
26 février 2025

Les nouveautés Atlassian en 2025

Lire la suite

Articles associés

Fresque numérique miniature image
16 avril 2025

Fresque du Numérique


Lire la suite
intelligence artificielle Ouicommit miniature image
17 mars 2025

Ouicommit – L’intelligence artificielle en entreprise, on y est ! 


Lire la suite
Image miniature Hackathon Women in Tech
13 mars 2025

Hackathon Women in Tech :  un engagement pour une tech plus inclusive 


Lire la suite

À propos

  • Qui sommes-nous ?
  • Références
  • RSE
  • Ressources

Offres

  • Applications métier
  • Collaboration des équipes
  • Sécurisation et optimisation du système d’information
  • Transformation numérique

Expertises

  • Développement logiciel
  • DevSecOps
  • Intégration de logiciels et négoce de licences
  • Logiciel de CRM et de gestion
  • UX/UI design
  • Accessibilité Numérique
  • Démarches simplifiées
  • Formations Atlassian

Carrières

  • Pourquoi rejoindre Ouidou ?
  • Nous rejoindre
  • Rencontrer nos collaborateurs
  • Grandir chez Ouidou

SIEGE SOCIAL
70-74 boulevard Garibaldi, 75015 Paris

Ouidou Nord
165 Avenue de Bretagne, 59000 Lille

Ouidou Rhône-Alpes
4 place Amédée Bonnet, 69002 Lyon

Ouidou Grand-Ouest
2 rue Crucy, 44000 Nantes

Ouidou Grand-Est
7 cour des Cigarières, 67000 Strasbourg

  • Linkedin Ouidou
  • GitHub Ouidou
  • Youtube Ouidou
© 2024 Ouidou | Tous droits réservés | Plan du site | Mentions légales | Déclaration d'accessibilité
    Nous contacter