Popoln vodnik za celovito testiranje API-jev z Dockerjem

Testiranje je na splošno bolečina. Nekateri ne vidijo smisla. Nekateri to vidijo, toda na to gledajo kot na dodaten korak, ki jih upočasni. Včasih so testi na voljo, vendar zelo dolgi ali nestabilni. V tem članku boste videli, kako lahko sami oblikujete teste z Dockerjem.

Želimo hitre, smiselne in zanesljive teste, napisane in vzdrževane z minimalnim naporom. Pomeni teste, ki so vam vsak dan koristni kot razvijalcu. Povečati bi morali vašo produktivnost in izboljšati kakovost vaše programske opreme. Imeti teste, ker vsi pravijo, "da bi morali imeti teste", ni dobro, če vas to upočasni.

Poglejmo, kako to doseči z ne toliko truda.

Primer, ki ga bomo preizkusili

V tem članku bomo preizkusili API, zgrajen z Node / express, in za testiranje uporabili chai / mocha. Izbral sem JS'y sklad, ker je koda zelo kratka in enostavna za branje. Uporabljena načela veljajo za kateri koli tehnološki sklad. Nadaljujte z branjem, tudi če vas Javascript muči.

Primer bo zajemal preprost nabor končnih točk CRUD za uporabnike. Več kot dovolj je, da dojamemo koncept in uporabimo zapletenejšo poslovno logiko vašega API-ja.

Za API bomo uporabili precej standardno okolje:

  • Baza podatkov Postgres
  • Grozd Redis
  • Naš API bo za svoje delo uporabljal druge zunanje API-je

Vaš API morda potrebuje drugačno okolje. Načela, uporabljena v tem članku, bodo ostala enaka. Za zagon poljubne komponente, ki jo potrebujete, boste uporabili različne osnovne slike Dockerja.

Zakaj Docker? In pravzaprav Docker Compose

Ta razdelek vsebuje veliko argumentov za uporabo Dockerja za testiranje. Lahko ga preskočite, če želite takoj priti do tehničnega dela.

Boleče alternative

Za preizkus API-ja v okolju, ki je blizu produkcije, imate na voljo dve možnosti. Lahko se posmehujete okolju na ravni kode ali izvajate teste na resničnem strežniku z nameščeno bazo podatkov itd.

Posmehovanje vsemu na ravni kode nereduje kodo in konfiguracijo našega API-ja. Prav tako pogosto ni zelo reprezentativno, kako se bo API obnašal v proizvodnji. Zagon stvari v resničnem strežniku je težka infrastruktura. Veliko je namestitve in vzdrževanja in ne meri. Če imate bazo podatkov v skupni rabi, lahko naenkrat izvedete samo en test, da zagotovite, da testni teki ne motijo ​​drug drugega.

Docker Compose nam omogoča, da dobimo najboljše iz obeh svetov. Ustvari "vsebniške" različice vseh zunanjih delov, ki jih uporabljamo. Posmehuje se, vendar na zunanji strani naše kode. Naš API meni, da je v resničnem fizičnem okolju. Sestavljanje Dockerja bo ustvarilo tudi izolirano omrežje za vse vsebnike za dani preizkusni zagon. To vam omogoča, da jih več vzporedno zaženete v lokalnem računalniku ali gostitelju CI.

Preveliko?

Morda se sprašujete, ali z Dockerjevim sestavljanjem sploh ni preveč težko izvajati preskusov od konca do konca. Kaj pa namesto tega samo zagnati enotne teste?

V zadnjih 10 letih so bili veliki monolitni programi razdeljeni na manjše storitve (usmerjene k živahnim "mikro storitvam"). Dana komponenta API temelji na več zunanjih delih (infrastruktura ali drugi API-ji). Ko se storitve zmanjšujejo, postane integracija z infrastrukturo večji del dela.

Morali bi ohraniti majhno vrzel med proizvodnjo in razvojnim okoljem. V nasprotnem primeru se bodo pojavile težave pri uvajanju proizvodnje. Po definiciji se te težave pojavijo v najslabšem trenutku. Pripeljali bodo do hitrih popravkov, padca kakovosti in razočaranja ekipe. Tega si nihče ne želi.

Morda se sprašujete, ali se preskusi konca do konca z Dockerjevim sestavljanjem izvajajo dlje kot običajni preskusi enot. Pravzaprav ne. V spodnjem primeru boste videli, da lahko preskuse enostavno obdržimo manj kot 1 minuto, in to z veliko koristjo: testi odražajo vedenje aplikacije v resničnem svetu. To je bolj dragoceno kot vedeti, ali vaš razred nekje na sredini aplikacije deluje v redu ali ne.

