En komplet guide til end-to-end API-test med Docker

Test er generelt en smerte. Nogle ser ikke meningen. Nogle ser det, men tænker på det som et ekstra trin, der sænker dem. Nogle gange er test der, men meget lange at køre eller ustabile. I denne artikel kan du se, hvordan du kan konstruere tests til dig selv med Docker.

Vi ønsker hurtige, meningsfulde og pålidelige tests skrevet og vedligeholdt med minimal indsats. Det betyder tests, der er nyttige for dig som udvikler dagligt. De skal øge din produktivitet og forbedre kvaliteten af ​​din software. At have tests, fordi alle siger "du skulle have test", er ikke godt, hvis det bremser dig.

Lad os se, hvordan man opnår dette med ikke så stor indsats.

Eksemplet vi skal teste

I denne artikel skal vi teste en API bygget med Node / express og bruge chai / mocha til test. Jeg har valgt en JS'y-stak, fordi koden er super kort og let at læse. De anvendte principper gælder for enhver tech stack. Fortsæt med at læse, selvom Javascript gør dig syg.

Eksemplet dækker et simpelt sæt CRUD-slutpunkter til brugere. Det er mere end nok at forstå konceptet og anvende den mere komplekse forretningslogik i din API.

Vi skal bruge et ret standardmiljø til API:

  • En Postgres-database
  • En Redis-klynge
  • Vores API bruger andre eksterne API'er til at udføre sit job

Din API har muligvis brug for et andet miljø. Principperne i denne artikel forbliver de samme. Du bruger forskellige Docker-basisbilleder til at køre den komponent, du muligvis har brug for.

Hvorfor Docker? Og faktisk Docker Compose

Dette afsnit indeholder mange argumenter til fordel for at bruge Docker til test. Du kan springe det over, hvis du med det samme vil komme til den tekniske del.

De smertefulde alternativer

For at teste din API i et tæt på produktionsmiljø har du to valg. Du kan spotte miljøet på kodeniveau eller køre testene på en rigtig server med databasen osv. Installeret.

At spotte alt på kodeniveau rodet koden og konfigurationen af ​​vores API. Det er ofte ikke særlig repræsentativt for, hvordan API'en opfører sig i produktionen. At køre sagen på en rigtig server er tung infrastruktur. Det er meget opsætning og vedligeholdelse, og det skaleres ikke. Når du har en delt database, kan du kun køre 1 test ad gangen for at sikre, at testkøringer ikke forstyrrer hinanden.

Docker Compose giver os mulighed for at få det bedste ud af begge verdener. Det opretter "containeriserede" versioner af alle de eksterne dele, vi bruger. Det håner, men på ydersiden af ​​vores kode. Vores API mener, at det er i et rigtigt fysisk miljø. Docker compose opretter også et isoleret netværk til alle containere til en given testkørsel. Dette giver dig mulighed for at køre flere af dem parallelt på din lokale computer eller en CI-vært.

Overkill?

Du spekulerer måske på, om det overhovedet ikke er overkilligt at udføre ende til slut-test overhovedet med Docker compose. Hvad med bare at køre enhedstest i stedet?

