Heim >Web-Frontend >js-Tutorial >So schreiben Sie Unit-Tests und Etests für NestJS-Anwendungen

So schreiben Sie Unit-Tests und Etests für NestJS-Anwendungen

Mary-Kate Olsen
Mary-Kate OlsenOriginal
2025-01-13 06:22:43400Durchsuche

Einführung

Vor kurzem habe ich Unit-Tests und E2E-Tests für ein NestJS-Projekt geschrieben. Dies ist das erste Mal, dass ich Tests für ein Backend-Projekt schreibe, und ich fand, dass sich der Prozess von meinen Erfahrungen mit Frontend-Tests unterscheidet, was den Anfang schwierig macht. Nachdem ich mir einige Beispiele angeschaut habe, habe ich ein klareres Verständnis dafür gewonnen, wie man beim Testen vorgeht. Deshalb habe ich vor, einen Artikel zu schreiben, um meine Erkenntnisse aufzuzeichnen und weiterzugeben, um anderen zu helfen, die möglicherweise mit ähnlichen Verwirrungen konfrontiert sind.

Darüber hinaus habe ich ein Demoprojekt mit der entsprechenden Einheit und abgeschlossenen E2E-Tests zusammengestellt, das von Interesse sein könnte. Der Code wurde auf Github hochgeladen: https://github.com/woai3c/nestjs-demo.

Unterschied zwischen Unit-Tests und E2E-Tests

Unit-Tests und E2E-Tests sind Methoden des Softwaretests, haben jedoch unterschiedliche Ziele und Anwendungsbereiche.

Bei Unit-Tests geht es um die Prüfung und Verifizierung der kleinsten testbaren Einheit innerhalb der Software. Als Einheit kann beispielsweise eine Funktion oder eine Methode betrachtet werden. Beim Unit-Test stellen Sie erwartete Ausgaben für verschiedene Eingaben einer Funktion bereit und validieren die Richtigkeit ihrer Funktionsweise. Das Ziel von Unit-Tests besteht darin, Fehler innerhalb der Funktion schnell zu identifizieren, und sie sind einfach zu schreiben und schnell auszuführen.

Andererseits simulieren E2E-Tests häufig reale Benutzerszenarien, um die gesamte Anwendung zu testen. Beispielsweise verwendet das Frontend typischerweise einen Browser oder einen Headless-Browser zum Testen, während das Backend dies durch die Simulation von API-Aufrufen tut.

Innerhalb eines NestJS-Projekts können Komponententests einen bestimmten Dienst oder eine Methode eines Controllers bewerten, z. B. die Überprüfung, ob die Aktualisierungsmethode im Benutzermodul einen Benutzer korrekt aktualisiert. Ein E2E-Test kann jedoch eine vollständige Benutzerreise untersuchen, von der Erstellung eines neuen Benutzers über die Aktualisierung seines Passworts bis hin zum schließlichen Löschen des Benutzers, was mehrere Dienste und Controller umfasst.

Unit-Tests schreiben

Das Schreiben von Unit-Tests für eine Dienstprogrammfunktion oder -methode, die keine Schnittstellen beinhaltet, ist relativ einfach. Sie müssen lediglich die verschiedenen Eingaben berücksichtigen und den entsprechenden Testcode schreiben. Allerdings wird die Situation komplexer, sobald Schnittstellen ins Spiel kommen. Nehmen wir als Beispiel Code:

async validateUser(
  username: string,
  password: string,
): Promise<UserAccountDto> {
  const entity = await this.usersService.findOne({ username });
  if (!entity) {
    throw new UnauthorizedException('User not found');
  }
  if (entity.lockUntil && entity.lockUntil > Date.now()) {
    const diffInSeconds = Math.round((entity.lockUntil - Date.now()) / 1000);
    let message = `The account is locked. Please try again in ${diffInSeconds} seconds.`;
    if (diffInSeconds > 60) {
      const diffInMinutes = Math.round(diffInSeconds / 60);
      message = `The account is locked. Please try again in ${diffInMinutes} minutes.`;
    }
    throw new UnauthorizedException(message);
  }
  const passwordMatch = bcrypt.compareSync(password, entity.password);
  if (!passwordMatch) {
    // $inc update to increase failedLoginAttempts
    const update = {
      $inc: { failedLoginAttempts: 1 },
    };
    // lock account when the third try is failed
    if (entity.failedLoginAttempts + 1 >= 3) {
      // $set update to lock the account for 5 minutes
      update['$set'] = { lockUntil: Date.now() + 5 * 60 * 1000 };
    }
    await this.usersService.update(entity._id, update);
    throw new UnauthorizedException('Invalid password');
  }
  // if validation is sucessful, then reset failedLoginAttempts and lockUntil
  if (
    entity.failedLoginAttempts > 0 ||
    (entity.lockUntil && entity.lockUntil > Date.now())
  ) {
    await this.usersService.update(entity._id, {
      $set: { failedLoginAttempts: 0, lockUntil: null },
    });
  }
  return { userId: entity._id, username } as UserAccountDto;
}

Der obige Code ist eine Methode „validateUser“ in der Datei auth.service.ts, die hauptsächlich dazu dient, zu überprüfen, ob der Benutzername und das Passwort, die der Benutzer bei der Anmeldung eingegeben hat, korrekt sind. Es enthält die folgende Logik:

  1. Überprüfen Sie anhand des Benutzernamens, ob der Benutzer existiert. Wenn nicht, lösen Sie eine 401-Ausnahme aus (eine 404-Ausnahme ist ebenfalls möglich).
  2. Überprüfen Sie, ob der Benutzer gesperrt ist. Wenn ja, lösen Sie eine 401-Ausnahme mit einer entsprechenden Meldung aus.
  3. Verschlüsseln Sie das Passwort und vergleichen Sie es mit dem Passwort in der Datenbank; Wenn es falsch ist, lösen Sie eine 401-Ausnahme aus (drei aufeinanderfolgende fehlgeschlagene Anmeldeversuche sperren das Konto für 5 Minuten).
  4. Wenn die Anmeldung erfolgreich ist, löschen Sie alle zuvor fehlgeschlagenen Anmeldeversuche (falls zutreffend) und geben Sie die Benutzer-ID und den Benutzernamen an den nächsten Schritt zurück.

Wie zu sehen ist, umfasst die Methode „validateUser“ vier Verarbeitungslogiken, und wir müssen entsprechenden Komponententestcode für diese vier Punkte schreiben, um sicherzustellen, dass die gesamte Funktion „validateUser“ korrekt funktioniert.

Der erste Testfall

Wenn wir mit dem Schreiben von Unit-Tests beginnen, stoßen wir auf ein Problem: Die findOne-Methode muss mit der Datenbank interagieren und sucht über den Benutzernamen nach entsprechenden Benutzern in der Datenbank. Wenn jedoch jeder Unit-Test mit der Datenbank interagieren müsste, wäre das Testen sehr umständlich. Daher können wir gefälschte Daten vortäuschen, um dies zu erreichen.

Angenommen, wir haben einen Benutzer namens woai3c registriert. Anschließend können die Benutzerdaten während der Anmeldung in der Methode „validateUser“ über const entity = waiting this.usersService.findOne({ username }); abgerufen werden. Solange diese Codezeile die gewünschten Daten zurückgeben kann, gibt es auch ohne Datenbankinteraktion kein Problem. Wir können dies durch Scheindaten erreichen. Schauen wir uns nun den relevanten Testcode für die Methode „validateUser“ an:

async validateUser(
  username: string,
  password: string,
): Promise<UserAccountDto> {
  const entity = await this.usersService.findOne({ username });
  if (!entity) {
    throw new UnauthorizedException('User not found');
  }
  if (entity.lockUntil && entity.lockUntil > Date.now()) {
    const diffInSeconds = Math.round((entity.lockUntil - Date.now()) / 1000);
    let message = `The account is locked. Please try again in ${diffInSeconds} seconds.`;
    if (diffInSeconds > 60) {
      const diffInMinutes = Math.round(diffInSeconds / 60);
      message = `The account is locked. Please try again in ${diffInMinutes} minutes.`;
    }
    throw new UnauthorizedException(message);
  }
  const passwordMatch = bcrypt.compareSync(password, entity.password);
  if (!passwordMatch) {
    // $inc update to increase failedLoginAttempts
    const update = {
      $inc: { failedLoginAttempts: 1 },
    };
    // lock account when the third try is failed
    if (entity.failedLoginAttempts + 1 >= 3) {
      // $set update to lock the account for 5 minutes
      update['$set'] = { lockUntil: Date.now() + 5 * 60 * 1000 };
    }
    await this.usersService.update(entity._id, update);
    throw new UnauthorizedException('Invalid password');
  }
  // if validation is sucessful, then reset failedLoginAttempts and lockUntil
  if (
    entity.failedLoginAttempts > 0 ||
    (entity.lockUntil && entity.lockUntil > Date.now())
  ) {
    await this.usersService.update(entity._id, {
      $set: { failedLoginAttempts: 0, lockUntil: null },
    });
  }
  return { userId: entity._id, username } as UserAccountDto;
}

Wir erhalten die Benutzerdaten, indem wir die Methode „findOne“ von „usersService“ aufrufen, daher müssen wir die Methode „findOne“ von „usersService“ im Testcode nachahmen:

import { Test } from '@nestjs/testing';
import { AuthService } from '@/modules/auth/auth.service';
import { UsersService } from '@/modules/users/users.service';
import { UnauthorizedException } from '@nestjs/common';
import { TEST_USER_NAME, TEST_USER_PASSWORD } from '@tests/constants';
describe('AuthService', () => {
  let authService: AuthService; // Use the actual AuthService type
  let usersService: Partial<Record<keyof UsersService, jest.Mock>>;
  beforeEach(async () => {
    usersService = {
      findOne: jest.fn(),
    };
    const module = await Test.createTestingModule({
      providers: [        AuthService,
        {
          provide: UsersService,
          useValue: usersService,
        },
      ],
    }).compile();
    authService = module.get<AuthService>(AuthService);
  });
  describe('validateUser', () => {
    it('should throw an UnauthorizedException if user is not found', async () => {
      await expect(
        authService.validateUser(TEST_USER_NAME, TEST_USER_PASSWORD),
      ).rejects.toThrow(UnauthorizedException);
    });
    // other tests...
  });
});

Wir verwenden jest.fn(), um eine Funktion zurückzugeben, die den echten usersService.findOne() ersetzt. Wenn jetzt „usersService.findOne()“ aufgerufen wird, gibt es keinen Rückgabewert, sodass der erste Unit-Testfall bestanden wird:

beforeEach(async () => {
    usersService = {
      findOne: jest.fn(), // mock findOne method
    };
    const module = await Test.createTestingModule({
      providers: [        AuthService, // real AuthService, because we are testing its methods
        {
          provide: UsersService, // use mock usersService instead of real usersService
          useValue: usersService,
        },
      ],
    }).compile();
    authService = module.get<AuthService>(AuthService);
  });

Da findOne in const entity = waiting this.usersService.findOne({ username }); Da es sich bei der Methode „validateUser“ um eine nachgetäuschte Fake-Funktion ohne Rückgabewert handelt, könnte die zweite bis vierte Codezeile in der Methode „validateUser“ Folgendes ausführen:

it('should throw an UnauthorizedException if user is not found', async () => {
  await expect(
    authService.validateUser(TEST_USER_NAME, TEST_USER_PASSWORD),
  ).rejects.toThrow(UnauthorizedException);
});

Wird einen 401-Fehler auslösen, was wie erwartet ist.

Der zweite Testfall