Če trenutno nimate nobenega testa, vam od majhnega truda prinesejo velike koristi. Vedeli boste, da vsi skladi aplikacije delujejo skupaj za najpogostejše scenarije. To je že nekaj! Od tam lahko vedno izboljšate strategijo za enotno testiranje kritičnih delov vaše aplikacije.

Naš prvi test

Začnimo z najlažjim delom: našim API-jem in bazo podatkov Postgres. In zaženimo preprost test CRUD. Ko vzpostavimo ta okvir, lahko v komponento in test dodamo več funkcij.

Tukaj je naš minimalni API z GET / POST za ustvarjanje in seznam uporabnikov:

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); }

Tu so naši testi, napisani s chai. Preizkusi ustvarijo novega uporabnika in ga pridobijo nazaj. Vidite lahko, da testi na noben način niso povezani s kodo našega API-ja. SERVER_URLSpremenljivka označuje končno točko testa. Lahko je lokalno ali oddaljeno okolje.

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(); }); }); });

Dobro. Zdaj, da preizkusimo naš API, določimo okolje za sestavljanje Dockerja. Datoteka z imenom docker-compose.ymlbo opisovala vsebnike, ki jih mora Docker zagnati.

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

Torej, kaj imamo tukaj. Na voljo so 3 posode:

  • db zavrti nov primerek PostgreSQL. Uporabljamo javno sliko Postgres iz Docker Hub. Uporabniško ime in geslo zbirke podatkov nastavimo. Dockerju povemo, naj izpostavi vrata 5432, ki jih bo poslušala baza podatkov, da se bodo lahko povezali drugi vsebniki
  • myapp je vsebnik, ki bo zagnal naš API. buildUkaz pove DOCKER dejansko zgraditi podobo posodo iz našega vira. Ostalo je kot db vsebnik: spremenljivke okolja in vrata
  • myapp-tests je vsebnik, ki bo izvajal naše teste. Uporabil bo isto sliko kot myapp, ker bo koda že tam, zato je ni treba znova zgraditi. Ukaz, ki se node db/init.js && yarn testzažene v vsebniku, bo inicializiral bazo podatkov (ustvaril tabele itd.) In zagnal teste. Dockerize uporabljamo, da počakamo, da se vsi zahtevani strežniki zaženejo in zaženejo. V depends_onmožnosti bo zagotovil, da so zabojniki začeti v določenem vrstnem redu. Ne zagotavlja, da je baza podatkov v vsebniku db dejansko pripravljena za sprejem povezav. Niti naš strežnik API že deluje.

Opredelitev okolja je kot 20 vrstic zelo razumljive kode. Edini možgan del je opredelitev okolja. Uporabniška imena, gesla in URL-ji morajo biti dosledni, da lahko vsebniki dejansko delujejo skupaj.

Opaziti je treba, da bo Docker compose nastavil gostitelja vsebnikov, ki jih ustvari, na ime vsebnika. Torej baza podatkov ne bo na voljo v okviru localhost:5432vendar db:5432. Na enak način bo naš API vročen pod myapp:8000. Tu ni nobenega lokalnega gostitelja.

To pomeni, da mora vaš API podpirati spremenljivke okolja, ko gre za opredelitev okolja. Brez trdno kodiranih stvari. Toda to nima nič skupnega z Dockerjem ali tem člankom. Konfigurabilna aplikacija je točka 3 manifesta za 12 faktorjev aplikacije, zato bi jo morali že početi.

Zadnja stvar, ki jo moramo povedati Dockerju, je, kako dejansko zgraditi vsebnik myapp . Uporabljamo Dockerfile kot spodaj. Vsebina je specifična za vaš tehnološki sklad, vendar je ideja združiti vaš API v zagnani strežnik.

Spodnji primer našega API-ja Node namesti Dockerize, namesti odvisnosti API-ja in kopira kodo API-ja znotraj vsebnika (strežnik je zapisan v surovem JS, zato ga ni treba prevajati).

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 . .

Značilno je, da iz vrstice WORKDIR ~/appin spodaj izvajate ukaze, ki gradijo vašo aplikacijo.

In tu je ukaz, ki ga uporabljamo za izvajanje preskusov:

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

Ta ukaz bo Dockerju sestavil, da zavrti komponente, opredeljene v naši docker-compose.ymldatoteki. Oznaka --buildbo sprožila gradnjo vsebnika myapp z izvajanjem vsebine Dockerfilezgoraj. --abort-on-container-exitBo povedal DOCKER sestavite zapreti okolja takoj, ko eden posodo izhode.

