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ęexpectz 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.expectjest jednym z najczęściej używanych podejść do formułowania asercji.const add = require('../utils/math');: Importujemy funkcjęaddz plikumath.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 argumentdescribe()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ęaddz argumentami2i3oraz zapisujemy wynik do zmiennejresult.expect(result).to.equal(5);: To jest asercja pochodząca z Chai. Używamy funkcjiexpect, aby sprawdzić, czy wynikresultjest 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 funkcjaadd()poprawnie sumuje liczby ujemne.const result = add(-2, -3);: Wywołujemy funkcjęaddz argumentami-2i-3, a wynik zapisujemy do zmiennejresult.expect(result).to.equal(-5);: Znów sprawdzamy, czy wynik jest zgodny z oczekiwaniami. W tym przypadku oczekujemy, żeadd(-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.
Tworzenie Kolekcji: Możesz utworzyć kolekcję zapytań Postman, co jest szczególnie przydatne, gdy masz wiele endpointów do testowania.
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
Importowanie Bibliotek i Aplikacji:
const request = require('supertest');
const app = require('../app');
requestpochodzi z biblioteki Supertest, która służy do testowania aplikacji HTTP. Pozwala na symulowanie żądań HTTP do naszego serwera.appto nasza aplikacja Express.js, którą testujemy. Importujemy ją, aby Supertest mógł wysyłać do niej żądania.apppochodzi z pliku../app(czyli ścieżka do aplikacji Express).
Opisanie Bloku Testów:
describe('Test API /api/hello', () => {
...
});
describe()jest metodą dostarczoną przez Mocha, która grupuje związane ze sobą testy. Blokdescribeto 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.
- 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)– funkcjadonejest 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.
- 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 aplikacjiappi 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łówekContent-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 wbudowanegoexpectSupertesta) do sprawdzenia, czy w odpowiedzires.body.messageznajduje się wiadomość'Hello, World!'. Jest to kluczowy element, który mówi nam, że odpowiedź jest zgodna z oczekiwaniami.done();– wywołaniedone()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
- Test startuje, a Mocha wywołuje naszą aplikację Express przy użyciu Supertest.
- Supertest wysyła żądanie GET do endpointu
/api/hello. - Test oczekuje, że:
- Content-Type odpowiedzi będzie
application/json. - Kod statusu odpowiedzi będzie 200.
- Content-Type odpowiedzi będzie
- 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.messageznajduje się odpowiednia wartość ("Hello, World!").
- 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');
expectz 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 plikuauth.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 middlewareauth.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łówkaAuthorization.
res(response): Tworzymy zastępczy obiekt res:status: Jest to stub, który udaje funkcjęres.status(), i zwracathis(tj.res), aby umożliwić łańcuchowanie metod (np.res.status(401).json(...)).json: Jest to spy, który monitoruje wywołanie metodyres.json().
next: Tworzymy spy dla funkcji next. Chcemy sprawdzić, czynext()zostanie wywołane, ale w tym przypadku zakładamy, że nie powinno.auth(req, res, next): Wywołujemy middlewareauthz wcześniej utworzonymi obiektami req, res i next. Celem jest symulacja rzeczywistego żądania.expect(res.status.calledWith(401)).to.be.true: Sprawdzamy, czyres.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, czyres.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 zheader, 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ę, żenext()zostanie wywołane, ponieważ dostarczony jest token.auth(req, res, next): Wywołujemy middlewareauth.expect(next.calledOnce).to.be.true: Sprawdzamy, czynext()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:
- Testowanie Jednostkowe: Przedstawiliśmy, jak pisać testy jednostkowe za pomocą Mocha i Chai, aby weryfikować poszczególne funkcje aplikacji.
- Testowanie API: Omówiliśmy użycie Postman do testowania endpointów ręcznie oraz Supertest do automatycznych testów integracyjnych.
- 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ę.