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).
$ npm i -D @automock/jest @automock/adapters.nestjs
Ou, pour Sinon :
$ 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.
@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 :
- Créons un environnement de test pour
CatsService
en utilisantTestBed.create(CatsService).compile()
. - Obtenons l’instance réelle de
CatsService
et une instance simulée deDatabase
en utilisantunit
etunitRef.get(Database)
, respectivement. - Nous simulons la méthode
getCats
de la classeDatabase
pour renvoyer une liste prédéfinie de chats. - Nous appelons ensuite la méthode
getAllCats
deCatsService
et vérifions qu’elle interagit correctement avec la classeDatabase
et renvoie les chats attendus.
Ajout d’un logger
Ajoutons à notre exemple un interface Logger
et intégrons-la dans la classe CatsService
.
@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 classeCatsService
. 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épendanceLogger
utilisée parCatsService
. 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
:
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.