Die zweite Logik in der Methode „validateUser“ besteht darin, mit dem entsprechenden Code wie folgt zu ermitteln, ob der Benutzer gesperrt ist:

if (!entity) {
  throw new UnauthorizedException('User not found');
}

Wie Sie sehen, können wir feststellen, dass das aktuelle Konto gesperrt ist, wenn in den Benutzerdaten eine Sperrzeit lockUntil vorliegt und die Endzeit der Sperre größer als die aktuelle Zeit ist. Daher müssen wir Benutzerdaten mit dem lockUntil-Feld verspotten:

async validateUser(
  username: string,
  password: string,
): Promise<UserAccountDto> {
  const entity = await this.usersService.findOne({ username });
  if (!entity) {
    throw new UnauthorizedException('User not found');
  }
  if (entity.lockUntil && entity.lockUntil > Date.now()) {
    const diffInSeconds = Math.round((entity.lockUntil - Date.now()) / 1000);
    let message = `The account is locked. Please try again in ${diffInSeconds} seconds.`;
    if (diffInSeconds > 60) {
      const diffInMinutes = Math.round(diffInSeconds / 60);
      message = `The account is locked. Please try again in ${diffInMinutes} minutes.`;
    }
    throw new UnauthorizedException(message);
  }
  const passwordMatch = bcrypt.compareSync(password, entity.password);
  if (!passwordMatch) {
    // $inc update to increase failedLoginAttempts
    const update = {
      $inc: { failedLoginAttempts: 1 },
    };
    // lock account when the third try is failed
    if (entity.failedLoginAttempts + 1 >= 3) {
      // $set update to lock the account for 5 minutes
      update['$set'] = { lockUntil: Date.now() + 5 * 60 * 1000 };
    }
    await this.usersService.update(entity._id, update);
    throw new UnauthorizedException('Invalid password');
  }
  // if validation is sucessful, then reset failedLoginAttempts and lockUntil
  if (
    entity.failedLoginAttempts > 0 ||
    (entity.lockUntil && entity.lockUntil > Date.now())
  ) {
    await this.usersService.update(entity._id, {
      $set: { failedLoginAttempts: 0, lockUntil: null },
    });
  }
  return { userId: entity._id, username } as UserAccountDto;
}

Im obigen Testcode wird zunächst ein Objekt „lockedUser“ definiert, das das von uns benötigte Feld „lockUntil“ enthält. Anschließend wird er als Rückgabewert für findOne verwendet, der durch „usersService.findOne.mockResolvedValueOnce(lockedUser);“ erreicht wird. Wenn also die Methode „validateUser“ ausgeführt wird, sind die darin enthaltenen Benutzerdaten die simulierten Daten, wodurch der zweite Testfall erfolgreich bestanden werden kann.

Unit-Test-Abdeckung

Die Unit-Test-Abdeckung (Code Coverage) ist eine Metrik, die beschreibt, wie viel des Anwendungscodes durch Unit-Tests abgedeckt oder getestet wurde. Er wird normalerweise als Prozentsatz ausgedrückt und gibt an, wie viele aller möglichen Codepfade von Testfällen abgedeckt wurden.

Die Unit-Test-Abdeckung umfasst normalerweise die folgenden Typen:

  • Zeilenabdeckung: Wie viele Codezeilen werden von den Tests abgedeckt.
  • Funktionsabdeckung: Wie viele Funktionen oder Methoden werden von den Tests abgedeckt.
  • Zweigabdeckung: Wie viele Codezweige werden von den Tests abgedeckt (z. B. if/else-Anweisungen).
  • Anweisungsabdeckung: Wie viele Anweisungen im Code werden von den Tests abgedeckt.

Die Unit-Test-Abdeckung ist eine wichtige Metrik zur Messung der Qualität von Unit-Tests, aber nicht die einzige Metrik. Eine hohe Abdeckungsrate kann dabei helfen, Fehler im Code zu erkennen, garantiert jedoch nicht die Qualität des Codes. Eine niedrige Abdeckungsrate kann bedeuten, dass es ungetesteten Code gibt, möglicherweise mit unentdeckten Fehlern.

