Mettez en place votre première suite de tests

Dans ce premier chapitre pratique, vous allez mettre les mains à la pâte et découvrir les bases de l'implémentation des tests unitaires en TypeScript avec Jasmine. Vous allez apprendre à écrire et à exécuter vos premiers tests simples.

D'abord, faisons un tour d'horizon de l'outil que vous allez utiliser pour écrire et exécuter vos tests : Jasmine.

Écrivez votre premier test

Pour que Jasmine reconnaisse un fichier comme contenant des tests, on lui attribue l'extension .spec.ts.

Dans le dossierservices de la feature table-management, créez un fichier table-display.service.spec.ts. Pour ce premier cas, nous suivrons le pattern d'organiser les tests par rapport à la classe qu'ils testent. Ce n'est pas la seule organisation possible, et nous en verrons d'autres par la suite.

Avec Jasmine, pour déclarer une suite de tests, on utilise describe  :

describe('TableDisplayService', () => {

});

Imaginons que le premier test que l'on souhaite écrire est pour vérifier que lorsqu'on appelle formatStatus avec le string "available", ça retourne bien "Available". Pour définir un test, on va utiliser it:

describe('TableDisplayService', () => {  
  
  it('should return "Available"', () => {  
      
  }); 
   
});

De quoi a-t-on besoin pour ce test ?

Déjà, il nous faut une instance du service. Ici, le service est une simple classe TypeScript sans dépendances, donc on peut l'instancier avecnew:

it('should return "Available"', () => {  
  const service = new TableDisplayService();  
});

Ensuite, on va appeler la méthode qui nous intéresse en y passant l'argument "available":

it('should return "Available"', () => {  
  const service = new TableDisplayService();
  const status = service.formatStatus('available');
});

Enfin, on va vérifier que le résultat est bien celui attendu :

it('should return "Available"', () => {  
  const service = new TableDisplayService();  
  const status = service.formatStatus('available');  
  
  expect(status).toBe('Available');  
});

Pour exécuter ce test, depuis une ligne de commande, exécutez simplement 

ng test

Au bout de quelques instants, un navigateur se lancera, et la ligne de commandes montrera quelque chose comme :

Watch mode enabled. Watching for file changes...
08 09 2025 12:23:10.166:WARN [karma]: No captured browser, open http://localhost:9876/
08 09 2025 12:23:10.182:INFO [karma-server]: Karma v6.4.4 server started at http://localhost:9876/
08 09 2025 12:23:10.183:INFO [launcher]: Launching browsers Chrome with concurrency unlimited
08 09 2025 12:23:10.187:INFO [launcher]: Starting browser Chrome
08 09 2025 12:23:11.171:INFO [Chrome 139.0.0.0 (Mac OS 10.15.7)]: Connected on socket BCGoVzo6Nsmi4K_dAAAB with id 93661214
Chrome 139.0.0.0 (Mac OS 10.15.7): Executed 1 of 1 SUCCESS (0.003 secs / 0.001 secs)
TOTAL: 1 SUCCESS

Félicitations ! Votre premier test passe ! Maintenant votre premier réflexe doit être de le casser...

Pourquoi ? Pour tenter de vérifier que vous testez bien ce que vous pensez tester, et que ce ne soit pas par chance (ou par erreur de conception) que votre test passe.

Comment le casser ? On peut modifier la valeur "attendu" :

it('should return "Available"', () => {  
  const service = new TableDisplayService();  
  const status = service.formatStatus('available');  
  
  expect(status).toBe('Unavailable');  
});

Immédiatement, dans le navigateur comme dans la ligne de commandes, on voit l'échec :

Chrome 139.0.0.0 (Mac OS 10.15.7) TableDisplayService should return "Available" FAILED
        Expected 'Available' to be 'Unavailable'.
            at <Jasmine>
            at UserContext.<anonymous> (src/app/features/table-management/services/table-display.service.spec.ts:9:20)
            at <Jasmine>
Chrome 139.0.0.0 (Mac OS 10.15.7): Executed 1 of 1 (1 FAILED) (0.009 secs / 0.002 secs)
TOTAL: 1 FAILED, 0 SUCCESS

Réparez le test pour que tout repasse au vert avant de continuer !

