Cjelovit vodič za testiranje API-ja s Dockerom

Testiranje je bol općenito. Neki ne vide smisao. Neki to vide, ali to smatraju dodatnim korakom koji ih usporava. Ponekad postoje testovi, ali oni se jako dugo rade ili su nestabilni. U ovom ćete članku vidjeti kako sami možete sami inženjerirati testove s Dockerom.

Želimo brze, sadržajne i pouzdane testove napisane i održavane uz minimalan napor. To znači testove koji su vam korisni kao programeru svakodnevno. Oni bi trebali povećati vašu produktivnost i poboljšati kvalitetu vašeg softvera. Imati testove jer svi kažu "trebali biste napraviti testove" nije dobro ako vas usporava.

Pogledajmo kako to postići s ne toliko truda.

Primjer koji ćemo testirati

U ovom ćemo članku testirati API izgrađen pomoću Node / expressa i koristiti chai / mocha za testiranje. Odabrao sam JS'y stog jer je kod super kratak i lak za čitanje. Primijenjena načela vrijede za bilo koji tehnološki skup. Nastavite čitati čak i ako vam Javascript pozli.

Primjer će pokriti jednostavan skup CRUD krajnjih točaka za korisnike. Više je nego dovoljno da shvatite koncept i primijenite se na složeniju poslovnu logiku vašeg API-ja.

Koristit ćemo prilično standardno okruženje za API:

  • Baza podataka Postgres
  • Skupina Redisa
  • Naš API će za obavljanje svog posla koristiti druge vanjske API-je

Vaš će API možda trebati drugačije okruženje. Načela primijenjena u ovom članku ostat će ista. Upotrijebit ćete različite osnovne Dockerove slike za pokretanje bilo koje komponente koja bi vam trebala.

Zašto Docker? A zapravo Docker Compose

Ovaj odjeljak sadrži puno argumenata u korist korištenja Dockera za testiranje. Možete ga preskočiti ako želite odmah doći do tehničkog dijela.

Bolne alternative

Za testiranje API-ja u blizini proizvodnog okruženja imate dva izbora. Možete se rugati okruženju na razini koda ili pokretati testove na stvarnom poslužitelju s instaliranom bazom podataka itd.

Izrugivanje svemu na razini koda pretrpava kôd i konfiguraciju našeg API-ja. Također često nije baš reprezentativno kako će se API ponašati u proizvodnji. Pokretanje stvari na pravom poslužitelju infrastrukturno je teško. Puno je postavljanja i održavanja, a ne mjeri se. Imajući zajedničku bazu podataka, istodobno možete pokrenuti samo 1 test kako biste bili sigurni da se probne vožnje međusobno ne mešaju.

Docker Compose omogućuje nam postizanje najboljeg iz oba svijeta. Stvara "kontejnerske" verzije svih vanjskih dijelova koje koristimo. To je podrugljivo, ali s vanjske strane našeg koda. Naš API misli da je u stvarnom fizičkom okruženju. Docker compose također će stvoriti izoliranu mrežu za sve spremnike za dano probno pokretanje. To vam omogućuje paralelno pokretanje nekoliko njih na vašem lokalnom računalu ili CI hostu.

Pretjerano?

Možda se pitate nije li pretjerano uopće izvoditi testove s kraja na kraj s Docker skladanjem. Što je sa samo pokretanjem jediničnih testova?

Posljednjih 10 godina veliki monolitni programi podijeljeni su u manje usluge (u trendu prema zbunjenim "mikroservisima"). Dana API komponenta oslanja se na više vanjskih dijelova (infrastruktura ili drugi API-ji). Kako se usluge smanjuju, integracija s infrastrukturom postaje sve veći dio posla.

Trebali biste zadržati mali jaz između vaše proizvodnje i vašeg razvojnog okruženja. Inače će se pojaviti problemi kada se ide u proizvodnju. Po definiciji ti se problemi pojavljuju u najgorem mogućem trenutku. Dovest će do hitnih popravaka, pada kvalitete i frustracije za tim. To nitko ne želi.

Možda se pitate hoće li se testovi do kraja s Dockerovim sastavljanjem izvoditi duže od tradicionalnih jediničnih testova. Ne baš. U primjeru u nastavku vidjet ćete da testove možemo lako zadržati manje od 1 minute, i to u velikoj koristi: testovi odražavaju ponašanje aplikacije u stvarnom svijetu. To je vrjednije od saznanja radi li vaš razred negdje u sredini aplikacije u redu ili ne.