To deluje dobro, saj je edina komponenta, ki naj bi izstopila, preskusni vsebnik myapp-tests po izvedbi testov. Češnja na torti, bo docker-composeukaz izstopil z isto izhodno kodo kot posoda, ki je sprožila izhod. To pomeni, da lahko iz ukazne vrstice preverimo, ali so testi uspeli ali ne. To je zelo koristno za avtomatizirane gradnje v okolju CI.

Ali ni to popolna testna nastavitev?

Celoten primer je tukaj na GitHubu. Lahko klonirate repozitorij in zaženete ukaz za sestavljanje dockerja:

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

Seveda potrebujete Docker nameščen. Docker ima težavo, da vas prisili, da se prijavite za račun samo zato, da stvar prenesete. Ampak dejansko vam ni treba. Pojdite na opombe ob izdaji (povezava za Windows in povezava za Mac) in prenesite ne najnovejšo različico, temveč tisto, ki je bila prej. To je neposredna povezava za prenos.

Že prva izvedba testov bo daljša kot običajno. To pa zato, ker bo moral Docker prenesti osnovne slike za vaše vsebnike in predpomniti nekaj stvari. Naslednji teki bodo veliko hitrejši.

Dnevniki s teka bodo videti spodaj. Vidite, da je Docker dovolj kul, da na isti časovni trak shrani dnevnike iz vseh komponent. To je zelo priročno pri iskanju napak.

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

Vidimo lahko, da je db vsebnik, ki se inicializira najdlje. Smiselno. Ko končate, se testi začnejo. Skupno delovanje prenosnega računalnika je 16 sekund. V primerjavi z 880 ms, ki se dejansko izvajajo, je veliko. V praksi so testi, ki trajajo manj kot 1 minuto, zlati, saj so skoraj takojšnja povratna informacija. 15-sekundni režijski stroški so časovni odkup, ki bo ob dodajanju dodatnih testov stalen. Lahko dodate stotine testov in še vedno ohranite čas izvajanja manj kot 1 minuto.

Voilà! Naš testni okvir je pripravljen in deluje. V resničnem projektu bi bili naslednji koraki izboljšanje funkcionalne pokritosti vašega API-ja z več testi. Razmislimo o zajetih operacijah CRUD. Čas je, da v testno okolje dodamo več elementov.

Dodajanje gruče Redis

Dodajte svojemu okolju API še en element, da bomo razumeli, kaj je potrebno. Opozorilo spojlerja: ni veliko.

Predstavljajmo si, da naš API hrani uporabniške seje v gruči Redis. Če se sprašujete, zakaj bi to storili, si predstavljajte 100 primerov vašega API-ja v izdelavi. Uporabniki zadenejo enega ali drugega strežnika na podlagi uravnoteženja obremenitve. Vsaka zahteva mora biti overjena.

To zahteva, da podatki o uporabniškem profilu preverijo privilegije in drugo poslovno logiko, specifično za aplikacijo. Eden od načinov je, da obiščete bazo podatkov, kadar koli jo potrebujete, vendar to ni zelo učinkovito. Z uporabo gruče zbirke podatkov v pomnilniku so podatki na voljo na vseh strežnikih za stroške branja lokalne spremenljivke.

Tako izboljšate testno okolje za sestavljanje Dockerja z dodatno storitvijo. Dodamo gručo Redis iz uradne slike Dockerja (obdržal sem samo nove dele datoteke):

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 ... ...

Saj vidite, da ni veliko. Dodali smo novo posodo z imenom redis . Uporablja uradno minimalno sliko redis, imenovano redis:alpine. V naš vsebnik API smo dodali konfiguracijo gostitelja in vrat Redis. In pred izvedbo testov smo poskrbeli, da so nanj počakali testi in drugi zabojniki.

Spremenimo svojo aplikacijo tako, da bo dejansko uporabljala gručo Redis:

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); } });

Spremenimo zdaj naše teste, da preverimo, ali je skupina Redis zapolnjena s pravimi podatki. Zato vsebnik myapp-tests dobi tudi konfiguracijo gostitelja in vrat Redis 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(); }); }); });

Poglejte, kako enostavno je bilo to. Za svoje teste lahko sestavite zapleteno okolje, kot če sestavljate kockice Lego.

Opazimo lahko še eno prednost tovrstnega preizkušanja celotnega okolja v zabojnikih. Preizkusi lahko dejansko preučijo sestavne dele okolja. Naši testi ne morejo samo preveriti, ali naš API vrne ustrezne odzivne kode in podatke. Prav tako lahko preverimo, ali imajo podatki v gruči Redis ustrezne vrednosti. Vsebino baze podatkov bi lahko tudi preverili.