La structure du test

Avant d'écrire d'autres tests, regardons la structure du test qu'on vient d'écrire. Le test se sépare en trois étapes qu'on appelle souvent les 3A :

it('should return "Available"', () => {
  // Arrange
  const service = new TableDisplayService();  
  // Act
  const status = service.formatStatus('available');  
  // Assert
  expect(status).toBe('Available');  
});
  • Arrange
    Créer et configurer le SUT, y compris ses dépendances si besoin

  • Act
    Déclencher l'action dont on veut tester le résultat

  • Assert
    Vérifier le résultat de l'action.

La structure 3A est extrêmement courante pour les tests unitaires, et nous l'utiliserons pour tout ce cours.

Testez les autres cas

Je vous propose de commencer par les happy paths. Un happy path est un cas "normal", c'est-à-dire un cas sans exception ni erreur : ce sont les chemins de code où on est heureux de voir passer l'exécution !

describe('TableDisplayService', () => {  
  
  it('should return "Available"', () => {  
    const service = new TableDisplayService();  
    const status = service.formatStatus('available');  
  
    expect(status).toBe('Available');  
  });  
  
  it('should return "Occupied"', () => {  
    const service = new TableDisplayService();  
    const status = service.formatStatus('occupied');  
  
    expect(status).toBe('Occupied');  
  });  
  
  it('should return "Being Cleaned"', () => {  
    const service = new TableDisplayService();  
    const status = service.formatStatus('cleaning');  
  
    expect(status).toBe('Being Cleaned');  
  });  
  
  it('should return "Reserved"', () => {  
    const service = new TableDisplayService();  
    const status = service.formatStatus('reserved');  
  
    expect(status).toBe('Reserved');  
  });  
  
});

En plus des happy paths, la méthode propose aussi une réponse au où on essayerait de formater un string qui ne correspond pas à un statut valide : il s'agit d'un edge case — une situation dans laquelle on ne devrait jamais se retrouver, mais qu'on doit quand même traiter et donc tester !

it('should return "Unknown" if the requested status is not recognized', () => {  
  const service = new TableDisplayService();  
  const status = service.formatStatus('invalid');  
  
  expect(status).toBe('Unknown');  
});

En tant que développeurs consciencieux, quelque chose doit nous sauter aux yeux dans cette suite de tests :

describe('TableDisplayService', () => {  
  
  it('should return "Available"', () => {  
    const service = new TableDisplayService();  
    const status = service.formatStatus('available');  
  
    expect(status).toBe('Available');  
  });  
  
  it('should return "Occupied"', () => {  
    const service = new TableDisplayService();  
    const status = service.formatStatus('occupied');  
  
    expect(status).toBe('Occupied');  
  });  
  
  it('should return "Being Cleaned"', () => {  
    const service = new TableDisplayService();  
    const status = service.formatStatus('cleaning');  
  
    expect(status).toBe('Being Cleaned');  
  });  
  
  it('should return "Reserved"', () => {  
    const service = new TableDisplayService();  
    const status = service.formatStatus('reserved');  
  
    expect(status).toBe('Reserved');  
  });  
  
  it('should return "Unknown" if the requested status is not recognized', () => {  
    const service = new TableDisplayService();  
    const status = service.formatStatus('invalid');  
  
    expect(status).toBe('Unknown');  
  });  
  
})

Ce code est très répétitif ! Les mêmes règles s'appliquent pour les tests que pour le code qui part en production : si une refactorisation permet de les rendre plus lisibles et faciles à comprendre, il faut y aller !

Le premier changement que je vous propose, c'est de sortir l'instantiation du service des différents tests et de la mutualiser grâce à beforeEach :

describe('TableDisplayService', () => {  
  
  let service: TableDisplayService;  
  
  beforeEach(() => {  
    service = new TableDisplayService();  
  });  
  
  it('should return "Available"', () => {  
    const status = service.formatStatus('available');  
  
    expect(status).toBe('Available');  
  });  
  
  it('should return "Occupied"', () => {  
    const status = service.formatStatus('occupied');  
  
    expect(status).toBe('Occupied');  
  });  
  
  // ...
  
});