Također, ako trenutno nemate testova, započinjanje od kraja do kraja donosi vam velike koristi za malo truda. Znat ćete da svi snopovi aplikacija rade zajedno za najčešće scenarije. To je već nešto! Od tamo uvijek možete pročistiti strategiju jedinstvenog testiranja kritičnih dijelova vaše aplikacije.

Naš prvi test

Počnimo s najlakšim dijelom: našim API-jem i bazom podataka Postgres. I pokrenimo jednostavan CRUD test. Jednom kada postavimo taj okvir, možemo dodati više značajki i našoj komponenti i testu.

Evo našeg minimalnog API-ja s GET / POST za stvaranje i popis korisnika:

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

Evo naših testova napisanih chai. Testovi stvaraju novog korisnika i vraćaju ga natrag. Možete vidjeti da testovi ni na koji način nisu povezani s kodom našeg API-ja. SERVER_URLVarijabla određuje završnu točku za ispitivanje. To može biti lokalno ili udaljeno okruženje.

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. Sada da testiramo naš API, definirajmo Dockerovo okruženje za sastavljanje. Datoteka pod nazivom docker-compose.ymlopisat će spremnike koje Docker treba pokrenuti.

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

Pa što imamo ovdje. Postoje 3 spremnika:

  • db okreće novu instancu PostgreSQL-a. Koristimo javnu Postgres sliku s Docker Hub-a. Postavljamo korisničko ime i lozinku baze podataka. Dockeru kažemo da izloži port 5432 koji će baza podataka slušati kako bi se mogli povezati drugi spremnici
  • myapp je spremnik koji će pokretati naš API. buildNaredba kaže lučki radnik zapravo izgraditi sliku kontejner od našeg izvora. Ostalo je poput db spremnika: varijable okruženja i priključci
  • myapp-tests je spremnik koji će izvršiti naše testove. Upotrijebit će istu sliku kao i myapp, jer će kôd već biti tamo, pa ga nema potrebe ponovno graditi. Naredba node db/init.js && yarn testizvršena na spremniku inicijalizirat će bazu podataka (stvoriti tablice itd.) I pokrenuti testove. Koristimo dockerize da bismo pričekali da svi potrebni poslužitelji rade i rade. U depends_onmogućnosti će osigurati da spremnici početi u određenom redoslijedu. Ne osigurava da je baza podataka unutar db spremnika zapravo spremna za prihvaćanje veza. Niti je naš API poslužitelj već pokrenut.

Definicija okoliša je poput 20 redaka vrlo lako razumljivog koda. Jedini mozak je definicija okoliša. Korisnička imena, lozinke i URL-ovi moraju biti dosljedni kako bi spremnici mogli stvarno raditi zajedno.

Jedna stvar koju treba primijetiti je da će Docker compose domaćinima spremnika koje kreira postaviti na ime spremnika. Dakle, baza podataka neće biti dostupna pod, localhost:5432ali db:5432. Na isti način na koji će se služiti naš API myapp:8000. Ovdje nema bilo kojeg lokalnog hosta.

To znači da vaš API mora podržavati varijable okoline kada je u pitanju definicija okoline. Nema tvrdo kodiranih stvari. Ali to nema nikakve veze s Dockerom ili ovim člankom. Konfigurabilna aplikacija je točka 3 manifesta aplikacije s 12 faktora, pa biste to već trebali raditi.

Posljednje što moramo reći Dockeru je kako zapravo izgraditi myapp spremnika . Koristimo Dockerfile kao u nastavku. Sadržaj je specifičan za vašu tehnološku hrpu, ali ideja je povezati vaš API u poslužitelj koji se može pokrenuti.

Primjer u nastavku za naš Node API instalira Dockerize, instalira API ovisnosti i kopira kod API-ja unutar spremnika (poslužitelj je napisan u sirovom JS-u, tako da ga nije potrebno kompajlirati).

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

Tipično iz retka WORKDIR ~/appi ispod pokrenuli biste naredbe koje će graditi vašu aplikaciju.

I evo naredbe koju koristimo za pokretanje testova:

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

Ova naredba reći će Dockeru da sastavi da zavrti komponente definirane u našoj docker-compose.ymldatoteci. Oznaka --buildće pokrenuti izgradnju myapp spremnika izvršavanjem sadržaja Dockerfilegore navedenog. --abort-on-container-exitĆe reći Docker sastaviti isključivanja okoliša čim jedan kontejner izlaze.