Das Bild unten zeigt die Ergebnisse der Unit-Test-Abdeckung für ein Demoprojekt:

How to write unit tests and Etests for NestJS applications

Für Dateien wie Dienste und Controller ist es im Allgemeinen besser, eine höhere Unit-Test-Abdeckung zu haben, während für Dateien wie Module weder Unit-Tests geschrieben werden müssen noch möglich sind, da dies bedeutungslos ist. Das obige Bild stellt die Gesamtmetriken für die gesamte Unit-Test-Abdeckung dar. Wenn Sie die Testabdeckung für eine bestimmte Funktion anzeigen möchten, können Sie die Datei coverage/lcov-report/index.html im Stammverzeichnis des Projekts öffnen. Ich möchte beispielsweise die konkrete Testsituation für die Methode „validateUser“ sehen:

How to write unit tests and Etests for NestJS applications

Wie zu sehen ist, beträgt die ursprüngliche Unit-Test-Abdeckung für die Methode „validateUser“ nicht 100 %, und es gibt immer noch zwei Codezeilen, die nicht ausgeführt wurden. Dies spielt jedoch keine große Rolle, da es die vier wichtigsten Verarbeitungsknoten nicht beeinträchtigt und man eine hohe Testabdeckung nicht eindimensional anstreben sollte.

Schreiben von E2E-Tests

In den Unit-Tests haben wir gezeigt, wie man Unit-Tests für jede Funktion der Funktion „validateUser()“ schreibt und dabei simulierte Daten verwendet, um sicherzustellen, dass jede Funktion getestet werden kann. Beim e2E-Testen müssen wir reale Benutzerszenarien simulieren, daher ist zum Testen eine Verbindung zu einer Datenbank erforderlich. Daher interagieren alle Methoden im auth.service.ts-Modul, die wir testen werden, mit der Datenbank.

Das Authentifizierungsmodul umfasst hauptsächlich die folgenden Funktionen:

  • Anmeldung
  • Anmelden
  • Token-Aktualisierung
  • Benutzerinformationen lesen
  • Passwort ändern
  • Benutzer löschen

E2E-Tests müssen diese sechs Funktionen einzeln testen, beginnend mit der Registrierung und endend mit dem Löschen von Benutzern. Während des Tests können wir einen dedizierten Testbenutzer erstellen, der die Tests durchführt, und diesen Testbenutzer dann nach Abschluss löschen, um keine unnötigen Informationen in der Testdatenbank zu hinterlassen.

async validateUser(
  username: string,
  password: string,
): Promise<UserAccountDto> {
  const entity = await this.usersService.findOne({ username });
  if (!entity) {
    throw new UnauthorizedException('User not found');
  }
  if (entity.lockUntil && entity.lockUntil > Date.now()) {
    const diffInSeconds = Math.round((entity.lockUntil - Date.now()) / 1000);
    let message = `The account is locked. Please try again in ${diffInSeconds} seconds.`;
    if (diffInSeconds > 60) {
      const diffInMinutes = Math.round(diffInSeconds / 60);
      message = `The account is locked. Please try again in ${diffInMinutes} minutes.`;
    }
    throw new UnauthorizedException(message);
  }
  const passwordMatch = bcrypt.compareSync(password, entity.password);
  if (!passwordMatch) {
    // $inc update to increase failedLoginAttempts
    const update = {
      $inc: { failedLoginAttempts: 1 },
    };
    // lock account when the third try is failed
    if (entity.failedLoginAttempts + 1 >= 3) {
      // $set update to lock the account for 5 minutes
      update['$set'] = { lockUntil: Date.now() + 5 * 60 * 1000 };
    }
    await this.usersService.update(entity._id, update);
    throw new UnauthorizedException('Invalid password');
  }
  // if validation is sucessful, then reset failedLoginAttempts and lockUntil
  if (
    entity.failedLoginAttempts > 0 ||
    (entity.lockUntil && entity.lockUntil > Date.now())
  ) {
    await this.usersService.update(entity._id, {
      $set: { failedLoginAttempts: 0, lockUntil: null },
    });
  }
  return { userId: entity._id, username } as UserAccountDto;
}

