Passer au contenu

Automock

Automock est une bibliothèque autonome puissante conçue pour les tests unitaires. Elle utilise l’API de réflexion TypeScript en interne pour générer des objets mocks, simplifiant ainsi le processus de test en simulant automatiquement les dépendances externes des classes. Automock vous permet d’optimiser le développement des tests et de vous concentrer sur l’écriture de tests unitaires robustes et efficaces.

Introduction

Le conteneur d’injection de dépendances (DI) est un élément fondamental du système de modules Nest, intégral aux phases d’exécution de l’application et de test. Dans les tests unitaires, les dépendances fictives sont essentielles pour isoler et évaluer le comportement de composants spécifiques. Cependant, la configuration manuelle et la gestion de ces objets mocks peuvent être complexes et sujettes à erreurs.

Automock offre une solution rationalisée. Plutôt que d’interagir avec le véritable conteneur DI de Nest, Automock introduit un conteneur virtuel où les dépendances sont automatiquement simulées. Cette approche contourne la tâche manuelle de substitution de chaque fournisseur dans le conteneur DI par des implémentations mocks. Avec Automock, la génération d’objets mocks pour toutes les dépendances est automatisée, simplifiant ainsi le processus de configuration des tests unitaires.

Installation

Automock prend en charge à la fois Jest et Sinon. Il suffit d’installer le package approprié pour votre cadre de test de choix. De plus, vous devez installer le @automock/adapters.nestjs (car Automock prend également en charge d’autres adaptateurs).

Fenêtre de terminal
$ npm i -D @automock/jest @automock/adapters.nestjs

Ou, pour Sinon :

Fenêtre de terminal
$ npm i -D @automock/sinon @automock/adapters.nestjs

Exemple

L’exemple fourni ici montre l’intégration d’Automock avec Jest. Cependant, les mêmes principes et fonctionnalités s’appliquent à Sinon.

Considérons la classe CatService, qui dépend d’une classe Database pour récupérer les chats. Nous allons simuler la classe Database pour tester la classe CatsService en isolation.

Classe Database
@Injectable()
export class Database {
getCats(): Promise<Cat[]> { ... }
}
@Injectable()
export class CatsService {
constructor(private database: Database) { }
async getAllCats(): Promise<Cat[]> {
return this.database.getCats();
}
}

Configurons un test unitaire pour la classe CatsService.

Dans la configuration du test, nous :

  1. Créons un environnement de test pour CatsService en utilisant TestBed.create(CatsService).compile().
  2. Obtenons l’instance réelle de CatsService et une instance simulée de Database en utilisant unit et unitRef.get(Database), respectivement.
  3. Nous simulons la méthode getCats de la classe Database pour renvoyer une liste prédéfinie de chats.
  4. Nous appelons ensuite la méthode getAllCats de CatsService et vérifions qu’elle interagit correctement avec la classe Database et renvoie les chats attendus.

Ajout d’un logger

Ajoutons à notre exemple un interface Logger et intégrons-la dans la classe CatsService.

Classe Logger
@Injectable()
export class Logger {
log(message: string): void { ... }
}
@Injectable()
export class CatsService {
constructor(private database: Database, private logger: Logger) { }
async getAllCats(): Promise<Cat[]> {
this.logger.log('Fetching all cats..');
return this.database.getCats();
}
}

Maintenant, lorsque vous configurez votre test, vous devrez également simuler la dépendance Logger.

beforeAll(() => {
let logger: jest.Mocked<Logger>;
const { unit, unitRef } = TestBed.create(CatsService).compile();
catsService = unit;
database = unitRef.get(Database);
logger = unitRef.get(Logger);
});
it('devrait enregistrer un message et récupérer les chats depuis la base de données', async () => {
const mockCats: Cat[] = [{ id: 1, name: 'Catty' }, { id: 2, name: 'Mitzy' }];
database.getCats.mockResolvedValue(mockCats);
const cats = await catsService.getAllCats();
expect(logger.log).toHaveBeenCalledWith('Fetching all cats..');
expect(database.getCats).toHaveBeenCalled();
expect(cats).toEqual(mockCats);
});

Utilisation de .mock().using() pour l’implémentation mock

Automock fournit un moyen plus déclaratif de spécifier les implémentations mock en utilisant la méthode .mock().using(). Cela vous permet de définir le comportement mock directement lors de la configuration de TestBed.

Voici comment vous pouvez modifier la configuration du test pour utiliser cette approche :

beforeAll(() => {
const mockCats: Cat[] = [{ id: 1, name: 'Catty' }, { id: 2, name: 'Mitzy' }];
const { unit, unitRef } = TestBed.create(CatsService)
.mock(Database)
.using({ getCats: async () => mockCats })
.compile();
catsService = unit;
database = unitRef.get(Database);
});

Avec cette approche, nous avons éliminé le besoin de simuler manuellement la méthode getCats dans le corps du test. Au lieu de cela, nous avons défini le comportement mock directement dans la configuration du test en utilisant .mock().using().

Références aux dépendances et accès aux instances

Lorsque vous utilisez TestBed, la méthode compile() renvoie un objet avec deux propriétés importantes : unit et unitRef. Ces propriétés donnent accès à l’instance de la classe testée et aux références de ses dépendances respectivement.

  • unit - La propriété unit représente l’instance réelle de la classe testée. Dans notre exemple, elle correspond à une instance de la classe CatsService. Cela vous permet d’interagir directement avec la classe et d’invoquer ses méthodes pendant vos scénarios de test.

  • unitRef - La propriété unitRef sert de référence aux dépendances de la classe testée. Dans notre exemple, elle fait référence à la dépendance Logger utilisée par CatsService. En accédant à unitRef, vous pouvez récupérer l’objet mock automatiquement généré pour la dépendance. Cela vous permet de simuler des méthodes, de définir des comportements et d’affirmer des appels de méthodes sur l’objet mock.

Travailler avec différents fournisseurs

Les fournisseurs sont l’un des éléments les plus importants de Nest. Vous pouvez penser à de nombreuses classes par défaut de Nest comme des fournisseurs, y compris les services, les dépôts, les usines, les helpers, etc. La fonction principale d’un fournisseur est de prendre la forme d’une dépendance Injectable.

Considérons le suivant CatsService, qui prend un paramètre, qui est une instance de l’interface suivante Logger:

Interface Logger
export interface Logger {
log(message: string): void;
}
@Injectable()
export class CatsService {
constructor(private logger: Logger) { }
}

L’API de réflexion de TypeScript ne prend pas encore en charge la réflexion d’interface. Nest résout ce problème avec des jetons d’injection basés sur des chaînes/symboles.

Automock suit cette pratique et vous permet de fournir un jeton basé sur une chaîne (ou basée sur un symbole) au lieu de fournir la véritable classe dans la méthode unitRef.get().

const { unit, unitRef } = TestBed.create(CatsService).compile();
let loggerMock: jest.Mocked<Logger> = unitRef.get('LOGGER_TOKEN');

Plus d’informations

Visitez le dépôt GitHub d’Automock ou le site Web d’Automock pour plus d’informations.