Testowanie Aplikacji Express.js

Tworzenie solidnych aplikacji webowych wymaga nie tylko pisania funkcjonalnego kodu, ale również zapewnienia, że aplikacja działa poprawnie w każdych okolicznościach. Testowanie jest kluczowym elementem procesu rozwoju oprogramowania, szczególnie w przypadku aplikacji Express.js. W tym artykule omówimy testowanie jednostkowe, testowanie API oraz testowanie middleware. Nauczysz się, jak skonfigurować swoje środowisko do testowania i pisać efektywne testy.

Testowanie Jednostkowe i Integracyjne

Wprowadzenie do Testów Jednostkowych w Node.js

Testy jednostkowe polegają na izolowanym testowaniu poszczególnych komponentów aplikacji, takich jak funkcje czy moduły. Dzięki nim możemy upewnić się, że każda część aplikacji działa poprawnie.

Do testowania aplikacji Node.js używamy popularnych narzędzi, takich jak Mocha (framework testowy) i Chai (biblioteka asercji).

Instalacja i Konfiguracja Narzędzi (Mocha, Chai)

Aby zacząć, zainstaluj Mocha, Chai oraz narzędzie nodemon, które pomoże w uruchamianiu aplikacji podczas testowania:

				
					npm install --save-dev mocha chai nodemon

				
			

Następnie dodaj konfigurację do pliku package.json, aby uruchamiać testy:

				
					"scripts": {
  "test": "mocha"
}

				
			

Pisanie Pierwszego Testu Jednostkowego

Przyjrzyjmy się prostemu przykładowi funkcji, którą przetestujemy:

				
					// utils/math.js
function add(a, b) {
  return a + b;
}

module.exports = add;

				
			

Teraz stwórzmy test dla tej funkcji:

				
					// test/math.test.js
const { expect } = require('chai');
const add = require('../utils/math');

describe('Test funkcji add()', () => {
  it('powinno zwrócić poprawną sumę dwóch liczb', () => {
    const result = add(2, 3);
    expect(result).to.equal(5);
  });

  it('powinno zwrócić liczbę ujemną dla odpowiednich argumentów', () => {
    const result = add(-2, -3);
    expect(result).to.equal(-5);
  });
});

				
			

W powyższym kodzie korzystamy z Mocha do struktury testów oraz Chai do tworzenia asercji.

Teraz wyjaśnię krok po kroku, co robi każdy fragment tego testu.

1. Importowanie Potrzebnych Bibliotek i Funkcji

				
					const { expect } = require('chai');
const add = require('../utils/math');

				
			
  • const { expect } = require('chai');: Importujemy funkcję expect z biblioteki Chai. Chai jest biblioteką do testowania, która oferuje różne asercje (stwierdzenia), aby sprawdzać, czy wynik działania funkcji jest zgodny z oczekiwaniami. expect jest jednym z najczęściej używanych podejść do formułowania asercji.

  • const add = require('../utils/math');: Importujemy funkcję add z pliku math.js. Ta funkcja będzie przedmiotem naszego testu.

2. Opisanie Grupy Testów za Pomocą describe()

				
					describe('Test funkcji add()', () => {
  // ...
});

				
			
  • describe(): Jest to funkcja pochodząca z Mocha – frameworku do testowania. Służy ona do opisywania grupy testów. Pierwszy argument describe() to opis testowanej grupy, który ma informować, co testujemy. W naszym przypadku opisaliśmy grupę jako "Test funkcji add()".

Wewnątrz funkcji describe() definiujemy zestaw testów, które odnoszą się do konkretnej funkcjonalności – w tym przypadku funkcji add().

3. Pisanie Konkretnego Testu z it()

				
					it('powinno zwrócić poprawną sumę dwóch liczb', () => {
  const result = add(2, 3);
  expect(result).to.equal(5);
});

				
			
  • it(): To funkcja Mocha używana do opisywania konkretnego przypadku testowego. Pierwszy argument tej funkcji to opis testu. "powinno zwrócić poprawną sumę dwóch liczb" to opis tego, czego oczekujemy w tym konkretnym teście.

  • const result = add(2, 3);: W tej linii wywołujemy funkcję add z argumentami 2 i 3 oraz zapisujemy wynik do zmiennej result.

  • expect(result).to.equal(5);: To jest asercja pochodząca z Chai. Używamy funkcji expect, aby sprawdzić, czy wynik result jest równy oczekiwanej wartości (5). Jeśli wynik będzie różny, test zakończy się niepowodzeniem.

