Pilnīgs ceļvedis par pilnīgu API testēšanu ar Docker

Pārbaude ir sāpes kopumā. Daži neredz jēgu. Daži to uzskata, bet domā par to kā par papildu soli, kas viņus palēnina. Dažreiz testi ir, bet tie ir ļoti ilgi vai nestabili. Šajā rakstā jūs redzēsiet, kā jūs varat izstrādāt testus pats ar Docker.

Mēs vēlamies ātrus, saturīgus un uzticamus testus, kas uzrakstīti un uzturēti ar minimālām pūlēm. Tas nozīmē testus, kas jums kā izstrādātājam ir noderīgi ikdienā. Tām vajadzētu palielināt jūsu produktivitāti un uzlabot programmatūras kvalitāti. Pārbaužu veikšana, jo visi saka: "Jums vajadzētu būt testiem", nav labi, ja tas jūs palēnina.

Apskatīsim, kā to panākt ar ne tik daudz pūļu.

Piemērs, kuru mēs pārbaudīsim

Šajā rakstā mēs pārbaudīsim API, kas izveidota ar Node / express, un testēšanai izmantosim chai / mocha. Esmu izvēlējies JS'y steku, jo kods ir ļoti īss un viegli lasāms. Piemērotie principi ir derīgi jebkurai tehnoloģiju kaudzei. Turpiniet lasīt, pat ja Javascript padara jūs slimu.

Piemērs aptvers vienkāršu CRUD galapunktu kopu lietotājiem. Tas ir vairāk nekā pietiekami, lai izprastu šo koncepciju un piemērotos sarežģītākai jūsu biznesa biznesa loģikai.

Mēs API izmantosim diezgan standarta vidi:

  • Postgres datu bāze
  • Redisa kopa
  • Lai veiktu savu darbu, mūsu API izmantos citas ārējās API

Jūsu API var būt nepieciešama cita vide. Šajā rakstā izmantotie principi paliks nemainīgi. Lai palaistu jebkuru nepieciešamo komponentu, jūs izmantosiet dažādus Docker bāzes attēlus.

Kāpēc Dokers? Un patiesībā Docker Compose

Šajā sadaļā ir daudz argumentu par labu Docker izmantošanai testēšanai. Jūs varat to izlaist, ja vēlaties uzreiz nokļūt tehniskajā daļā.

Sāpīgās alternatīvas

Lai pārbaudītu savu API tuvu ražošanas videi, jums ir divas iespējas. Jūs varat izsmiet vidi koda līmenī vai veikt testus reālā serverī ar instalētu datu bāzi utt.

Ņirgāšanās par visu koda līmenī pārblīvē mūsu API kodu un konfigurāciju. Tas bieži vien nav pārāk reprezentatīvs par to, kā API rīkosies ražošanā. Lietas vadīšana reālā serverī ir ļoti sarežģīta infrastruktūrā. Tas ir daudz iestatīšanas un apkopes, un tas nav mērogā. Ja jums ir koplietojama datu bāze, varat vienlaikus izpildīt tikai 1 testu, lai pārliecinātos, ka testa braucieni netraucē viens otram.

Docker Compose ļauj mums iegūt labāko no abām pasaulēm. Tas izveido visu mūsu izmantoto ārējo daļu "konteineros" versijas. Tas ir ņirgāšanās, bet ārpus mūsu koda. Mūsu API domā, ka tas atrodas reālā fiziskā vidē. Docker sastādīšana arī izveidos izolētu tīklu visiem konteineriem attiecīgajam testa braucienam. Tas ļauj vairākus no tiem paralēli palaist lokālajā datorā vai CI resursdatorā.

Pārspīlēt?

Jūs varētu domāt, vai tas nav pārspīlēts, ja vispār veicat testus no gala līdz beigām, izmantojot Docker komponēšanu. Kā ir tikai ar vienības testu veikšanu tā vietā?

Pēdējo 10 gadu laikā lielās monolītās lietojumprogrammas ir sadalītas mazākos pakalpojumos (tendence uz rosīgajiem "mikropakalpojumiem"). Konkrētais API komponents ir atkarīgs no vairākām ārējām daļām (infrastruktūras vai citām API). Kad pakalpojumi kļūst mazāki, integrācija ar infrastruktūru kļūst par lielāku darba daļu.