Dodajanje lažnih API-jev

Pogost element komponent API je priklic drugih komponent API.

Recimo, da mora naš API pri ustvarjanju uporabnika preveriti neželeno e-pošto uporabnikov. Preverjanje se opravi s pomočjo storitve tretje osebe:

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); } });

Zdaj imamo težavo pri preizkušanju česar koli. Uporabnikov ne moremo ustvariti, če API za zaznavanje neželene e-pošte ni na voljo. Spreminjanje našega API-ja, da v preskusnem načinu zaobide ta korak, je nevarno nered v kodi.

Tudi če bi lahko uporabili resnično storitev tretjih oseb, tega ne želimo storiti. Praviloma naši testi ne smejo biti odvisni od zunanje infrastrukture. Najprej zato, ker boste svoje preizkuse verjetno veliko izvajali kot del postopka vmesnika. Ni tako kul, če bi v ta namen zaužili drug produkcijski API. Drugič, API morda začasno ne deluje, zato testi niso uspeli iz napačnih razlogov.

Prava rešitev je posmeh zunanjim API-jem v naših testih.

Ni potrebe po kakršnem koli modnem okviru. V ~ 20 vrstic kode bomo zgradili generično posmeh iz vanilije JS. To nam bo omogočilo nadzor nad tem, kaj bo API vrnil naši komponenti. Omogoča preizkušanje scenarijev napak.

Zdaj pa izboljšajmo naše teste.

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(); }); }); });

Preizkusi zdaj preverjajo, ali je bil zunanji API med klicem našega API-ja z ustreznimi podatki.

Dodamo lahko tudi druge teste, ki preverjajo, kako se obnaša naš API na podlagi zunanjih odzivnih kod API:

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(); }); }); });

Seveda je odvisno od tega, kako boste v svoji aplikaciji obravnavali napake API-jev tretjih oseb. Toda vi razumete bistvo.

Za zagon teh testov moramo vsebniku myapp povedati, kateri je osnovni URL storitve tretje osebe:

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

Zaključek in nekaj drugih misli

Upajmo, da vam je ta članek predstavil, kaj lahko Docker sestavi za vas, ko gre za testiranje API-jev. Celoten primer je tukaj na GitHubu.

Uporaba Dockerjevega sestavljanja omogoča, da se testi hitro izvajajo v okolju blizu produkcije. Ne zahteva prilagoditev kode komponente. Edina zahteva je podpora konfiguraciji, ki jo vodijo spremenljivke okolja.

Logika komponent v tem primeru je zelo preprosta, vendar načela veljajo za kateri koli API. Vaši testi bodo le daljši ali bolj zapleteni. Veljajo tudi za kateri koli tehnološki sklad, ki ga lahko vstavite v posodo (to so vsi). In ko ste že tam, vas čaka še en korak do namestitve vaših zabojnikov v proizvodnjo, če je to potrebno.

Če trenutno nimate preizkusov, vam priporočam, da začnete: od konca do konca testiranje z Docker compose. Tako preprosto je, da bi lahko prvi test opravili v nekaj urah. Če imate vprašanja ali potrebujete nasvet, se obrnite na mene. Z veseljem bi pomagal.

Upam, da vam je bil ta članek všeč in boste začeli preizkušati svoje API-je z Docker Compose. Ko pripravite teste, jih lahko sprostite na naši platformi za neprekinjeno integracijo Fire CI.

Še zadnja ideja za uspeh z avtomatiziranim testiranjem.

Ko gre za vzdrževanje velikih testnih paketov, je najpomembnejša lastnost, da je teste enostavno brati in razumeti. To je ključnega pomena, da motivirate svojo ekipo, da preskusi posodabljajo. Malo verjetno je, da bi bili okviri zapletenih testov dolgoročno pravilno uporabljeni.

Ne glede na sklad za vaš API, boste morda želeli razmisliti o uporabi chai / mocha za pisanje testov zanj. Morda se zdi nenavadno imeti različne sklade za izvajalno kodo in preskusno kodo, če pa bo delo opravljeno ... Kot lahko vidite iz primerov v tem članku, je preskušanje API-ja REST z chai / mocha čim bolj preprosto . Krivulja učenja je blizu nič.

Torej, če sploh nimate testov in imate API REST za preskušanje, napisan v Javi, Pythonu, RoR, .NET ali katerem koli drugem skladu, lahko poskusite chai / mocha.

Če se sprašujete, kako sploh začeti z nenehno integracijo, sem o tem napisal širši vodnik. Tukaj je: Kako začeti z nenehno integracijo

Prvotno objavljeno na spletnem dnevniku Fire CI.