I de sidste 10 år er store monolitapplikationer blevet opdelt i mindre tjenester (tendens mod de livlige "mikrotjenester"). En given API-komponent er afhængig af flere eksterne dele (infrastruktur eller andre API'er). Efterhånden som tjenester bliver mindre, bliver integration med infrastrukturen en større del af jobbet.

Du skal holde et lille hul mellem din produktion og dine udviklingsmiljøer. Ellers opstår der problemer, når produktionen implementeres. Pr. Definition vises disse problemer i det værst mulige øjeblik. De vil føre til forhastede rettelser, fald i kvalitet og frustration for holdet. Ingen ønsker det.

Du spekulerer måske på, om test til ende til slut med Docker compose kører længere end traditionelle enhedstests. Ikke rigtig. Du ser i eksemplet nedenfor, at vi let kan holde testene under 1 minut og til stor fordel: testene afspejler applikationsadfærd i den virkelige verden. Dette er mere værdifuldt end at vide, om din klasse et sted midt i appen fungerer OK eller ej.

Også, hvis du ikke har nogen tests lige nu, starter du fra slutning til slut giver dig store fordele for lidt indsats. Du ved, at alle stakke af applikationen arbejder sammen om de mest almindelige scenarier. Det er allerede noget! Derfra kan du altid forfine en strategi til enhedstest af kritiske dele af din applikation.

Vores første test

Lad os starte med den nemmeste del: vores API og Postgres-databasen. Og lad os køre en simpel CRUD-test. Når vi har den ramme på plads, kan vi tilføje flere funktioner både til vores komponent og til testen.

Her er vores minimale API med en GET / POST til at oprette og liste brugere:

const express = require('express'); const bodyParser = require('body-parser'); const cors = require('cors'); const config = require('./config'); const db = require('knex')({ client: 'pg', connection: { host : config.db.host, user : config.db.user, password : config.db.password, }, }); const app = express(); app.use(bodyParser.urlencoded({ extended: false })); app.use(bodyParser.json()); app.use(cors()); app.route('/api/users').post(async (req, res, next) => { try { const { email, firstname } = req.body; // ... validate inputs here ... const userData = { email, firstname }; const result = await db('users').returning('id').insert(userData); const id = result[0]; res.status(201).send({ id, ...userData }); } catch (err) { console.log(`Error: Unable to create user: ${err.message}. ${err.stack}`); return next(err); } }); app.route('/api/users').get((req, res, next) => { db('users') .select('id', 'email', 'firstname') .then(users => res.status(200).send(users)) .catch(err => { console.log(`Unable to fetch users: ${err.message}. ${err.stack}`); return next(err); }); }); try { console.log("Starting web server..."); const port = process.env.PORT || 8000; app.listen(port, () => console.log(`Server started on: ${port}`)); } catch(error) { console.error(error.stack); }

Her er vores tests skrevet med chai. Testene opretter en ny bruger og henter den tilbage. Du kan se, at testene ikke på nogen måde er forbundet med koden i vores API. Den SERVER_URLvariabel angiver slutpunktet for testen. Det kan være et lokalt eller et fjernt miljø.

const chai = require("chai"); const chaiHttp = require("chai-http"); const should = chai.should(); const SERVER_URL = process.env.APP_URL || "//localhost:8000"; chai.use(chaiHttp); const TEST_USER = { email: "[email protected]", firstname: "John" }; let createdUserId; describe("Users", () => { it("should create a new user", done => { chai .request(SERVER_URL) .post("/api/users") .send(TEST_USER) .end((err, res) => { if (err) done(err) res.should.have.status(201); res.should.be.json; res.body.should.be.a("object"); res.body.should.have.property("id"); done(); }); }); it("should get the created user", done => { chai .request(SERVER_URL) .get("/api/users") .end((err, res) => { if (err) done(err) res.should.have.status(200); res.body.should.be.a("array"); const user = res.body.pop(); user.id.should.equal(createdUserId); user.email.should.equal(TEST_USER.email); user.firstname.should.equal(TEST_USER.firstname); done(); }); }); });

Godt. Lad os nu definere et Docker-komponeringsmiljø for at teste vores API. En fil, der hedder docker-compose.yml, beskriver de containere, som Docker skal køre.

version: '3.1' services: db: image: postgres environment: POSTGRES_USER: john POSTGRES_PASSWORD: mysecretpassword expose: - 5432 myapp: build: . image: myapp command: yarn start environment: APP_DB_HOST: db APP_DB_USER: john APP_DB_PASSWORD: mysecretpassword expose: - 8000 depends_on: - db myapp-tests: image: myapp command: dockerize -wait tcp://db:5432 -wait tcp://myapp:8000 -timeout 10s bash -c "node db/init.js && yarn test" environment: APP_URL: //myapp:8000 APP_DB_HOST: db APP_DB_USER: john APP_DB_PASSWORD: mysecretpassword depends_on: - db - myapp

Så hvad har vi her. Der er 3 containere:

  • db spinder en ny forekomst af PostgreSQL op. Vi bruger det offentlige Postgres-billede fra Docker Hub. Vi indstiller databasens brugernavn og adgangskode. Vi beder Docker om at eksponere port 5432, som databasen lytter til, så andre containere kan oprette forbindelse
  • myapp er den container, der kører vores API. Den buildkommando fortæller Docker til rent faktisk at bygge beholderen billede fra vores kilde. Resten er som db-containeren: miljøvariabler og porte
  • myapp-tests er containeren, der udfører vores tests. Det bruger det samme billede som myapp, fordi koden allerede vil være der, så der er ingen grund til at bygge den igen. Kommandokørslen node db/init.js && yarn testpå containeren initialiserer databasen (opretter tabeller osv.) Og kører testene. Vi bruger dockerize til at vente på, at alle de nødvendige servere er i gang. De depends_onmuligheder, vil sikre, at beholderne starte i en bestemt rækkefølge. Det sikrer ikke, at databasen inde i db-containeren faktisk er klar til at acceptere forbindelser. Heller ikke at vores API-server allerede er oppe.

Definitionen af ​​miljøet er som 20 linjer med meget let at forstå kode. Den eneste hjerne del er miljødefinitionen. Brugernavne, adgangskoder og URL'er skal være ensartede, så containere rent faktisk kan arbejde sammen.

En ting at bemærke er, at Docker compose indstiller værten for de containere, den opretter, til navnet på containeren. Så databasen vil ikke være tilgængelig under localhost:5432men db:5432. På samme måde som vores API vil blive serveret under myapp:8000. Der er ingen lokal vært af nogen art her.

Dette betyder, at din API skal understøtte miljøvariabler, når det kommer til miljødefinition. Ingen hardkodede ting. Men det har intet at gøre med Docker eller denne artikel. En konfigurerbar applikation er punkt 3 i 12-faktor-appmanifestet, så du skal allerede gøre det.

Den sidste ting, vi skal fortælle Docker, er, hvordan man faktisk bygger containeren myapp . Vi bruger en Dockerfil som nedenfor. Indholdet er specifikt for din tech stack, men ideen er at samle din API i en kørbar server.

Eksemplet nedenfor for vores Node API installerer Dockerize, installerer API afhængigheder og kopierer koden for API inde i containeren (serveren er skrevet i rå JS, så det er ikke nødvendigt at kompilere det).

FROM node AS base # Dockerize is needed to sync containers startup ENV DOCKERIZE_VERSION v0.6.0 RUN wget //github.com/jwilder/dockerize/releases/download/$DOCKERIZE_VERSION/dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ && tar -C /usr/local/bin -xzvf dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz \ && rm dockerize-alpine-linux-amd64-$DOCKERIZE_VERSION.tar.gz RUN mkdir -p ~/app WORKDIR ~/app COPY package.json . COPY yarn.lock . FROM base AS dependencies RUN yarn FROM dependencies AS runtime COPY . .

Typisk fra linjen WORKDIR ~/appog under kører du kommandoer, der bygger din applikation.

Og her er kommandoen, vi bruger til at køre testene:

docker-compose up --build --abort-on-container-exit

Denne kommando vil fortælle Docker compose at spinde de komponenter, der er defineret i vores docker-compose.ymlfil, op. Den --buildflag vil udløse build af mitpgm beholderen ved at udføre indholdet af Dockerfileovenstående. Den --abort-on-container-exitfortæller Docker compose at lukke miljøet, så snart en container kommer ud.

Det fungerer godt, da den eneste komponent, der er beregnet til at afslutte, er testcontaineren myapp-tests, efter at testene er udført. Kirsebær på kagen, docker-composekommandoen afslutter med samme udgangskode som beholderen, der udløste udgangen. Det betyder, at vi kan kontrollere, om testene lykkedes eller ikke fra kommandolinjen. Dette er meget nyttigt til automatiserede builds i et CI-miljø.

Er det ikke den perfekte testopsætning?

Det fulde eksempel er her på GitHub. Du kan klone lageret og køre kommandoen docker compose:

docker-compose up --build --abort-on-container-exit

Selvfølgelig har du brug for Docker installeret. Docker har den besværlige tendens til at tvinge dig til at tilmelde dig en konto bare for at downloade sagen. Men det behøver du faktisk ikke. Gå til udgivelsesnoterne (link til Windows og link til Mac) og download ikke den nyeste version, men den lige før. Dette er et link til direkte download.

Den allerførste kørsel af testene vil være længere end normalt. Dette skyldes, at Docker bliver nødt til at downloade basisbillederne til dine containere og cache et par ting. De næste kørsler vil være meget hurtigere.

Logfiler fra kørslen ser ud som nedenfor. Du kan se, at Docker er cool nok til at placere logfiler fra alle komponenterne på den samme tidslinje. Dette er meget praktisk, når du leder efter fejl.

Creating tuto-api-e2e-testing_db_1 ... done Creating tuto-api-e2e-testing_redis_1 ... done Creating tuto-api-e2e-testing_myapp_1 ... done Creating tuto-api-e2e-testing_myapp-tests_1 ... done Attaching to tuto-api-e2e-testing_redis_1, tuto-api-e2e-testing_db_1, tuto-api-e2e-testing_myapp_1, tuto-api-e2e-testing_myapp-tests_1 db_1 | The files belonging to this database system will be owned by user "postgres". redis_1 | 1:M 09 Nov 2019 21:57:22.161 * Running mode=standalone, port=6379. myapp_1 | yarn run v1.19.0 redis_1 | 1:M 09 Nov 2019 21:57:22.162 # WARNING: The TCP backlog setting of 511 cannot be enforced because /proc/sys/net/core/somaxconn is set to the lower value of 128. redis_1 | 1:M 09 Nov 2019 21:57:22.162 # Server initialized db_1 | This user must also own the server process. db_1 | db_1 | The database cluster will be initialized with locale "en_US.utf8". db_1 | The default database encoding has accordingly been set to "UTF8". db_1 | The default text search configuration will be set to "english". db_1 | db_1 | Data page checksums are disabled. db_1 | db_1 | fixing permissions on existing directory /var/lib/postgresql/data ... ok db_1 | creating subdirectories ... ok db_1 | selecting dynamic shared memory implementation ... posix myapp-tests_1 | 2019/11/09 21:57:25 Waiting for: tcp://db:5432 myapp-tests_1 | 2019/11/09 21:57:25 Waiting for: tcp://redis:6379 myapp-tests_1 | 2019/11/09 21:57:25 Waiting for: tcp://myapp:8000 myapp_1 | $ node server.js redis_1 | 1:M 09 Nov 2019 21:57:22.163 # WARNING you have Transparent Huge Pages (THP) support enabled in your kernel. This will create latency and memory usage issues with Redis. To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled. db_1 | selecting default max_connections ... 100 myapp_1 | Starting web server... myapp-tests_1 | 2019/11/09 21:57:25 Connected to tcp://myapp:8000 myapp-tests_1 | 2019/11/09 21:57:25 Connected to tcp://db:5432 redis_1 | 1:M 09 Nov 2019 21:57:22.164 * Ready to accept connections myapp-tests_1 | 2019/11/09 21:57:25 Connected to tcp://redis:6379 myapp_1 | Server started on: 8000 db_1 | selecting default shared_buffers ... 128MB db_1 | selecting default time zone ... Etc/UTC db_1 | creating configuration files ... ok db_1 | running bootstrap script ... ok db_1 | performing post-bootstrap initialization ... ok db_1 | syncing data to disk ... ok db_1 | db_1 | db_1 | Success. You can now start the database server using: db_1 | db_1 | pg_ctl -D /var/lib/postgresql/data -l logfile start db_1 | db_1 | initdb: warning: enabling "trust" authentication for local connections db_1 | You can change this by editing pg_hba.conf or using the option -A, or db_1 | --auth-local and --auth-host, the next time you run initdb. db_1 | waiting for server to start....2019-11-09 21:57:24.328 UTC [41] LOG: starting PostgreSQL 12.0 (Debian 12.0-2.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit db_1 | 2019-11-09 21:57:24.346 UTC [41] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" db_1 | 2019-11-09 21:57:24.373 UTC [42] LOG: database system was shut down at 2019-11-09 21:57:23 UTC db_1 | 2019-11-09 21:57:24.383 UTC [41] LOG: database system is ready to accept connections db_1 | done db_1 | server started db_1 | CREATE DATABASE db_1 | db_1 | db_1 | /usr/local/bin/docker-entrypoint.sh: ignoring /docker-entrypoint-initdb.d/* db_1 | db_1 | waiting for server to shut down....2019-11-09 21:57:24.907 UTC [41] LOG: received fast shutdown request db_1 | 2019-11-09 21:57:24.909 UTC [41] LOG: aborting any active transactions db_1 | 2019-11-09 21:57:24.914 UTC [41] LOG: background worker "logical replication launcher" (PID 48) exited with exit code 1 db_1 | 2019-11-09 21:57:24.914 UTC [43] LOG: shutting down db_1 | 2019-11-09 21:57:24.930 UTC [41] LOG: database system is shut down db_1 | done db_1 | server stopped db_1 | db_1 | PostgreSQL init process complete; ready for start up. db_1 | db_1 | 2019-11-09 21:57:25.038 UTC [1] LOG: starting PostgreSQL 12.0 (Debian 12.0-2.pgdg100+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 8.3.0-6) 8.3.0, 64-bit db_1 | 2019-11-09 21:57:25.039 UTC [1] LOG: listening on IPv4 address "0.0.0.0", port 5432 db_1 | 2019-11-09 21:57:25.039 UTC [1] LOG: listening on IPv6 address "::", port 5432 db_1 | 2019-11-09 21:57:25.052 UTC [1] LOG: listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432" db_1 | 2019-11-09 21:57:25.071 UTC [59] LOG: database system was shut down at 2019-11-09 21:57:24 UTC db_1 | 2019-11-09 21:57:25.077 UTC [1] LOG: database system is ready to accept connections myapp-tests_1 | Creating tables ... myapp-tests_1 | Creating table 'users' myapp-tests_1 | Tables created succesfully myapp-tests_1 | yarn run v1.19.0 myapp-tests_1 | $ mocha --timeout 10000 --bail myapp-tests_1 | myapp-tests_1 | myapp-tests_1 | Users myapp-tests_1 | Mock server started on port: 8002 myapp-tests_1 | ✓ should create a new user (151ms) myapp-tests_1 | ✓ should get the created user myapp-tests_1 | ✓ should not create user if mail is spammy myapp-tests_1 | ✓ should not create user if spammy mail API is down myapp-tests_1 | myapp-tests_1 | myapp-tests_1 | 4 passing (234ms) myapp-tests_1 | myapp-tests_1 | Done in 0.88s. myapp-tests_1 | 2019/11/09 21:57:26 Command finished successfully. tuto-api-e2e-testing_myapp-tests_1 exited with code 0

Vi kan se, at db er den container, der initialiseres længst. Giver mening. Når det er gjort, starter testene. Den samlede driftstid på min bærbare computer er 16 sekunder. Sammenlignet med de 880ms, der bruges til faktisk at udføre testene, er det meget. I praksis er test, der løber under 1 minut, guld, da det næsten er øjeblikkelig feedback. De 15 sekunders overhead er et buy-in tid, der vil være konstant, når du tilføjer flere tests. Du kan tilføje hundreder af tests og stadig holde eksekveringstiden under 1 minut.

Voilà! Vi har vores testrammer i gang. I et rigtigt verdensprojekt ville de næste trin være at forbedre funktionel dækning af din API med flere tests. Lad os overveje, at CRUD-operationer er dækket. Det er tid til at tilføje flere elementer til vores testmiljø.

Tilføjelse af en Redis-klynge

Lad os tilføje et andet element til vores API-miljø for at forstå, hvad det kræver. Spoiler alarm: det er ikke meget.

Lad os forestille os, at vores API holder brugersessioner i en Redis-klynge. Hvis du spekulerer på, hvorfor vi ville gøre det, så forestil dig 100 forekomster af din API i produktion. Brugere rammer den ene eller den anden server baseret på round robin load balancing. Hver anmodning skal godkendes.

Dette kræver brugerprofildata for at kontrollere privilegier og anden applikationsspecifik forretningslogik. En vej at gå er at lave en rundtur til databasen for at hente dataene hver gang du har brug for det, men det er ikke særlig effektivt. Brug af en databasehukommelse i hukommelsen gør dataene tilgængelige på tværs af alle servere til prisen for en lokal variabel læsning.

Sådan forbedrer du dit Docker-komponenttestmiljø med en ekstra service. Lad os tilføje en Redis-klynge fra det officielle Docker-billede (jeg har kun bevaret de nye dele af filen):

services: db: ... redis: image: "redis:alpine" expose: - 6379 myapp: environment: APP_REDIS_HOST: redis APP_REDIS_PORT: 6379 ... myapp-tests: command: dockerize ... -wait tcp://redis:6379 ... environment: APP_REDIS_HOST: redis APP_REDIS_PORT: 6379 ... ...

Du kan se, at det ikke er meget. Vi tilføjede en ny container kaldet redis . Det bruger det officielle minimale redis-billede kaldet redis:alpine. Vi tilføjede Redis-værts- og portkonfiguration til vores API-container. Og vi har fået test til at vente på det såvel som de andre containere, før testene udføres.

Lad os ændre vores applikation til faktisk at bruge Redis-klyngen:

const redis = require('redis').createClient({ host: config.redis.host, port: config.redis.port, }) ... app.route('/api/users').post(async (req, res, next) => { try { const { email, firstname } = req.body; // ... validate inputs here ... const userData = { email, firstname }; const result = await db('users').returning('id').insert(userData); const id = result[0]; // Once the user is created store the data in the Redis cluster await redis.set(id, JSON.stringify(userData)); res.status(201).send({ id, ...userData }); } catch (err) { console.log(`Error: Unable to create user: ${err.message}. ${err.stack}`); return next(err); } });

Lad os nu ændre vores tests for at kontrollere, at Redis-klyngen er befolket med de rigtige data. Derfor får myapp-testcontaineren også Redis-værten og portkonfigurationen ind docker-compose.yml.

it("should create a new user", done => { chai .request(SERVER_URL) .post("/api/users") .send(TEST_USER) .end((err, res) => { if (err) throw err; res.should.have.status(201); res.should.be.json; res.body.should.be.a("object"); res.body.should.have.property("id"); res.body.should.have.property("email"); res.body.should.have.property("firstname"); res.body.id.should.not.be.null; res.body.email.should.equal(TEST_USER.email); res.body.firstname.should.equal(TEST_USER.firstname); createdUserId = res.body.id; redis.get(createdUserId, (err, cacheData) => { if (err) throw err; cacheData = JSON.parse(cacheData); cacheData.should.have.property("email"); cacheData.should.have.property("firstname"); cacheData.email.should.equal(TEST_USER.email); cacheData.firstname.should.equal(TEST_USER.firstname); done(); }); }); });

Se hvor let dette var. Du kan opbygge et komplekst miljø til dine tests, ligesom du samler Lego-mursten.

Vi kan se en anden fordel ved denne type containertestet miljøtest. Testene kan faktisk se på miljøets komponenter. Vores tests kan ikke kun kontrollere, at vores API returnerer de korrekte svarkoder og data. Vi kan også kontrollere, at data i Redis-klyngen har de korrekte værdier. Vi kunne også kontrollere databaseindholdet.

Tilføjelse af API-mocks

Et fælles element for API-komponenter er at kalde andre API-komponenter.

Lad os sige, at vores API skal kontrollere spam-bruger-e-mails, når du opretter en bruger. Kontrollen udføres ved hjælp af en tredjepartstjeneste:

const validateUserEmail = async (email) => { const res = await fetch(`${config.app.externalUrl}/validate?email=${email}`); if(res.status !== 200) return false; const json = await res.json(); return json.result === 'valid'; } app.route('/api/users').post(async (req, res, next) => { try { const { email, firstname } = req.body; // ... validate inputs here ... const userData = { email, firstname }; // We don't just create any user. Spammy emails should be rejected const isValidUser = await validateUserEmail(email); if(!isValidUser) { return res.sendStatus(403); } const result = await db('users').returning('id').insert(userData); const id = result[0]; await redis.set(id, JSON.stringify(userData)); res.status(201).send({ id, ...userData }); } catch (err) { console.log(`Error: Unable to create user: ${err.message}. ${err.stack}`); return next(err); } });

Nu har vi et problem med at teste noget. Vi kan ikke oprette nogen brugere, hvis API'en til at opdage spammy-e-mails ikke er tilgængelig. Ændring af vores API for at omgå dette trin i testtilstand er en farlig rod i koden.

Selvom vi kunne bruge den rigtige tredjepartstjeneste, vil vi ikke gøre det. Som hovedregel bør vores tests ikke afhænge af ekstern infrastruktur. Først og fremmest, fordi du sandsynligvis vil køre dine tests meget som en del af din CI-proces. Det er ikke så sejt at forbruge en anden produktions-API til dette formål. For det andet er API'en muligvis midlertidigt nede og fejler dine tests af de forkerte grunde.

Den rigtige løsning er at spotte de eksterne API'er i vores tests.

Intet behov for nogen fancy rammer. Vi bygger en generisk mock i vanille JS i ~ 20 linjer kode. Dette giver os mulighed for at kontrollere, hvad API'en returnerer til vores komponent. Det giver mulighed for at teste fejlscenarier.

Lad os nu forbedre vores tests.

const express = require("express"); ... const MOCK_SERVER_PORT = process.env.MOCK_SERVER_PORT || 8002; // Some object to encapsulate attributes of our mock server // The mock stores all requests it receives in the `requests` property. const mock = { app: express(), server: null, requests: [], status: 404, responseBody: {} }; // Define which response code and content the mock will be sending const setupMock = (status, body) => { mock.status = status; mock.responseBody = body; }; // Start the mock server const initMock = async () => { mock.app.use(bodyParser.urlencoded({ extended: false })); mock.app.use(bodyParser.json()); mock.app.use(cors()); mock.app.get("*", (req, res) => { mock.requests.push(req); res.status(mock.status).send(mock.responseBody); }); mock.server = await mock.app.listen(MOCK_SERVER_PORT); console.log(`Mock server started on port: ${MOCK_SERVER_PORT}`); }; // Destroy the mock server const teardownMock = () => { if (mock.server) { mock.server.close(); delete mock.server; } }; describe("Users", () => { // Our mock is started before any test starts ... before(async () => await initMock()); // ... killed after all the tests are executed ... after(() => { redis.quit(); teardownMock(); }); // ... and we reset the recorded requests between each test beforeEach(() => (mock.requests = [])); it("should create a new user", done => { // The mock will tell us the email is valid in this test setupMock(200, { result: "valid" }); chai .request(SERVER_URL) .post("/api/users") .send(TEST_USER) .end((err, res) => { // ... check response and redis as before createdUserId = res.body.id; // Verify that the API called the mocked service with the right parameters mock.requests.length.should.equal(1); mock.requests[0].path.should.equal("/api/validate"); mock.requests[0].query.should.have.property("email"); mock.requests[0].query.email.should.equal(TEST_USER.email); done(); }); }); });

Testene kontrollerer nu, at den eksterne API er blevet ramt med de korrekte data under opkaldet til vores API.

Vi kan også tilføje andre tests, der kontrollerer, hvordan vores API opfører sig baseret på de eksterne API-svarkoder:

describe("Users", () => { it("should not create user if mail is spammy", done => { // The mock will tell us the email is NOT valid in this test ... setupMock(200, { result: "invalid" }); chai .request(SERVER_URL) .post("/api/users") .send(TEST_USER) .end((err, res) => { // ... so the API should fail to create the user // We could test that the DB and Redis are empty here res.should.have.status(403); done(); }); }); it("should not create user if spammy mail API is down", done => { // The mock will tell us the email checking service // is down for this test ... setupMock(500, {}); chai .request(SERVER_URL) .post("/api/users") .send(TEST_USER) .end((err, res) => { // ... in that case also a user should not be created res.should.have.status(403); done(); }); }); });

Hvordan du håndterer fejl fra tredjeparts API'er i din applikation er selvfølgelig op til dig. Men du forstår pointen.

For at køre disse tests er vi nødt til at fortælle containeren myapp, hvad der er basis-URL til tredjeparts service:

 myapp: environment: APP_EXTERNAL_URL: //myapp-tests:8002/api ... myapp-tests: environment: MOCK_SERVER_PORT: 8002 ...

Konklusion og et par andre tanker

Forhåbentlig gav denne artikel dig en smag af, hvad Docker komponere kan gøre for dig, når det kommer til API-test. Det fulde eksempel er her på GitHub.

Brug af Docker compose gør, at test kører hurtigt i et miljø tæt på produktionen. Det kræver ingen tilpasninger til din komponentkode. Det eneste krav er at understøtte konfiguration af miljøvariabler.

Komponentlogikken i dette eksempel er meget enkel, men principperne gælder for enhver API. Dine tests bliver bare længere eller mere komplekse. De gælder også for enhver tech stack, der kan placeres i en container (det er dem alle). Og når du først er der, er du et skridt væk fra at implementere dine containere til produktion, hvis det er nødvendigt.

Hvis du ikke har nogen tests lige nu, er det sådan, jeg anbefaler, at du skal starte: test til ende til slut med Docker compose. Det er så simpelt, at du kunne have din første test kørt om et par timer. Du er velkommen til at kontakte mig, hvis du har spørgsmål eller har brug for rådgivning. Jeg hjælper gerne.

Jeg håber, du har haft glæde af denne artikel og vil begynde at teste dine API'er med Docker Compose. Når du har testene klar, kan du køre dem ud af kassen på vores kontinuerlige integrationsplatform Fire CI.

En sidste idé til at få succes med automatiseret test.

Når det kommer til vedligeholdelse af store testpakker, er den vigtigste funktion, at test er lette at læse og forstå. Dette er nøglen til at motivere dit team til at holde testene opdaterede. Komplekse testrammer vil sandsynligvis ikke blive brugt korrekt i det lange løb.

Uanset stakken til din API kan du overveje at bruge chai / mocha til at skrive tests til den. Det kan virke usædvanligt at have forskellige stakke til runtime-kode og testkode, men hvis det bliver gjort jobbet ... Som du kan se fra eksemplerne i denne artikel, er det så simpelt som det bliver at teste en REST API med chai / mocha . Læringskurven er tæt på nul.

Så hvis du overhovedet ikke har nogen tests og har en REST API til test skrevet i Java, Python, RoR, .NET eller en hvilken som helst anden stak, kan du overveje at prøve chai / mocha.

Hvis du spekulerer på, hvordan man overhovedet kommer i gang med kontinuerlig integration, har jeg skrevet en bredere guide om det. Her er det: Sådan kommer du i gang med kontinuerlig integration

Oprindeligt offentliggjort på Fire CI Blog.