Jums jāsaglabā neliela plaisa starp ražošanu un attīstības vidi. Pretējā gadījumā rodas problēmas, dodoties uz ražošanas izvietošanu. Pēc definīcijas šīs problēmas parādās iespējami sliktākajā brīdī. Tie izraisīs steidzamus labojumus, kvalitātes kritumu un neapmierinātību komandā. To neviens negrib.

Jūs varētu domāt, vai Docker sastādītās pārbaudes līdz galam ir ilgākas nekā tradicionālie vienības testi. Ne īsti. Zemāk redzamajā piemērā redzēsit, ka testus mēs varam viegli turēt mazāk par 1 minūti un ar lielu labumu: testi atspoguļo lietojuma uzvedību reālajā pasaulē. Tas ir vērtīgāk nekā zināt, vai jūsu klase kaut kur lietotnes vidū darbojas labi vai nē.

Turklāt, ja jums pašlaik nav neviena testa, sākums no gala līdz beigām sniedz lielas priekšrocības par nelielu piepūli. Jūs zināt, ka visi lietojumprogrammas skursteņi darbojas kopā visbiežāk sastopamajiem scenārijiem. Tas jau ir kaut kas! Turpmāk jūs vienmēr varat precizēt stratēģiju, lai pārbaudītu kritiskās lietojumprogrammas daļas.

Mūsu pirmais pārbaudījums

Sāksim ar vienkāršāko daļu: mūsu API un Postgres datu bāzi. Un palaidīsim vienkāršu CRUD testu. Kad šī sistēma ir izveidota, mēs varam pievienot vairāk funkciju gan savam komponentam, gan testam.

Šeit ir mūsu minimālais API ar GET / POST, lai izveidotu un uzskaitītu lietotājus:

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

Šeit ir mūsu testi, kas rakstīti ar chai. Testi izveido jaunu lietotāju un atgūst to. Var redzēt, ka testi nekādā veidā nav saistīti ar mūsu API kodu. SERVER_URLMainīgais precizē galapunkta testu. Tā var būt vietēja vai attāla vide.

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

Labi. Tagad, lai pārbaudītu mūsu API, definēsim Docker komponēšanas vidi. Izsauktajā failā docker-compose.ymltiks aprakstīti konteineri, kas Docker jāpalaiž.

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

Kas tad mums te ir. Ir 3 konteineri:

  • db izveido jaunu PostgreSQL gadījumu. Mēs izmantojam publisko Postgres attēlu no Docker Hub. Mēs iestatām datu bāzes lietotājvārdu un paroli. Mēs sakām Dockeram atklāt portu 5432, kuru datu bāze klausīsies, lai citi konteineri varētu izveidot savienojumu
  • myapp ir konteiners, kurā darbosies mūsu API. buildKomanda stāsta dokers faktiski veidot konteineru attēlu no mūsu avota. Pārējais ir kā db konteiners: vides mainīgie un porti
  • myapp-tests ir konteiners, kas veiks mūsu testus. Tas izmantos to pašu attēlu kā myapp, jo kods jau būs tur, tāpēc nav nepieciešams to veidot no jauna. Tvertnē node db/init.js && yarn testpalaistās komandas inicializēs datu bāzi (izveidos tabulas utt.) Un veiks testus. Mēs izmantojam dockerize, lai gaidītu, kamēr visi nepieciešamie serveri darbojas un darbojas. Šīs depends_oniespējas nodrošina, ka konteineri sākas noteiktā secībā. Tas nenodrošina, ka DB konteinerā esošā datu bāze patiešām ir gatava pieņemt savienojumus. Tāpat arī tas, ka mūsu API serveris jau darbojas.

Vides definīcija ir kā 20 ļoti viegli saprotama koda rindiņas. Vienīgā prātīgā daļa ir vides definīcija. Lietotāju vārdiem, parolēm un URL jābūt konsekventiem, lai konteineri varētu faktiski darboties kopā.

Viena lieta, kas jāievēro, ir tas, ka Docker sastādīšana radīto konteineru saimniekdatoram noteiks konteinera nosaukumu. Tātad datubāze nebūs pieejama zem localhost:5432, bet db:5432. Tādā pašā veidā mūsu API tiks apkalpots zem myapp:8000. Šeit nav neviena veida vietējā hosta.

Tas nozīmē, ka jūsu API ir jāatbalsta vides mainīgie, kad runa ir par vides definīciju. Nav cietā kodējuma. Bet tam nav nekāda sakara ar Dokeru vai šo rakstu. Konfigurējama lietojumprogramma ir 12 faktoru lietotnes manifesta 3. punkts, tāpēc jums tas jau jādara.

Pēdējā lieta, kas mums jāpasaka Docker, ir tas, kā faktiski izveidot konteineru myapp . Mēs izmantojam Dockerfile kā zemāk. Saturs ir specifisks jūsu tehnoloģiju kaudzei, bet ideja ir apvienot jūsu API palaistā serverī.

Zemāk esošajā piemērā par mūsu mezglu API tiek instalēta programma Dockerize, instalētas API atkarības un kopēts API kods konteinerā (serveris ir rakstīts neapstrādātā JS, tāpēc nav nepieciešams to apkopot).

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

Parasti no līnijas WORKDIR ~/appun zemāk jūs palaistu komandas, kas izveidotu jūsu lietojumprogrammu.

Un šeit ir komanda, kuru izmantojam, lai palaistu testus:

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

Šī komanda Docker sacīs, lai komponents izveidotu mūsu docker-compose.ymlfailā definētos komponentus . --buildKarogs liks veidot no Operētājsistēmai konteinera izpildot saturu no Dockerfileiepriekš. --abort-on-container-exitTeiks dokers sacerēt shutdown vidi, cik drīz vien vienu konteineru izejām.

Tas darbojas labi, jo vienīgais komponents, kas paredzēts izejai, ir testa konteinera myapp testi pēc testu veikšanas. Ķirsis uz kūkas, docker-composekomanda izies ar tādu pašu izejas kodu kā konteiners, kas izraisīja izeju. Tas nozīmē, ka mēs varam pārbaudīt, vai testi ir izdevušies, vai ne, izmantojot komandrindu. Tas ir ļoti noderīgi automatizētām būvēm KI vidē.

Vai tā nav ideāla testa iestatīšana?

Pilns piemērs ir šeit, vietnē GitHub. Varat klonēt repozitoriju un palaist komandu Docker Compose:

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

Protams, jums ir nepieciešams instalēt Docker. Docker ir apgrūtinoša tendence piespiest jūs reģistrēties kontā, lai tikai lejupielādētu lietu. Bet patiesībā jums tas nav jādara. Dodieties uz laidiena piezīmēm (saite Windows un saite Mac) un lejupielādējiet nevis jaunāko, bet tieši iepriekšējo versiju. Šī ir tieša lejupielādes saite.

Pats pirmais testu brauciens būs ilgāks nekā parasti. Tas ir tāpēc, ka Docker būs jālejupielādē jūsu konteineru bāzes attēli un kešatmiņā dažas lietas. Nākamie braucieni būs daudz ātrāki.

Žurnāli no skrējiena izskatīsies šādi. Var redzēt, ka Docker ir pietiekami foršs, lai žurnālus no visiem komponentiem varētu ievietot vienā un tajā pašā laika skalā. Tas ir ļoti ērti, meklējot kļūdas.

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

Mēs varam redzēt, ka db ir konteiners, kas inicializē visilgāk. Ir jēga. Kad tas ir izdarīts, sākas testi. Kopējais mana klēpjdatora darbības laiks ir 16 sekundes. Salīdzinot ar 880 sekundēm, kas tika izmantotas, lai faktiski veiktu testus, tas ir daudz. Praksē testi, kuru ilgums nepārsniedz 1 minūti, ir zelts, jo tas ir gandrīz tūlītējs atgriezeniskā saite. 15 sekundes virs galvas ir pirkšanas laiks, kas būs nemainīgs, pievienojot vairāk testu. Jūs varētu pievienot simtiem testu un tomēr izpildes laiku saglabāt zem 1 minūtes.

Voilà! Mūsu testa sistēma ir izveidota un darbojas. Reālās pasaules projektā nākamie soļi būtu uzlabot API funkcionālo pārklājumu, izmantojot vairāk testu. Apsvērsim aptvertās CRUD operācijas. Ir pienācis laiks pievienot vairāk elementu mūsu testa videi.

Redis kopas pievienošana

Pievienosim vēl vienu elementu mūsu API videi, lai saprastu, kas tam nepieciešams. Spoilera brīdinājums: tas nav daudz.

Iedomāsimies, ka mūsu API lietotāju sesijas glabā Redis kopā. Ja jūs domājat, kāpēc mēs to darītu, iedomājieties 100 jūsu API gadījumus ražošanā. Lietotāji nokļūst vienā vai otrā serverī, pamatojoties uz apļa slodzes līdzsvarošanu. Katrs pieprasījums ir jāapstiprina.

Lai pārbaudītu privilēģijas un citu lietojumprogrammu specifisko biznesa loģiku, ir nepieciešami lietotāja profila dati. Viens veids, kā iet, ir veikt turp un atpakaļ uz datu bāzi, lai ielādētu datus katru reizi, kad tas nepieciešams, taču tas nav ļoti efektīvi. Izmantojot atmiņas datu bāzes kopu, dati ir pieejami visos serveros par vietējā mainīgā nolasīšanas izmaksām.

Šādi jūs uzlabojat savu Docker sastādīšanas testa vidi, izmantojot papildu pakalpojumu. Pievienosim Redis kopu no oficiālā Docker attēla (es esmu saglabājis tikai jaunās faila daļas):

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

Var redzēt, ka tas nav daudz. Mēs pievienojām jaunu konteineru ar nosaukumu redis . Tas izmanto oficiālo minimālo redis attēlu ar nosaukumu redis:alpine. Mēs pievienojām Redis resursdatora un porta konfigurāciju mūsu API konteineram. Pirms testu veikšanas mēs testus esam gaidījuši, kā arī citus konteinerus.

Pārveidosim savu lietojumprogrammu, lai faktiski izmantotu Redis kopu:

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

Tagad mainīsim savus testus, lai pārbaudītu, vai Redis kopa ir aizpildīta ar pareizajiem datiem. Tāpēc myapp-tests konteiners saņem arī Redis resursdatora un porta konfigurāciju 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(); }); }); });

Skatiet, cik viegli tas bija. Pārbaudēm varat izveidot sarežģītu vidi, piemēram, montējot Lego ķieģeļus.

Mēs varam redzēt vēl vienu šāda veida konteinerizētas pilnīgas vides testēšanas priekšrocību. Testi faktiski var izpētīt vides komponentus. Mūsu testi var ne tikai pārbaudīt, vai mūsu API atgriež pareizos atbildes kodus un datus. Mēs varam arī pārbaudīt, vai Redis kopas datiem ir atbilstošas ​​vērtības. Mēs varētu pārbaudīt arī datu bāzes saturu.

Pievienojot API izspēles

API komponentu kopīgs elements ir citu API komponentu izsaukšana.

Pieņemsim, ka mūsu API, veidojot lietotāju, jāpārbauda, ​​vai lietotājiem nav surogātpasta e-pastu. Pārbaude tiek veikta, izmantojot trešās puses pakalpojumu:

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

Tagad mums ir problēma kaut ko pārbaudīt. Mēs nevaram izveidot nevienu lietotāju, ja API nav pieejama surogātpasta e-pastiem. Mūsu API pārveidošana, lai testa režīmā apietu šo soli, ir bīstama koda pārblīvēšana.

Pat ja mēs varētu izmantot reālo trešo personu pakalpojumu, mēs to nevēlamies darīt. Parasti mūsu testiem nevajadzētu būt atkarīgiem no ārējās infrastruktūras. Pirmkārt, tāpēc, ka jūs, iespējams, daudz veicat savus testus kā daļu no sava CI procesa. Nav tik forši patērēt citu ražošanas API šim nolūkam. Otrkārt, API var īslaicīgi nedarboties, ja nepareizu iemeslu dēļ jūsu testi neizdosies.

Pareizais risinājums ir ņirgāties par ārējām API mūsu testos.

Nav nepieciešams nekāds izdomāts ietvars. Mēs izveidosim vispārēju izspēli vaniļas JS ~ 20 koda rindiņās. Tas mums dos iespēju kontrolēt to, ko API atgriezīs mūsu komponentā. Tas ļauj pārbaudīt kļūdu scenārijus.

Tagad uzlabosim savus testus.

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

Pārbaudēs tagad tiek pārbaudīts, vai, veicot izsaukumu uz mūsu API, ārējam API ir trāpīti atbilstoši dati.

Mēs varam pievienot arī citus testus, pārbaudot, kā darbojas mūsu API, pamatojoties uz ārējiem API atbildes kodiem:

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

Protams, tas, kā jūs apstrādājat kļūdas no trešās puses API jūsu lietojumprogrammā, ir atkarīgs tikai no jums. Bet tu saproti.

Lai veiktu šos testus, konteineram myapp ir jāpasaka, kāds ir trešās puses pakalpojuma URL:

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

Secinājums un dažas citas domas

Cerams, ka šis raksts ļāva jums nobaudīt to, ko Docker sacerējums var jums palīdzēt, kad runa ir par API testēšanu. Pilns piemērs ir šeit, vietnē GitHub.

Izmantojot Docker komponēšanu, testi ātri darbojas vidē, kas ir tuvu ražošanai. Tam nav nepieciešami komponenta koda pielāgojumi. Vienīgā prasība ir atbalstīt ar vides mainīgajiem balstītu konfigurāciju.

Komponentu loģika šajā piemērā ir ļoti vienkārša, taču principi attiecas uz jebkuru API. Jūsu testi būs tikai garāki vai sarežģītāki. Tie attiecas arī uz visām tehnikas kaudzēm, kuras var ievietot konteinerā (tas ir visi no tiem). Kad esat ieradies, jums ir viens solis, lai vajadzības gadījumā izvietotu savus konteinerus ražošanā.

Ja jums pašlaik nav testu, iesakām sākt ar testēšanu ar Docker compose. Tas ir tik vienkārši, ka pirmais tests varētu notikt dažu stundu laikā. Nekautrējieties sazināties ar mani, ja jums ir jautājumi vai nepieciešama padoma. Es labprāt palīdzētu.

Es ceru, ka jums patika šis raksts un jūs sāksit testēt savas API, izmantojot Docker Compose. Kad testi būs gatavi, tos varēsit palaist ārpus mūsu nepārtrauktās integrācijas platformas Fire CI.

Vēl viena ideja gūt panākumus ar automatizētu testēšanu.

Runājot par lielu testa komplektu uzturēšanu, vissvarīgākā iezīme ir tā, ka testus ir viegli lasīt un saprast. Tas ir galvenais, lai motivētu komandu atjaunināt testus. Maz ticams, ka ilgtermiņā sarežģītas testu sistēmas tiks pareizi izmantotas.

Neatkarīgi no jūsu API kaudzes, ieteicams apsvērt iespēju izmantot chai / mocha, lai rakstītu tam testus. Var šķist neparasti, ka izpildlaika kodam un testa kodam ir dažādas kaudzes, bet, ja tas tiek paveikts, darbs ... Kā redzat no šī raksta piemēriem, REST API testēšana ar chai / mocha ir tikpat vienkārša, cik tā izpaužas . Mācīšanās līkne ir tuvu nullei.

Tātad, ja jums vispār nav testu un jums ir REST API testēšanai, kas rakstīts Java, Python, RoR, .NET vai kādā citā kaudzē, jūs varētu apsvērt iespēju izmēģināt chai / mocha.

Ja jūs domājat, kā vispār sākt ar nepārtrauktu integrāciju, es par to esmu uzrakstījis plašāku ceļvedi. Lūk, kā: kā sākt darbu ar nepārtrauktu integrāciju

Sākotnēji publicēts Fire CI emuārā.