Talents en applications - Blogue | Nexapp

Lisibilité des tests, DSL et refactoring

Rédigé par Alexandre Rivest | May 19, 2023 4:00:00 AM

 

Lorsque l'on travaille en programmation, nommer correctement les éléments de notre code est essentiel pour la lisibilité, la maintenance et la compréhension de notre travail par les autres développeurs. C'est particulièrement vrai pour les tests frontend, qui peuvent être complexes en raison des interactions avec l'utilisateur et des changements d'état dynamiques de l'interface utilisateur. Les noms de test clairs et précis peuvent aider à comprendre facilement l'objectif de chaque test, à documenter les fonctionnalités testées et à organiser les tests en groupes logiques. Qu’en est-il de leur contenu?

 

Prenons comme exemple une page de connexion en React qui informe l’utilisateur lorsqu’un des champs est vide.

import type { FormEvent } from 'react';
import React, { useState } from 'react';


interface Props {
 onLogin: (email: string, password: string) => void;
}

export function Login({ onLogin }: Props) {
 const [error, setError] = useState('');

 const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
   const {
     email: { value: email },
     password: { value: password }
   } = event.target.elements;

   if (!email) {
     setError('Email is required');
   } else if (!password) {
     setError('Password is required');
   } else {
     setError('');
     onLogin(email, password);
   }
 };

 return (
   <form onSubmit={handleSubmit}>
     <label htmlFor="email">Email</label>
     <input id="email" name="email" type="text" />


     <label htmlFor="password">Password</label>
     <input id="password" name="password" type="password" />

     <button type="submit">Submit</button>

     {error ? <div role="alert">{error}</div> : null}
   </form>
 );
}

Voici une suite de tests qui valide que notre implémentation fonctionne bel et bien.

describe('Login', () => {
 it('should call onLogin when clicking on the submit button given email and password are filled', () => {
   const onLogin = jest.fn();
   render(<Login onLogin={onLogin} />);

   const emailInput = screen.getByLabelText(/Email/);
   userEvent.type(emailInput, 'email@email.ca');
   const passwordInput = screen.getByLabelText(/Password/);
   userEvent.type(passwordInput, 'password-123');

   const submitButton = screen.getByRole('button', { name: /Submit/ });
   userEvent.click(submitButton);

   expect(onLogin).toHaveBeenCalledWith('email@email.ca', 'password-123');
 });

 it('should display email is required when submitting the form without email', () => {
   render(<Login onLogin={jest.fn()} />);

   const passwordInput = screen.getByLabelText(/Password/);
   userEvent.type(passwordInput, 'password-123');

   const submitButton = screen.getByRole('button', { name: /Submit/ });
   userEvent.click(submitButton);

   const emailRequiredError = screen.getByText(/Email is required/);
   expect(emailRequiredError).toBeInTheDocument();
 });

 it('should display password is required when submitting the form without password', () => {
   render(<Login onLogin={jest.fn()} />);

   const emailInput = screen.getByLabelText(/Email/);
   userEvent.type(emailInput, 'email@email.ca');

   const submitButton = screen.getByRole('button', { name: /Submit/ });
   userEvent.click(submitButton);

   const emailRequiredError = screen.getByText(/Password is required/);
   expect(emailRequiredError).toBeInTheDocument();
 });
});

À première vue, les tests semblent plus complexes qu’ils ne le devraient pour la simplicité du comportement qu’ils valident. En effet, on peut y voir beaucoup d'éléments spécifiques aux librairies utilisées (Jest, Testing-Library, User-Event et React). Or, l’objectif des tests est de valider le comportement utilisateur sur la page, et non son implémentation. Les personnes qui utilisent l’application n'ont pas conscience des technologies utilisées. Les tests de UI devraient se lire de la même façon qu’une personne qui observe l’utilisateur sur la plateforme.

Domain Specific Language

Afin d’écrire nos tests d’une façon qui ressemble davantage à comment l’utilisateur va jouer avec notre application, il est possible de se créer un “Domain Specific Language” (DSL). Le DSL est un langage conçu pour résoudre des problèmes spécifiques à un domaine particulier. Dans notre cas, le domaine est les différents éléments et actions qui composent la page de connexion. Les DSL sont généralement plus simples à utiliser et à apprendre que les langages de programmation généraux, car ils sont conçus pour refléter le vocabulaire du domaine.

 

La première étape à faire pour définir notre DSL est de se poser les questions suivantes:

  • Quels sont les différents éléments que l’utilisateur voit sur l’application.
  • Quelles actions l’utilisateur peut effectuer sur la page?
  • À quoi peut s’attendre l’utilisateur lorsqu’il fait une action X?

Les réponses à ses questions deviendront la base de votre DSL. Voici à quoi pourrait ressembler le DSL de notre page de connexion:

const loadLoginPage = () => {
 const onLogin = jest.fn();
 render(<Login onLogin={onLogin} />);

 return onLogin;
};

const fillPassword = (password: string) => {
 const passwordInput = screen.getByLabelText(/Password/);
 userEvent.type(passwordInput, password);
};

const fillEmail = (email: string) => {
 const emailInput = screen.getByLabelText(/Email/);
 userEvent.type(emailInput, email);
};

const clickOnSubmit = () => {
 const submitButton = screen.getByRole('button', { name: /Submit/ });
 userEvent.click(submitButton);
};

const shouldDisplayError = (error: RegExp) => {
 const emailRequiredError = screen.getByText(error);
 expect(emailRequiredError).toBeInTheDocument();
};

L’utilisateur peut:

  • Voir la page de connexion
  • Effectuer 3 actions (remplir le courriel, le mot de passe et cliquer sur soumettre)
  • S’attendre à voir un message d’erreur

En utilisant notre nouveau DSL dans notre suite de tests, nous nous retrouvons donc avec:

describe('Login', () => {
 it('should call onLogin when clicking on the submit button given email and password are filled', () => {
   const onLogin = loadLoginPage();

   fillEmail('email@email.ca');
   fillPassword('password-123');
   clickOnSubmit();

   expect(onLogin).toHaveBeenCalledWith('email@email.ca', 'password-123');
 });

 it('should display email is required when submitting the form without email', () => {
   loadLoginPage();

   fillPassword('password-123');
   clickOnSubmit();

   shouldDisplayError(/Email is required/);
 });

 it('should display password is required when submitting the form without password', () => {
   loadLoginPage();

   fillEmail('email@email.ca');
   clickOnSubmit();

   shouldDisplayError(/Password is required/);
 });

Comme on peut le constater, le "flow" des tests est maintenant beaucoup plus clair à suivre! L’ensemble du nom du test est représenté clairement dans le test.

 

De plus, ce refactor a permis d’éliminer de la duplication de code, soit les différentes actions possibles sur l’interface.

Conclusion

Les tests automatisés sont une excellente source de documentation pour nos logiciels. Il est important qu’ils soient faciles à comprendre à la première lecture. Pour y arriver, se faire un “Domain Specific Language” permet de vulgariser les différentes étapes dans un langage compréhensible de tous. Ce dernier nous permet aussi de bâtir les prochaines validations de fonctionnalités.