Die beforeAll-Hook-Funktion wird ausgeführt, bevor alle Tests beginnen, sodass wir hier ein Testkonto TEST_USER_NAME registrieren können. Die AfterAll-Hook-Funktion wird ausgeführt, nachdem alle Tests beendet sind. Daher eignet es sich hier, das Testkonto TEST_USER_NAME zu löschen, und testet außerdem bequem die Registrierungs- und Löschfunktionen.

In den Komponententests des vorherigen Abschnitts haben wir relevante Komponententests rund um die Methode „validateUser“ geschrieben. Tatsächlich wird diese Methode während der Anmeldung ausgeführt, um zu überprüfen, ob das Konto und das Passwort des Benutzers korrekt sind. Daher wird dieser e2E-Test auch den Anmeldevorgang verwenden, um zu demonstrieren, wie e2E-Testfälle erstellt werden.

Der gesamte Anmeldetestprozess umfasst fünf kleine Tests:

import { Test } from '@nestjs/testing';
import { AuthService } from '@/modules/auth/auth.service';
import { UsersService } from '@/modules/users/users.service';
import { UnauthorizedException } from '@nestjs/common';
import { TEST_USER_NAME, TEST_USER_PASSWORD } from '@tests/constants';
describe('AuthService', () => {
  let authService: AuthService; // Use the actual AuthService type
  let usersService: Partial<Record<keyof UsersService, jest.Mock>>;
  beforeEach(async () => {
    usersService = {
      findOne: jest.fn(),
    };
    const module = await Test.createTestingModule({
      providers: [        AuthService,
        {
          provide: UsersService,
          useValue: usersService,
        },
      ],
    }).compile();
    authService = module.get<AuthService>(AuthService);
  });
  describe('validateUser', () => {
    it('should throw an UnauthorizedException if user is not found', async () => {
      await expect(
        authService.validateUser(TEST_USER_NAME, TEST_USER_PASSWORD),
      ).rejects.toThrow(UnauthorizedException);
    });
    // other tests...
  });
});

Diese fünf Tests lauten wie folgt:

  1. Erfolgreiche Anmeldung, Rückgabe von 200
  2. Wenn der Benutzer nicht existiert, lösen Sie eine 401-Ausnahme aus
  3. Wenn kein Passwort oder Benutzername angegeben wird, wird eine 400-Ausnahme ausgelöst
  4. Melden Sie sich mit dem falschen Passwort an und lösen Sie eine 401-Ausnahme aus
  5. Wenn das Konto gesperrt ist, lösen Sie eine 401-Ausnahme aus

Jetzt beginnen wir mit dem Schreiben der e2E-Tests:

beforeEach(async () => {
    usersService = {
      findOne: jest.fn(), // mock findOne method
    };
    const module = await Test.createTestingModule({
      providers: [        AuthService, // real AuthService, because we are testing its methods
        {
          provide: UsersService, // use mock usersService instead of real usersService
          useValue: usersService,
        },
      ],
    }).compile();
    authService = module.get<AuthService>(AuthService);
  });

Das Schreiben von e2E-Testcode ist relativ einfach: Sie rufen einfach die Schnittstelle auf und überprüfen dann das Ergebnis. Für den erfolgreichen Anmeldetest müssen wir beispielsweise nur überprüfen, ob das zurückgegebene Ergebnis 200 ist.

Die ersten vier Tests sind recht einfach. Schauen wir uns nun einen etwas komplizierteren e2E-Test an, bei dem überprüft wird, ob ein Konto gesperrt ist.