To dobro funkcionira jer je jedina komponenta kojoj je cilj izaći testni spremnik myapp-testovi nakon izvršenih testova. Trešnja na torti, docker-composenaredba će izaći s istim izlaznim kodom kao i spremnik koji je pokrenuo izlaz. To znači da s naredbenog retka možemo provjeriti jesu li testovi uspjeli ili ne. Ovo je vrlo korisno za automatizirane gradnje u CI okruženju.

Nije li to savršeno postavljanje testa?

Potpuni primjer nalazi se ovdje na GitHubu. Možete klonirati spremište i pokrenuti naredbu docker compose:

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

Naravno, trebate instalirati Docker. Docker vas muči s prisilom da se prijavite za račun samo da biste stvar preuzeli. Ali zapravo ne morate. Idite na bilješke o izdanju (veza za Windows i veza za Mac) i preuzmite ne najnoviju verziju, već onu prije. Ovo je izravna poveznica za preuzimanje.

Prvo pokretanje testova bit će duže nego obično. To je zato što će Docker morati preuzeti osnovne slike za vaše spremnike i predmemorirati nekoliko stvari. Sljedeće vožnje bit će puno brže.

Evidencije iz trčanja izgledat će kao u nastavku. Možete vidjeti da je Docker dovoljno cool da zapisnike svih komponenata stavi na isti vremenski slijed. Ovo je vrlo zgodno kada se traže pogreške.

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 da je db spremnik koji se najdulje inicijalizira. Ima smisla. Nakon što završe, testovi započinju. Ukupno vrijeme rada mog prijenosnog računala je 16 sekundi. U usporedbi s 880ms koji se koriste za stvarno provođenje testova, to je puno. U praksi su testovi kraći od jedne minute zlatni, jer su to gotovo neposredne povratne informacije. 15-sekundni režijski troškovi vremenom su kupnje koji će biti stalni dok dodajete još testova. Možete dodati stotine testova i zadržati vrijeme izvršavanja ispod 1 minute.

Voilà! Imamo svoj testni okvir i pokrenut. U projektu iz stvarnog svijeta sljedeći koraci bili bi poboljšati funkcionalnu pokrivenost vašeg API-ja s više testova. Razmotrimo CRUD operacije koje su pokrivene. Vrijeme je da u naše testno okruženje dodamo još elemenata.

Dodavanje Redis klastera

Dodajmo još jedan element u naše API okruženje da bismo razumjeli što je potrebno. Upozorenje spojlera: nije puno.

Zamislimo da naš API zadržava korisničke sesije u klasteru Redis. Ako se pitate zašto bismo to učinili, zamislite 100 primjeraka vašeg API-ja u proizvodnji. Korisnici pogađaju jedan ili drugi poslužitelj na temelju uravnoteženja opterećenja. Svaki zahtjev mora biti ovjeren.

To zahtijeva podatke korisničkog profila za provjeru privilegija i druge poslovne logike specifične za aplikaciju. Jedan od načina je kružno putovanje do baze podataka kako biste dohvatili podatke svaki put kada vam zatrebaju, ali to nije vrlo učinkovito. Korištenje klastera baze podataka u memoriji čini podatke dostupnima na svim poslužiteljima po cijeni čitanja lokalne varijable.

Na ovaj način poboljšavate svoje okruženje za testiranje sastavljanja Dockera dodatnom uslugom. Dodajmo Redis klaster sa službene Dockerove slike (zadržao sam samo nove dijelove 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 ... ...

Vidite da nije puno. Dodali smo novi spremnik nazvan redis . Koristi službenu minimalnu redis sliku tzv redis:alpine. Dodali smo konfiguraciju hosta i porta Redis u naš API spremnik. I učinili smo da testovi čekaju na njega kao i ostali spremnici prije izvođenja testova.

Izmijenimo našu aplikaciju tako da zapravo koristi klaster 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); } });

Promijenimo sada svoje testove kako bismo provjerili je li klaster Redis popunjen pravim podacima. Zbog toga spremnik za testove myapp također dobiva konfiguraciju domaćina i porta 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(); }); }); });

Pogledajte kako je ovo bilo lako. Za svoje testove možete izgraditi složeno okruženje poput sastavljanja Lego kockica.

Možemo vidjeti još jednu korist ove vrste kontejneriziranog testiranja punog okoliša. Testovi zapravo mogu proučiti komponente okoliša. Naši testovi ne mogu samo provjeriti vraća li naš API ispravne kodove odgovora i podatke. Također možemo provjeriti imaju li podaci u klasteru Redis odgovarajuće vrijednosti. Također bismo mogli provjeriti sadržaj baze podataka.

Dodavanje API lažnih