4. Drugi Testowy Przypadek

				
					it('powinno zwrócić liczbę ujemną dla odpowiednich argumentów', () => {
  const result = add(-2, -3);
  expect(result).to.equal(-5);
});

				
			

W tej części piszemy drugi przypadek testowy:

  • Opis testu: "powinno zwrócić liczbę ujemną dla odpowiednich argumentów" mówi nam, co dokładnie jest testowane. Sprawdzamy, czy funkcja add() poprawnie sumuje liczby ujemne.

  • const result = add(-2, -3);: Wywołujemy funkcję add z argumentami -2 i -3, a wynik zapisujemy do zmiennej result.

  • expect(result).to.equal(-5);: Znów sprawdzamy, czy wynik jest zgodny z oczekiwaniami. W tym przypadku oczekujemy, że add(-2, -3) zwróci -5.

Aby uruchomić testy, wykonaj:

				
					npm test

				
			

Możesz zauważyć, że Mocha tworzy raporty testowe, a każde niepowodzenie jest jasno opisane.

Testowanie API

Używanie Postman do Testowania Endpointów

Postman to popularne narzędzie do ręcznego testowania endpointów API. Dzięki Postman możesz wysyłać różne żądania HTTP (GET, POST, PUT, DELETE) do serwera i weryfikować odpowiedzi.

  1. Tworzenie Kolekcji: Możesz utworzyć kolekcję zapytań Postman, co jest szczególnie przydatne, gdy masz wiele endpointów do testowania.

  2. Testy Automatyczne: W Postman możesz pisać proste skrypty do automatycznego weryfikowania odpowiedzi, np.:

				
					pm.test("Status code is 200", function () {
  pm.response.to.have.status(200);
});

				
			

Automatyczne Testy Integracyjne z Supertest

Supertest to narzędzie, które pozwala pisać automatyczne testy dla endpointów Express.js. Zainstaluj Supertest:

				
					npm install --save-dev supertest

				
			

Poniżej znajdziesz przykład prostego testu integracyjnego dla API:

				
					// app.js
const express = require('express');
const app = express();

app.get('/api/hello', (req, res) => {
  res.status(200).json({ message: 'Hello, World!' });
});

module.exports = app;

				
			

Teraz stwórzmy plik testowy:

				
					// test/app.test.js
const request = require('supertest');
const app = require('../app');

describe('Test API /api/hello', () => {
  it('powinno zwrócić status 200 i wiadomość "Hello, World!"', (done) => {
    request(app)
      .get('/api/hello')
      .expect('Content-Type', /json/)
      .expect(200)
      .end((err, res) => {
        if (err) return done(err);
        expect(res.body.message).to.equal('Hello, World!');
        done();
      });
  });
});

				
			

W powyższym kodzie używamy Supertest do wysyłania żądania do serwera oraz expect z Chai do weryfikacji odpowiedzi.

Wyjaśnienie Kodu Kroku po Kroku

  1. Importowanie Bibliotek i Aplikacji:

				
					const request = require('supertest');