async validateUser(
  username: string,
  password: string,
): Promise<UserAccountDto> {
  const entity = await this.usersService.findOne({ username });
  if (!entity) {
    throw new UnauthorizedException('User not found');
  }
  if (entity.lockUntil && entity.lockUntil > Date.now()) {
    const diffInSeconds = Math.round((entity.lockUntil - Date.now()) / 1000);
    let message = `The account is locked. Please try again in ${diffInSeconds} seconds.`;
    if (diffInSeconds > 60) {
      const diffInMinutes = Math.round(diffInSeconds / 60);
      message = `The account is locked. Please try again in ${diffInMinutes} minutes.`;
    }
    throw new UnauthorizedException(message);
  }
  const passwordMatch = bcrypt.compareSync(password, entity.password);
  if (!passwordMatch) {
    // $inc update to increase failedLoginAttempts
    const update = {
      $inc: { failedLoginAttempts: 1 },
    };
    // lock account when the third try is failed
    if (entity.failedLoginAttempts + 1 >= 3) {
      // $set update to lock the account for 5 minutes
      update['$set'] = { lockUntil: Date.now() + 5 * 60 * 1000 };
    }
    await this.usersService.update(entity._id, update);
    throw new UnauthorizedException('Invalid password');
  }
  // if validation is sucessful, then reset failedLoginAttempts and lockUntil
  if (
    entity.failedLoginAttempts > 0 ||
    (entity.lockUntil && entity.lockUntil > Date.now())
  ) {
    await this.usersService.update(entity._id, {
      $set: { failedLoginAttempts: 0, lockUntil: null },
    });
  }
  return { userId: entity._id, username } as UserAccountDto;
}

Wenn sich ein Benutzer dreimal hintereinander nicht anmeldet, wird das Konto gesperrt. Daher können wir in diesem Test das Testkonto TEST_USER_NAME nicht verwenden, da dieses Konto bei erfolgreichem Test gesperrt wird und die folgenden Tests nicht fortgesetzt werden können. Wir müssen einen weiteren neuen Benutzer, TEST_USER_NAME2, speziell zum Testen der Kontosperre registrieren und diesen Benutzer löschen, nachdem der Test erfolgreich war. Wie Sie sehen, ist der Code für diesen e2E-Test ziemlich umfangreich und erfordert viel Auf- und Abbauarbeit, aber der eigentliche Testcode besteht nur aus diesen wenigen Zeilen:

import { Test } from '@nestjs/testing';
import { AuthService } from '@/modules/auth/auth.service';
import { UsersService } from '@/modules/users/users.service';
import { UnauthorizedException } from '@nestjs/common';
import { TEST_USER_NAME, TEST_USER_PASSWORD } from '@tests/constants';
describe('AuthService', () => {
  let authService: AuthService; // Use the actual AuthService type
  let usersService: Partial<Record<keyof UsersService, jest.Mock>>;
  beforeEach(async () => {
    usersService = {
      findOne: jest.fn(),
    };
    const module = await Test.createTestingModule({
      providers: [        AuthService,
        {
          provide: UsersService,
          useValue: usersService,
        },
      ],
    }).compile();
    authService = module.get<AuthService>(AuthService);
  });
  describe('validateUser', () => {
    it('should throw an UnauthorizedException if user is not found', async () => {
      await expect(
        authService.validateUser(TEST_USER_NAME, TEST_USER_PASSWORD),
      ).rejects.toThrow(UnauthorizedException);
    });
    // other tests...
  });
});

Das Schreiben von e2E-Testcode ist relativ einfach. Sie müssen keine Scheindaten oder Testabdeckung berücksichtigen. Es reicht aus, wenn der gesamte Systemprozess wie erwartet abläuft.

Ob Tests geschrieben werden sollen

Wenn möglich, empfehle ich generell, Tests zu schreiben. Dies kann die Robustheit, Wartbarkeit und Entwicklungseffizienz des Systems verbessern.

Verbesserung der Systemrobustheit