Chaque test est ainsi simplifié : nous avons mutualisé l'étape Arrange car elle est commune à tous les tests, et chaque test ne contient plus que les étapes Act et Assert.

Tous les tests dans cette suite ont exactement le même format : ils appellent formatStatus en y passant une valeur, et ils en vérifient le résultat. Certains frameworks de test permettent de créer un test paramétrisé — un test qui exécute la même action pour N valeurs différentes et y associe les résultats attendus — mais pour Jasmine ce n'est pas le cas. On peut, cependant, créer une fonction pour simplifier encore plus notre suite de tests :

describe('TableDisplayService', () => {  
  
  let service: TableDisplayService;  
  
  beforeEach(() => {  
    service = new TableDisplayService();  
  });  
  
  it('should return "Available"', () => {  
    expectFormattedStatus('available', 'Available');  
  });  
  
  it('should return "Occupied"', () => {  
    expectFormattedStatus('occupied', 'Occupied');  
  });  
  
  it('should return "Being Cleaned"', () => {  
    expectFormattedStatus('cleaning', 'Being Cleaned');  
  });  
  
  it('should return "Reserved"', () => {  
    expectFormattedStatus('reserved', 'Reserved');  
  });  
  
  it('should return "Unknown" if the requested status is not recognized', () => {  
    expectFormattedStatus('invalid status', 'Unknown');  
  });  
  
  function expectFormattedStatus(status: string, expected: string) {  
    expect(service.formatStatus(status)).toBe(expected);  
  }  
  
});

Cette dernière étape est facultative, et dépend profondément du contexte : est-ce que cette refactorisation rend plus ou moins lisible et compréhensible la suite de tests ? À vous de choisir.

Qu’ai-je contre ces tests ?

Je vous ai présenté ce cas pour faciliter l'apprentissage des outils de base de Jasmine sans les outils Angular, mais comme je l'ai dit au début du chapitre, ces tests ne sont pas de "bons tests". Sans se demander si ce comportement devrait être implémenté dans un service (ce qui est déjà un peu douteux !), ici on teste une méthode sur un service et non un comportement applicatif. Du coup, on se retrouve obligés de modifier voire supprimer ce test si on déplace cette logique.

Si on regarde où cette méthode est utilisée, on se rend compte qu'elle est appelé par le component TableDisplay pour afficher les propriétés d'une table. Le comportement à tester serait donc plutôt : "pour une table donnée, est-ce que son statut apparaît correctement ?" Comme on le verra dans les chapitres suivants, l'endroit où on place ce test dépend de la couche de l'application qui est responsable de formater la table :

  • soit on décide que la table doit être formatée avant d'arriver au component, et à ce moment-là on mettra le test sur la façade (si vous ne connaissez pas ce pattern, pas de panique, promis je vous expliquerai !)

  • soit on décide que c'est au component de formater la table (potentiellement via un Pipe), et à ce moment-là on mettra le test sur le component

Si tout cela paraît encore très flou, ne vous inquiétez pas, c'est normal ! On va mettre tout ça en pratique dans les chapitres à venir.

À vous de jouer

Contexte

La méthode  getStatusColor()  de ce service n'est pas testée !

Consigne

Écrivez des tests qui vérifient les différents résultats possibles de  getStatusColor()  .

Vous pouvez les implémenter dans la même suite de tests. Vous pouvez aussi créer une séparation à l'intérieur du fichier avec des 

describe imbriqués :

describe('TableDisplayService', () => {
  describe('formatStatus', () => {
    // formatStatus tests go here, including utility functions
  });
  describe('getStatusColor', () => {
    // getStatusColor tests and utility functions go here
  });
});

En résumé

  • les tests sont construits en trois étapesArrangeActAssert

  • on déclare une suite de tests Jasmine avec  describe  , un test avec  it  , et une assertion avec  expect

  • la description de chaque test doit être claire et décrire le comportement attendue

  • la refactorisation des tests est aussi importante que celle du code de production 

Dans le prochain chapitre, vous découvrirez les outils de tests spécifiques à Angular !

Et si vous obteniez un diplôme OpenClassrooms ?
  • Formations jusqu’à 100 % financées
  • Date de début flexible
  • Projets professionnalisants
  • Mentorat individuel
Trouvez la formation et le financement faits pour vous