Kompleksowy przewodnik po testach integracyjnych, skupiający się na testowaniu API za pomocą Supertest, obejmujący konfigurację, najlepsze praktyki i zaawansowane techniki.
Testy integracyjne: Opanowanie testowania API za pomocą Supertest
W świecie tworzenia oprogramowania, kluczowe jest zapewnienie, że poszczególne komponenty działają poprawnie w izolacji (testy jednostkowe). Równie ważne jest jednak sprawdzenie, czy te komponenty płynnie ze sobą współpracują. W tym miejscu do gry wchodzą testy integracyjne. Testy integracyjne koncentrują się na weryfikacji interakcji między różnymi modułami lub usługami w aplikacji. Ten artykuł zagłębia się w testy integracyjne, ze szczególnym uwzględnieniem testowania API za pomocą Supertest, potężnej i przyjaznej dla użytkownika biblioteki do testowania asercji HTTP w Node.js.
Czym są testy integracyjne?
Testy integracyjne to rodzaj testowania oprogramowania, który łączy poszczególne moduły oprogramowania i testuje je jako grupę. Celem jest wykrycie defektów w interakcjach między zintegrowanymi jednostkami. W przeciwieństwie do testów jednostkowych, które koncentrują się na poszczególnych komponentach, testy integracyjne weryfikują przepływ danych i przepływ sterowania między modułami. Typowe podejścia do testów integracyjnych obejmują:
- Integracja zstępująca (top-down): Rozpoczynanie od modułów najwyższego poziomu i integracja w dół.
- Integracja wstępująca (bottom-up): Rozpoczynanie od modułów najniższego poziomu i integracja w górę.
- Integracja „wielkiego wybuchu” (big-bang): Integracja wszystkich modułów jednocześnie. Podejście to jest generalnie mniej zalecane ze względu na trudności w izolowaniu problemów.
- Integracja kanapkowa (sandwich): Połączenie integracji zstępującej i wstępującej.
W kontekście API, testy integracyjne polegają na weryfikacji, czy różne API działają poprawnie razem, czy dane przekazywane między nimi są spójne i czy cały system funkcjonuje zgodnie z oczekiwaniami. Wyobraźmy sobie na przykład aplikację e-commerce z oddzielnymi API do zarządzania produktami, uwierzytelniania użytkowników i przetwarzania płatności. Testy integracyjne zapewniłyby, że te API komunikują się poprawnie, umożliwiając użytkownikom przeglądanie produktów, bezpieczne logowanie i dokonywanie zakupów.
Dlaczego testowanie integracyjne API jest ważne?
Testowanie integracyjne API jest kluczowe z kilku powodów:
- Zapewnia niezawodność systemu: Pomaga wcześnie zidentyfikować problemy integracyjne w cyklu rozwoju, zapobiegając nieoczekiwanym awariom w środowisku produkcyjnym.
- Weryfikuje integralność danych: Sprawdza, czy dane są poprawnie przesyłane i przekształcane między różnymi API.
- Poprawia wydajność aplikacji: Może ujawnić wąskie gardła wydajności związane z interakcjami API.
- Zwiększa bezpieczeństwo: Może zidentyfikować luki w zabezpieczeniach wynikające z nieprawidłowej integracji API. Na przykład, zapewnienie prawidłowego uwierzytelniania i autoryzacji, gdy API komunikują się ze sobą.
- Redukuje koszty rozwoju: Naprawianie problemów integracyjnych na wczesnym etapie jest znacznie tańsze niż zajmowanie się nimi później w cyklu życia oprogramowania.
Rozważmy globalną platformę rezerwacji podróży. Testowanie integracyjne API jest kluczowe dla zapewnienia płynnej komunikacji między API obsługującymi rezerwacje lotów, rezerwacje hoteli i bramki płatnicze z różnych krajów. Nieprawidłowa integracja tych API mogłaby prowadzić do błędnych rezerwacji, nieudanych płatności i złego doświadczenia użytkownika, negatywnie wpływając na reputację i przychody platformy.
Przedstawiamy Supertest: Potężne narzędzie do testowania API
Supertest to wysokopoziomowa abstrakcja do testowania żądań HTTP. Zapewnia wygodne i płynne API do wysyłania żądań do aplikacji i sprawdzania odpowiedzi. Zbudowany na Node.js, Supertest jest specjalnie zaprojektowany do testowania serwerów HTTP w Node.js. Działa wyjątkowo dobrze z popularnymi frameworkami testowymi, takimi jak Jest i Mocha.
Kluczowe cechy Supertest:
- Łatwy w użyciu: Supertest oferuje proste i intuicyjne API do wysyłania żądań HTTP i tworzenia asercji.
- Testowanie asynchroniczne: Płynnie obsługuje operacje asynchroniczne, co czyni go idealnym do testowania API opartych na logice asynchronicznej.
- Płynny interfejs: Zapewnia płynny interfejs, umożliwiając łączenie metod w celu uzyskania zwięzłych i czytelnych testów.
- Kompleksowe wsparcie asercji: Obsługuje szeroki zakres asercji do weryfikacji kodów statusu, nagłówków i treści odpowiedzi.
- Integracja z frameworkami testowymi: Płynnie integruje się z popularnymi frameworkami testowymi, takimi jak Jest i Mocha, umożliwiając korzystanie z istniejącej infrastruktury testowej.
Konfiguracja środowiska testowego
Zanim zaczniemy, skonfigurujmy podstawowe środowisko testowe. Zakładamy, że masz zainstalowany Node.js i npm (lub yarn). Użyjemy Jest jako naszego frameworka testowego i Supertest do testowania API.
- Stwórz projekt Node.js:
mkdir api-testing-example
cd api-testing-example
npm init -y
- Zainstaluj zależności:
npm install --save-dev jest supertest
npm install express # Lub preferowany framework do tworzenia API
- Skonfiguruj Jest: Dodaj następujący fragment do pliku
package.json
:
{
"scripts": {
"test": "jest"
}
}
- Stwórz prosty punkt końcowy API: Utwórz plik o nazwie
app.js
(lub podobnej) z następującym kodem:
const express = require('express');
const app = express();
const port = 3000;
app.get('/hello', (req, res) => {
res.send('Hello, World!');
});
app.listen(port, () => {
console.log(`Example app listening at http://localhost:${port}`);
});
module.exports = app; // Eksportuj do celów testowych
Pisanie pierwszego testu z Supertest
Teraz, gdy mamy już skonfigurowane środowisko, napiszmy prosty test z Supertest, aby zweryfikować nasz punkt końcowy API. Utwórz plik o nazwie app.test.js
(lub podobnej) w głównym katalogu projektu:
const request = require('supertest');
const app = require('./app');
describe('GET /hello', () => {
it('responds with 200 OK and returns "Hello, World!"', async () => {
const response = await request(app).get('/hello');
expect(response.statusCode).toBe(200);
expect(response.text).toBe('Hello, World!');
});
});
Wyjaśnienie:
- Importujemy
supertest
i naszą aplikację Express. - Używamy
describe
do grupowania naszych testów. - Używamy
it
do zdefiniowania konkretnego przypadku testowego. - Używamy
request(app)
, aby utworzyć agenta Supertest, który będzie wysyłał żądania do naszej aplikacji. - Używamy
.get('/hello')
, aby wysłać żądanie GET do punktu końcowego/hello
. - Używamy
await
, aby poczekać na odpowiedź. Metody Supertest zwracają promise'y, co pozwala nam używać async/await dla czystszego kodu. - Używamy
expect(response.statusCode).toBe(200)
, aby potwierdzić, że kod statusu odpowiedzi to 200 OK. - Używamy
expect(response.text).toBe('Hello, World!')
, aby potwierdzić, że treść odpowiedzi to „Hello, World!”.
Aby uruchomić test, wykonaj następujące polecenie w terminalu:
npm test
Jeśli wszystko jest poprawnie skonfigurowane, powinieneś zobaczyć, że test przechodzi pomyślnie.
Zaawansowane techniki Supertest
Supertest oferuje szeroki zakres funkcji do zaawansowanego testowania API. Przyjrzyjmy się niektórym z nich.
1. Wysyłanie ciała żądania
Aby wysłać dane w ciele żądania, możesz użyć metody .send()
. Na przykład, stwórzmy punkt końcowy, który akceptuje dane w formacie JSON:
app.post('/users', express.json(), (req, res) => {
const { name, email } = req.body;
// Symulacja tworzenia użytkownika w bazie danych
const user = { id: Date.now(), name, email };
res.status(201).json(user);
});
Oto jak można przetestować ten punkt końcowy za pomocą Supertest:
describe('POST /users', () => {
it('creates a new user', async () => {
const userData = {
name: 'John Doe',
email: 'john.doe@example.com',
};
const response = await request(app)
.post('/users')
.send(userData)
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe(userData.name);
expect(response.body.email).toBe(userData.email);
});
});
Wyjaśnienie:
- Używamy
.post('/users')
, aby wysłać żądanie POST do punktu końcowego/users
. - Używamy
.send(userData)
, aby wysłać obiektuserData
w ciele żądania. Supertest automatycznie ustawia nagłówekContent-Type
naapplication/json
. - Używamy
.expect(201)
, aby potwierdzić, że kod statusu odpowiedzi to 201 Created. - Używamy
expect(response.body).toHaveProperty('id')
, aby potwierdzić, że treść odpowiedzi zawiera właściwośćid
. - Używamy
expect(response.body.name).toBe(userData.name)
orazexpect(response.body.email).toBe(userData.email)
, aby potwierdzić, że właściwościname
iemail
w treści odpowiedzi odpowiadają danym, które wysłaliśmy w żądaniu.
2. Ustawianie nagłówków
Aby ustawić niestandardowe nagłówki w żądaniach, możesz użyć metody .set()
. Jest to przydatne do ustawiania tokenów uwierzytelniających, typów zawartości lub innych niestandardowych nagłówków.
describe('GET /protected', () => {
it('requires authentication', async () => {
const response = await request(app).get('/protected').expect(401);
});
it('returns 200 OK with a valid token', async () => {
// Symulacja uzyskania ważnego tokenu
const token = 'valid-token';
const response = await request(app)
.get('/protected')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.text).toBe('Protected Resource');
});
});
Wyjaśnienie:
- Używamy
.set('Authorization', `Bearer ${token}`)
, aby ustawić nagłówekAuthorization
naBearer ${token}
.
3. Obsługa plików cookie
Supertest potrafi również obsługiwać pliki cookie. Możesz ustawić pliki cookie za pomocą metody .set('Cookie', ...)
lub użyć właściwości .cookies
, aby uzyskać dostęp i modyfikować pliki cookie.
4. Testowanie przesyłania plików
Supertest może być używany do testowania punktów końcowych API, które obsługują przesyłanie plików. Możesz użyć metody .attach()
, aby dołączyć pliki do żądania.
5. Używanie bibliotek asercji (Chai)
Chociaż wbudowana biblioteka asercji Jesta jest wystarczająca w wielu przypadkach, z Supertest można również używać potężniejszych bibliotek asercji, takich jak Chai. Chai zapewnia bardziej wyrazistą i elastyczną składnię asercji. Aby użyć Chai, należy go zainstalować:
npm install --save-dev chai
Następnie możesz zaimportować Chai do swojego pliku testowego i używać jego asercji:
const request = require('supertest');
const app = require('./app');
const chai = require('chai');
const expect = chai.expect;
describe('GET /hello', () => {
it('responds with 200 OK and returns "Hello, World!"', async () => {
const response = await request(app).get('/hello');
expect(response.statusCode).to.equal(200);
expect(response.text).to.equal('Hello, World!');
});
});
Uwaga: Może być konieczne skonfigurowanie Jesta do poprawnej współpracy z Chai. Często wiąże się to z dodaniem pliku konfiguracyjnego, który importuje Chai i konfiguruje go do pracy z globalnym expect
Jesta.
6. Ponowne wykorzystywanie agentów
W przypadku testów wymagających skonfigurowania określonego środowiska (np. uwierzytelniania) często korzystne jest ponowne wykorzystanie agenta Supertest. Pozwala to uniknąć zbędnego kodu konfiguracyjnego w każdym przypadku testowym.
describe('Authenticated API Tests', () => {
let agent;
beforeAll(() => {
agent = request.agent(app); // Utwórz trwałego agenta
// Symulacja uwierzytelniania
return agent
.post('/login')
.send({ username: 'testuser', password: 'password123' });
});
it('can access a protected resource', async () => {
const response = await agent.get('/protected').expect(200);
expect(response.text).toBe('Protected Resource');
});
it('can perform other actions that require authentication', async () => {
// Wykonaj inne uwierzytelnione akcje tutaj
});
});
W tym przykładzie tworzymy agenta Supertest w haku beforeAll
i go uwierzytelniamy. Kolejne testy w bloku describe
mogą następnie ponownie wykorzystać tego uwierzytelnionego agenta, bez konieczności ponownego uwierzytelniania dla każdego testu.
Najlepsze praktyki w testowaniu integracyjnym API z Supertest
Aby zapewnić skuteczne testowanie integracyjne API, rozważ następujące najlepsze praktyki:
- Testuj kompleksowe przepływy pracy (end-to-end): Skoncentruj się na testowaniu kompletnych przepływów użytkownika, a nie na izolowanych punktach końcowych API. Pomaga to zidentyfikować problemy integracyjne, które mogą nie być widoczne podczas testowania poszczególnych API w izolacji.
- Używaj realistycznych danych: Używaj realistycznych danych w swoich testach, aby symulować rzeczywiste scenariusze. Obejmuje to używanie prawidłowych formatów danych, wartości granicznych i potencjalnie nieprawidłowych danych do testowania obsługi błędów.
- Izoluj swoje testy: Upewnij się, że Twoje testy są od siebie niezależne i nie polegają na współdzielonym stanie. Sprawi to, że Twoje testy będą bardziej niezawodne i łatwiejsze do debugowania. Rozważ użycie dedykowanej bazy danych testowej lub mockowanie zewnętrznych zależności.
- Mockuj zewnętrzne zależności: Użyj mockowania, aby odizolować swoje API od zewnętrznych zależności, takich jak bazy danych, API stron trzecich czy inne usługi. Sprawi to, że Twoje testy będą szybsze i bardziej niezawodne, a także pozwoli Ci testować różne scenariusze bez polegania na dostępności usług zewnętrznych. Biblioteki takie jak
nock
są przydatne do mockowania żądań HTTP. - Pisz kompleksowe testy: Dąż do kompleksowego pokrycia testami, włączając testy pozytywne (weryfikujące pomyślne odpowiedzi), testy negatywne (weryfikujące obsługę błędów) i testy graniczne (weryfikujące przypadki brzegowe).
- Automatyzuj swoje testy: Zintegruj swoje testy integracyjne API z potokiem ciągłej integracji (CI), aby upewnić się, że są one uruchamiane automatycznie przy każdej zmianie w kodzie. Pomoże to wcześnie identyfikować problemy integracyjne i zapobiegać ich dotarciu na produkcję.
- Dokumentuj swoje testy: Dokumentuj swoje testy integracyjne API w sposób jasny i zwięzły. Ułatwi to innym programistom zrozumienie celu testów i ich utrzymanie w przyszłości.
- Używaj zmiennych środowiskowych: Przechowuj poufne informacje, takie jak klucze API, hasła do baz danych i inne wartości konfiguracyjne, w zmiennych środowiskowych, zamiast umieszczać je na stałe w kodzie testów. Sprawi to, że Twoje testy będą bezpieczniejsze i łatwiejsze do skonfigurowania dla różnych środowisk.
- Rozważ kontrakty API: Wykorzystaj testowanie kontraktowe API, aby zweryfikować, czy Twoje API przestrzega zdefiniowanego kontraktu (np. OpenAPI/Swagger). Pomaga to zapewnić kompatybilność między różnymi usługami i zapobiega niszczącym zmianom. Do testowania kontraktowego można używać narzędzi takich jak Pact.
Częste błędy, których należy unikać
- Brak izolacji testów: Testy powinny być niezależne. Unikaj polegania na wyniku innych testów.
- Testowanie szczegółów implementacji: Skup się na zachowaniu i kontrakcie API, a nie na jego wewnętrznej implementacji.
- Ignorowanie obsługi błędów: Dokładnie przetestuj, jak Twoje API radzi sobie z nieprawidłowymi danymi wejściowymi, przypadkami brzegowymi i nieoczekiwanymi błędami.
- Pominięcie testowania uwierzytelniania i autoryzacji: Upewnij się, że mechanizmy bezpieczeństwa Twojego API są odpowiednio przetestowane, aby zapobiec nieautoryzowanemu dostępowi.
Podsumowanie
Testowanie integracyjne API jest istotną częścią procesu tworzenia oprogramowania. Używając Supertest, możesz łatwo pisać kompleksowe i niezawodne testy integracyjne API, które pomagają zapewnić jakość i stabilność Twojej aplikacji. Pamiętaj, aby skupić się na testowaniu kompleksowych przepływów pracy, używaniu realistycznych danych, izolowaniu testów i automatyzacji procesu testowania. Postępując zgodnie z tymi najlepszymi praktykami, możesz znacznie zmniejszyć ryzyko problemów integracyjnych i dostarczyć bardziej solidny i niezawodny produkt.
Ponieważ API nadal napędzają nowoczesne aplikacje i architektury mikroserwisów, znaczenie solidnego testowania API, a zwłaszcza testów integracyjnych, będzie tylko rosło. Supertest dostarcza potężny i dostępny zestaw narzędzi dla programistów na całym świecie, aby zapewnić niezawodność i jakość interakcji ich API.