const app = require('../app');

				
			
    • request pochodzi z biblioteki Supertest, która służy do testowania aplikacji HTTP. Pozwala na symulowanie żądań HTTP do naszego serwera.
    • app to nasza aplikacja Express.js, którą testujemy. Importujemy ją, aby Supertest mógł wysyłać do niej żądania. app pochodzi z pliku ../app (czyli ścieżka do aplikacji Express).
  1. Opisanie Bloku Testów:

				
					describe('Test API /api/hello', () => {
  ...
});

				
			
    • describe() jest metodą dostarczoną przez Mocha, która grupuje związane ze sobą testy. Blok describe to opisowy blok, który pomaga zorganizować testy w logiczne grupy.
    • 'Test API /api/hello' – to nazwa naszej grupy testów. Opisuje, co dokładnie testujemy. W tym przypadku jest to endpoint /api/hello.
  1. Zdefiniowanie Testu:
				
					it('powinno zwrócić status 200 i wiadomość "Hello, World!"', (done) => {
  ...
});

				
			
    • it() to metoda, która definiuje konkretny przypadek testowy.
    • 'powinno zwrócić status 200 i wiadomość "Hello, World!"' – to opis testu. Opisuje, co dokładnie jest sprawdzane.
    • (done) – funkcja done jest argumentem callback, który jest wywoływany, aby poinformować Mocha, że asynchroniczny test został zakończony. Używamy tego, gdy test wymaga asynchronicznej operacji, takiej jak oczekiwanie na odpowiedź z serwera.
  1. Wysyłanie Żądania do Endpointu:
				
					request(app)
  .get('/api/hello')
  .expect('Content-Type', /json/)
  .expect(200)
  .end((err, res) => {
    if (err) return done(err);
    expect(res.body.message).to.equal('Hello, World!');
    done();
  });

				
			
    • request(app) – tutaj używamy Supertest do uruchomienia aplikacji app i wysłania do niej żądania HTTP.
    • .get('/api/hello') – określa rodzaj żądania, które chcemy wysłać. W tym przypadku jest to żądanie GET do endpointu /api/hello.
    • .expect('Content-Type', /json/) – sprawdza, czy odpowiedź ma nagłówek Content-Type, który pasuje do wyrażenia regularnego /json/. Spodziewamy się, że odpowiedź powinna być w formacie JSON.
    • .expect(200) – oczekujemy, że serwer zwróci kod statusu 200 OK, co oznacza, że żądanie zostało pomyślnie przetworzone.
    • .end((err, res) => { ... }) – metoda .end() kończy żądanie i przyjmuje callback (err, res), który pozwala na obsługę odpowiedzi oraz błędów.
      • if (err) return done(err); – jeśli podczas wykonywania żądania wystąpił błąd, zakończ test i zwróć błąd.
      • expect(res.body.message).to.equal('Hello, World!'); – używamy Chai (lub wbudowanego expect Supertesta) do sprawdzenia, czy w odpowiedzi res.body.message znajduje się wiadomość 'Hello, World!'. Jest to kluczowy element, który mówi nam, że odpowiedź jest zgodna z oczekiwaniami.
      • done(); – wywołanie done() informuje Mocha, że test się zakończył i wszystko jest poprawne. Bez tego test mógłby „wisieć” w nieskończoność, oczekując na zakończenie asynchronicznej operacji.

Przebieg Testu

  1. Test startuje, a Mocha wywołuje naszą aplikację Express przy użyciu Supertest.
  2. Supertest wysyła żądanie GET do endpointu /api/hello.
  3. Test oczekuje, że:
    • Content-Type odpowiedzi będzie application/json.
    • Kod statusu odpowiedzi będzie 200.
  4. Po otrzymaniu odpowiedzi:
    • Jeśli wystąpi błąd (np. serwer zwróci kod błędu inny niż oczekiwany), test zwraca ten błąd.
    • Jeśli nie ma błędu, sprawdzamy, czy w res.body.message znajduje się odpowiednia wartość ("Hello, World!").
  5. Jeśli wszystko jest zgodne z oczekiwaniami, wywołujemy done() i test kończy się sukcesem.

Kod test/app.test.js jest testem integracyjnym, który sprawdza, czy endpoint /api/hello działa poprawnie. Wykorzystujemy tu Supertest, Mocha, i Chai, aby przetestować odpowiedzi HTTP. Ten rodzaj testów jest szczególnie przydatny, ponieważ symuluje rzeczywiste żądania wysyłane do aplikacji, dzięki czemu możemy mieć pewność, że nasze API zachowuje się zgodnie z oczekiwaniami.

Pisanie Testów dla Middleware