Uobičajeni element za API komponente je pozivanje drugih API komponenata.

Recimo da naš API treba stvoriti neželjenu korisničku e-poštu prilikom stvaranja korisnika. Provjera se vrši pomoću usluge treće strane:

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

Sad imamo problem s bilo čim testiranjem. Ne možemo stvoriti korisnike ako API za otkrivanje neželjene e-pošte nije dostupan. Izmjena našeg API-ja kako bi se zaobišao ovaj korak u testnom načinu rada opasno je pretrpavanje koda.

Čak i ako bismo mogli koristiti stvarnu uslugu treće strane, to ne želimo učiniti. Općenito, naši testovi ne bi trebali ovisiti o vanjskoj infrastrukturi. Prije svega, jer ćete svoje testove vjerojatno puno izvoditi kao dio procesa CI. Nije baš cool konzumirati drugi proizvodni API u tu svrhu. Drugo, API možda privremeno nije u funkciji, što je pogrešno zbog neuspješnih testova.

Pravo rješenje je ismijavanje vanjskih API-ja u našim testovima.

Nema potrebe za bilo kakvim otmjenim okvirom. Izgradit ćemo generički model u vanilije JS u ~ 20 redaka koda. To će nam pružiti priliku da kontroliramo što će API vratiti našoj komponenti. Omogućuje testiranje scenarija pogrešaka.

Sada ćemo poboljšati naše testove.

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

Testovi sada provjeravaju je li vanjski API pogođen odgovarajućim podacima tijekom poziva našem API-ju.

Možemo dodati i druge testove koji provjeravaju kako se naš API ponaša na temelju vanjskih kodova odgovora na 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(); }); }); });

Način na koji ćete postupati s pogreškama API-ja nezavisnih proizvođača u svojoj aplikaciji, naravno ovisi o vama. Ali shvatili ste poantu.

Da bismo pokrenuli ove testove, moramo reći spremniku myapp koji je osnovni URL usluge treće strane:

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

Zaključak i nekoliko drugih misli

Nadamo se da vam je ovaj članak dao okus što Docker sastavlja za vas kad je u pitanju API testiranje. Potpuni primjer nalazi se ovdje na GitHubu.

Korištenje Dockerove kompozicije omogućuje brzo provođenje testova u okruženju bliskom proizvodnji. Ne zahtijeva prilagodbe vašeg koda komponente. Jedini uvjet je podržati konfiguraciju vođenu varijablama okruženja.

Logika komponenata u ovom primjeru vrlo je jednostavna, ali principi se primjenjuju na bilo koji API. Vaši će testovi samo biti duži ili složeniji. Također se primjenjuju na bilo koji tehnološki snop koji se može staviti u spremnik (to su svi oni). A kad stignete, na korak je od postavljanja kontejnera u proizvodnju ako je potrebno.

Ako trenutno nemate testova, preporučujem vam da započnete: kraj s krajem testiranje s Docker compose. Tako je jednostavno da biste svoj prvi test mogli pokrenuti za nekoliko sati. Slobodno me kontaktirajte ako imate pitanja ili trebate savjet. Rado bih pomogao.

Nadam se da vam se svidio ovaj članak i da ćete početi testirati svoje API-je pomoću Docker Compose. Nakon što pripremite testove, možete ih pokrenuti na našoj platformi za kontinuiranu integraciju Fire CI.

Posljednja ideja za uspjeh s automatiziranim testiranjem.

Što se tiče održavanja velikih testnih kompleta, najvažnija je značajka da se testovi lako čitaju i razumiju. To je ključno za motivaciju vašeg tima da testovi budu ažurni. Malo je vjerojatno da će se okviri složenih testova dugoročno pravilno koristiti.

Bez obzira na stog za vaš API, možda biste trebali razmotriti upotrebu chai / mocha za pisanje testova za njega. Možda se čini neobičnim imati različite stogove za runtime kod i testni kôd, ali ako posao završi ... Kao što možete vidjeti iz primjera u ovom članku, testiranje REST API-ja s chai / mocha jednako je jednostavno . Krivulja učenja je blizu nule.

Dakle, ako uopće nemate testova i imate REST API za testiranje napisan na Javi, Pythonu, RoR-u, .NET-u ili bilo kojem drugom steku, možete razmisliti o pokušaju chai / mocha-e.

Ako se pitate kako uopće započeti s kontinuiranom integracijom, o tome sam napisao širi vodič. Evo ga: Kako započeti s kontinuiranom integracijom

Izvorno objavljeno na blogu Fire CI.