Beim Schreiben von Code konzentrieren wir uns normalerweise auf den Programmablauf bei normalen Eingaben, um sicherzustellen, dass die Kernfunktionalität ordnungsgemäß funktioniert. Allerdings übersehen wir oft einige Randfälle, wie zum Beispiel abnormale Eingaben. Das Schreiben von Tests ändert dies; Es zwingt Sie dazu, darüber nachzudenken, wie Sie mit diesen Fällen umgehen und angemessen reagieren, um so Abstürze zu verhindern. Man kann sagen, dass das Schreiben von Tests indirekt die Robustheit des Systems verbessert.

Verbesserung der Wartbarkeit

Ein neues Projekt zu übernehmen, das umfassende Tests beinhaltet, kann sehr angenehm sein. Sie dienen als Leitfaden und helfen Ihnen, die verschiedenen Funktionalitäten schnell zu verstehen. Wenn Sie sich einfach den Testcode ansehen, können Sie das erwartete Verhalten und die Randbedingungen jeder Funktion leicht erfassen, ohne jede Zeile des Funktionscodes durchgehen zu müssen.

Steigerung der Entwicklungseffizienz

Stellen Sie sich vor, ein Projekt, das eine Weile nicht aktualisiert wurde, erhält plötzlich neue Anforderungen. Nachdem Sie Änderungen vorgenommen haben, befürchten Sie möglicherweise, dass Fehler auftreten. Ohne Tests müssten Sie das gesamte Projekt erneut manuell testen – was Zeitverschwendung und Ineffizienz wäre. Bei vollständigen Tests können Sie mit einem einzigen Befehl feststellen, ob sich die Codeänderungen auf bestehende Funktionalitäten ausgewirkt haben. Auch wenn es Fehler gibt, können diese schnell lokalisiert und behoben werden.

Wann sollte man keine Tests schreiben?

Für kurzfristige Projekte und Projekte mit sehr schnellen Anforderungsiterationen wird das Schreiben von Tests nicht empfohlen. Beispielsweise sind für einige Projekte, die für Veranstaltungen gedacht sind, die nach Ende der Veranstaltung unbrauchbar sind, keine Tests erforderlich. Auch für Projekte, die sehr schnelle Anforderungsiterationen durchlaufen, habe ich gesagt, dass das Schreiben von Tests die Entwicklungseffizienz verbessern könnte, aber das basiert auf der Prämisse, dass Funktionsiterationen langsam sind. Wenn sich die gerade abgeschlossene Funktion in ein oder zwei Tagen ändert, muss der zugehörige Testcode neu geschrieben werden. Daher ist es besser, überhaupt keine Tests zu schreiben und sich stattdessen auf das Testteam zu verlassen, denn das Schreiben von Tests ist sehr zeitaufwändig und die Mühe nicht wert.

Abschluss

Nachdem ich ausführlich erklärt habe, wie man Unit-Tests und e2E-Tests für NestJS-Projekte schreibt, möchte ich noch einmal auf die Bedeutung des Testens hinweisen. Es kann die Robustheit, Wartbarkeit und Entwicklungseffizienz des Systems verbessern. Wenn Sie keine Möglichkeit haben, Tests zu schreiben, empfehle ich Ihnen, selbst ein Übungsprojekt zu starten oder an einigen Open-Source-Projekten teilzunehmen und Code dazu beizutragen. Für Open-Source-Projekte gelten im Allgemeinen strengere Codeanforderungen. Wenn Sie Code beisteuern, müssen Sie möglicherweise neue Testfälle schreiben oder bestehende ändern.

Referenzmaterialien

  • NestJS: Ein Framework zum Erstellen effizienter, skalierbarer serverseitiger Node.js-Anwendungen.
  • MongoDB: Eine NoSQL-Datenbank zur Datenspeicherung.
  • Jest: Ein Testframework für JavaScript und TypeScript.
  • Supertest: Eine Bibliothek zum Testen von HTTP-Servern.

Das obige ist der detaillierte Inhalt vonSo schreiben Sie Unit-Tests und Etests für NestJS-Anwendungen. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Stellungnahme:
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn