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.nestjsOu, pour Sinon :
$ npm i -D @automock/sinon @automock/adapters.nestjsExemple
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
CatsServiceen utilisantTestBed.create(CatsService).compile(). - Obtenons l’instance réelle de
CatsServiceet une instance simulée deDatabaseen utilisantunitetunitRef.get(Database), respectivement. - Nous simulons la méthode
getCatsde la classeDatabasepour renvoyer une liste prédéfinie de chats. - Nous appelons ensuite la méthode
getAllCatsdeCatsServiceet vérifions qu’elle interagit correctement avec la classeDatabaseet 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éunitrepré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éunitRefsert de référence aux dépendances de la classe testée. Dans notre exemple, elle fait référence à la dépendanceLoggerutilisé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.