Middleware w Express.js jest kluczowym elementem przetwarzania żądań HTTP, a jego testowanie jest istotne, aby upewnić się, że funkcje pośrednie działają zgodnie z oczekiwaniami.

Przykładowe middleware sprawdzające autoryzację użytkownika:

				
					// middleware/auth.js
function auth(req, res, next) {
  const token = req.header('Authorization');

  if (!token) {
    return res.status(401).json({ message: 'Brak tokena, autoryzacja odrzucona' });
  }

  try {
    // Zakładamy weryfikację tokena (np. JWT)
    req.user = { id: 1 }; // Przykładowe dane
    next();
  } catch (error) {
    res.status(401).json({ message: 'Token jest nieprawidłowy' });
  }
}

module.exports = auth;

				
			

Testowanie tego middleware:

				
					// test/auth.test.js
const { expect } = require('chai');
const sinon = require('sinon');
const auth = require('../middleware/auth');

describe('Middleware auth', () => {
  it('powinno zwrócić 401, jeśli token nie jest dostarczony', () => {
    const req = { header: sinon.stub().returns(null) };
    const res = {
      status: sinon.stub().returnsThis(),
      json: sinon.spy(),
    };
    const next = sinon.spy();

    auth(req, res, next);

    expect(res.status.calledWith(401)).to.be.true;
    expect(res.json.calledWith({ message: 'Brak tokena, autoryzacja odrzucona' })).to.be.true;
  });

  it('powinno wywołać next() jeśli token jest dostarczony', () => {
    const req = { header: sinon.stub().returns('Bearer token') };
    const res = {};
    const next = sinon.spy();

    auth(req, res, next);

    expect(next.calledOnce).to.be.true;
  });
});

				
			

W powyższym kodzie korzystamy z biblioteki sinon do tworzenia sztucznych obiektów (stub, spy), co umożliwia sprawdzanie wywołań funkcji. Middleware jest przetestowane zarówno w przypadku, gdy token jest dostarczony, jak i wtedy, gdy go brakuje.

Poniżej znajdziesz wyjaśnienie poszczególnych części tego kodu.

1. Importowanie Potrzebnych Bibliotek i Middleware

				
					const { expect } = require('chai');
const sinon = require('sinon');
const auth = require('../middleware/auth');

				
			
  • expect z Chai: Służy do tworzenia asercji w testach. Dzięki temu możemy sprawdzić, czy wyniki testowanej funkcji są zgodne z oczekiwaniami.
  • sinon: Jest to narzędzie, które pomaga tworzyć „sztuczne” obiekty, takie jak stub (zastępczy obiekt, który udaje rzeczywistą funkcję) lub spy (rejestrator, który sprawdza, czy i jak często funkcja jest wywoływana). Dzięki temu możemy kontrolować i monitorować zachowanie kodu.
  • auth: Importujemy middleware do autoryzacji z pliku auth.js.

2. Opis Testów

Używamy describe() i it() do organizowania testów:

  • describe(): Grupuje testy związane z danym komponentem — w tym przypadku middleware auth.
  • it(): Definiuje pojedynczy przypadek testowy.

3. Test 1: Gdy Token Nie Jest Dostarczony

				
					it('powinno zwrócić 401, jeśli token nie jest dostarczony', () => {
  const req = { header: sinon.stub().returns(null) };
  const res = {
    status: sinon.stub().returnsThis(),
    json: sinon.spy(),
  };
  const next = sinon.spy();

  auth(req, res, next);

  expect(res.status.calledWith(401)).to.be.true;
  expect(res.json.calledWith({ message: 'Brak tokena, autoryzacja odrzucona' })).to.be.true;
});

				
			
  • req (request): Tworzymy zastępczy obiekt req z użyciem Sinon.

    • req.header: To stub, który symuluje funkcję req.header(), aby zwrócić null. Zastępujemy rzeczywiste zachowanie tej funkcji, aby udawała, że żądanie nie zawiera nagłówka Authorization.
  • res (response): Tworzymy zastępczy obiekt res:

    • status: Jest to stub, który udaje funkcję res.status(), i zwraca this (tj. res), aby umożliwić łańcuchowanie metod (np. res.status(401).json(...)).
    • json: Jest to spy, który monitoruje wywołanie metody res.json().
  • next: Tworzymy spy dla funkcji next. Chcemy sprawdzić, czy next() zostanie wywołane, ale w tym przypadku zakładamy, że nie powinno.

  • auth(req, res, next): Wywołujemy middleware auth z wcześniej utworzonymi obiektami req, res i next. Celem jest symulacja rzeczywistego żądania.

  • expect(res.status.calledWith(401)).to.be.true: Sprawdzamy, czy res.status() zostało wywołane z wartością 401 (oczekujemy statusu Unauthorized).

  • expect(res.json.calledWith({ message: 'Brak tokena, autoryzacja odrzucona' })).to.be.true: Sprawdzamy, czy res.json() zostało wywołane z odpowiednim komunikatem o błędzie.

Podsumowując: Test sprawdza, czy gdy nie podamy tokena, middleware zwróci status 401 oraz odpowiednią wiadomość.

4. Test 2: Gdy Token Jest Dostarczony

				
					it('powinno wywołać next() jeśli token jest dostarczony', () => {
  const req = { header: sinon.stub().returns('Bearer token') };
  const res = {};
  const next = sinon.spy();

  auth(req, res, next);

  expect(next.calledOnce).to.be.true;
});

				
			
  • req (request): Tworzymy obiekt req z header, który zwraca symulowany token (Bearer token).

  • res: W tym przypadku obiekt res jest pusty, ponieważ w tej sytuacji (gdy token jest prawidłowy) middleware nie powinien z niego korzystać.

  • next: Tworzymy spy dla funkcji next. Tym razem spodziewamy się, że next() zostanie wywołane, ponieważ dostarczony jest token.

  • auth(req, res, next): Wywołujemy middleware auth.

  • expect(next.calledOnce).to.be.true: Sprawdzamy, czy next() zostało wywołane raz, co oznacza, że middleware przepuściło żądanie dalej, zakładając, że autoryzacja jest poprawna.

Podsumowując: Test sprawdza, czy w przypadku dostarczenia tokena middleware poprawnie wywoła funkcję next().

Middleware: Weryfikuje, czy użytkownik przesyła nagłówek Authorization. Jeśli nie — zwraca błąd 401. Jeśli token jest, przekazuje kontrolę dalej, wywołując next().

Sinon:

  • sinon.stub(): Służy do tworzenia zastępczych funkcji, które zwracają określone wartości. Używamy go do symulowania żądań HTTP.
  • sinon.spy(): Monitoruje, czy funkcja została wywołana oraz ile razy.

Chai:

  • expect(): Służy do tworzenia asercji i sprawdzania, czy określone warunki są spełnione (np. expect(next.calledOnce).to.be.true).

Cele testów:

    • Sprawdzenie, czy middleware zwraca status 401, gdy token nie jest dostarczony.
    • Sprawdzenie, czy next() jest wywołane, gdy token jest dostarczony.

Dzięki takim testom masz pewność, że Twój middleware działa zgodnie z oczekiwaniami, weryfikuje tokeny i kontroluje, kto ma dostęp do dalszej części aplikacji.

Podsumowanie

Testowanie aplikacji Express.js jest kluczowe, aby zapewnić, że aplikacja działa zgodnie z oczekiwaniami i jest odporna na różne błędy. W tym artykule:

  1. Testowanie Jednostkowe: Przedstawiliśmy, jak pisać testy jednostkowe za pomocą Mocha i Chai, aby weryfikować poszczególne funkcje aplikacji.
  2. Testowanie API: Omówiliśmy użycie Postman do testowania endpointów ręcznie oraz Supertest do automatycznych testów integracyjnych.
  3. Testowanie Middleware: Pokazaliśmy, jak testować middleware w Express.js za pomocą sinon.

Te techniki są istotne dla zapewnienia jakości aplikacji. Dzięki regularnemu testowaniu możemy w łatwy sposób identyfikować problemy i naprawiać błędy, zanim trafią one na produkcję.