Teljes útmutató az end-to-end API teszteléshez a Dockerrel

A tesztelés általában fájdalom. Néhányan nem látják értelmét. Néhányan látják, de úgy gondolják, hogy ez egy további lépés, amely lelassítja őket. Néha vannak tesztek, de nagyon hosszúak vagy instabilak. Ebben a cikkben megtudhatja, hogyan tervezhet saját maga teszteket a Dockerrel.

Gyors, tartalmas és megbízható teszteket akarunk, amelyeket minimális erőfeszítéssel írnak és tartanak fenn. Olyan teszteket jelent, amelyek fejlesztőként mindennap hasznosak. Növelniük kell a termelékenységet és javítani a szoftver minőségét. Azért van teszt, mert mindenki azt mondja, hogy "teszteket kellene készítenie", nem jó, ha lelassítja.

Lássuk, hogyan lehet ezt elérni annyi erőfeszítéssel.

A példát, amelyet tesztelni fogunk

Ebben a cikkben a Node / express segítségével épített API-t fogjuk tesztelni, és a chai / mokát használjuk a teszteléshez. Azért választottam egy JS'y stacket, mert a kód nagyon rövid és könnyen olvasható. Az alkalmazott elvek minden tech stackre érvényesek. Olvassa tovább, még akkor is, ha a Javascript megbetegít.

A példa a CRUD végpontok egyszerű készletét fedi le a felhasználók számára. Több mint elég megérteni a koncepciót és alkalmazni az API összetettebb üzleti logikájára.

Elég szabványos környezetet fogunk használni az API számára:

  • Postgres adatbázis
  • Egy Redis-klaszter
  • API-junk más külső API-kat fog használni munkájának elvégzéséhez

Előfordulhat, hogy az API-nak más környezetre van szüksége. A cikkben alkalmazott elvek változatlanok maradnak. Különböző Docker alapképekkel fog futtatni minden szükséges összetevőt.

Miért éppen Docker? És valójában a Docker Compose

Ez a szakasz sok érvet tartalmaz a Docker teszteléshez való használata mellett. Átugorhatja, ha azonnal el akarja érni a műszaki részt.

A fájdalmas alternatívák

Kétféle lehetőség közül választhat, ha az API-t gyártási környezetben teszteli. Gúnyolhatja a környezetet kódszinten, vagy futtathatja a teszteket egy valódi szerveren, telepítve az adatbázist stb.

Ha mindent megcsúfolunk kódszinten, akkor összetörik az API kódja és konfigurációja. Gyakran nem is nagyon reprezentatív az API viselkedésének a gyártásban. A dolog valódi szerveren történő futtatása infrastruktúra-nehéz. Sok telepítés és karbantartás, és nem méretezhető. Megosztott adatbázissal egyszerre csak 1 tesztet futtathat, hogy a tesztfuttatások ne zavarják egymást.

A Docker Compose lehetővé teszi számunkra, hogy mindkét világból a legjobbat hozzuk ki. Létrehozza az összes általunk használt külső alkatrész "konténeres" változatát. Gúnyos, de kívül esik a kódunkon. API-nk úgy gondolja, hogy valódi fizikai környezetben van. A Docker compose egy izolált hálózatot is létrehoz egy adott tesztfutás összes tárolójához. Ez lehetővé teszi, hogy többet futtasson párhuzamosan a helyi számítógépen vagy a CI-állomáson.

Túlzás?

Kíváncsi lehet, hogy a Docker compose segítségével egyáltalán nem túlzottan teszteljük-e a végpontok közötti teszteket. Mi a helyzet az egység tesztek futtatásával?

Az elmúlt 10 évben a nagy monolit alkalmazásokat kisebb szolgáltatásokra bontották (a zümmögő "mikroszolgáltatások" felé irányultak). Egy adott API-összetevő több külső részre (infrastruktúra vagy más API-ra) támaszkodik. Ahogy a szolgáltatások egyre kisebbek lesznek, az infrastruktúrával való integráció a munka nagyobb részévé válik.

Tartson egy kis szakadékot a termelés és a fejlesztési környezetek között. Ellenkező esetben problémák merülnek fel a gyártási telepítés megkezdése során. Definíció szerint ezek a problémák a lehető legrosszabb pillanatban jelentkeznek. Rohanó javításokhoz, minőségromláshoz és csalódáshoz vezetnek a csapat számára. Ezt senki nem akarja.

Kíváncsi lehet, hogy a Docker végpont-vége tesztek hosszabb ideig futnak-e, mint a hagyományos egység tesztek. Nem igazán. Az alábbi példában láthatja, hogy könnyen 1 perc alatt tarthatjuk a teszteket, és nagy haszonnal járunk: a tesztek tükrözik az alkalmazás valós viselkedését. Ez sokkal értékesebb, mint tudni, hogy az osztály valahol az alkalmazás közepén rendben működik-e vagy sem.

Továbbá, ha még nincsenek tesztjei, a végponttól a végéig történő indítás nagy előnyökkel jár, kevés erőfeszítés nélkül. Tudni fogja, hogy az alkalmazás összes halma együtt működik a leggyakoribb esetekben. Ez már valami! Innentől kezdve mindig finomíthatja a stratégiát az alkalmazás kritikus részeinek tesztelésére.

Az első tesztünk

Kezdjük a legegyszerűbb ponttal: az API-val és a Postgres adatbázissal. Futtassunk egy egyszerű CRUD tesztet. Amint megvan a keretrendszer, további szolgáltatásokat adhatunk mind az összetevőnkhöz, mind a teszthez.

Itt van a minimális API-nk GET / POST segítségével a felhasználók létrehozásához és listázásához:

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

Itt vannak chai-val írt tesztjeink. A tesztek új felhasználót hoznak létre, és visszakeresik. Láthatja, hogy a tesztek semmilyen módon nincsenek összekapcsolva az API kódjával. A SERVER_URLváltozó meghatározza a tesztelni kívánt végpontot. Lehet helyi vagy távoli környezet.

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

Jó. Az API teszteléséhez határozzunk meg egy Docker írási környezetet. Az úgynevezett fájl docker-compose.ymlleírja a Docker futtatásához szükséges tárolókat.

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

Tehát mi van itt. 3 konténer van:

  • A db felpörgeti a PostgreSQL friss példányát. A Docker Hub nyilvános Postgres képét használjuk. Beállítjuk az adatbázis felhasználónevét és jelszavát. Azt mondjuk a Dockernek, hogy tegye ki az 5432-es portot, amelyet az adatbázis hallgat, hogy más konténerek csatlakozhassanak
  • A myapp az a tároló, amely az API-t futtatja. A buildparancs felszólítja a Dockert, hogy a forráskódból építse fel a tároló képet. A többi olyan, mint a db tároló: környezeti változók és portok
  • A myapp-tesztek az a tároló, amely végrehajtja tesztjeinket. Ugyanazt a képet fogja használni, mint a myapp, mert a kód már ott lesz, így nem kell újra felépíteni. A node db/init.js && yarn testtárolón futtatott parancs inicializálja az adatbázist (táblákat készít stb.), És lefuttatja a teszteket. A dockerize használatával megvárjuk, amíg az összes szükséges szerver beindul. Az depends_onopciók biztosítják, hogy a konténerek egy bizonyos sorrendben induljanak. Nem biztosítja, hogy a db tárolóban lévő adatbázis valóban készen áll a kapcsolatok elfogadására. Azt sem, hogy az API-kiszolgálónk már fent van.

A környezet meghatározása olyan, mint 20 sor nagyon könnyen érthető kód. Az egyetlen okos rész a környezet meghatározása. A felhasználói neveknek, jelszavaknak és URL-eknek konzisztenseknek kell lenniük, hogy a tárolók valóban együtt tudjanak működni.

Egy dolgot kell észrevenni, hogy a Docker Compose a létrehozott tárolók gazdagépét a tároló nevére állítja. Így az adatbázis nem lesz elérhető alá localhost:5432, de db:5432. Ugyanúgy, ahogy az API-t is kiszolgáljuk myapp:8000. Itt nincs semmiféle helyi vendéglátó.

Ez azt jelenti, hogy az API-nak támogatnia kell a környezeti változókat, amikor a környezetről van szó. Nincs keménykódolás. De ennek semmi köze sem Dockerhez, sem ehhez a cikkhez. A konfigurálható alkalmazás a 12 faktoros alkalmazás-kiáltvány 3. pontja, ezért már most meg kell tennie.

A legutolsó dolog, amit el kell mondanunk a Docker-nek, hogy miként építsük fel a myapp konténert . Az alábbihoz hasonló Docker fájlt használunk. A tartalom az Ön technikai veremére vonatkozik, de az ötlet az, hogy az API-t egy futtatható szerverbe csomagolja.

Az alábbi példa a Node API-ra telepíti a Dockerize-t, telepíti az API-függőségeket és átmásolja az API kódját a tárolóba (a szerver nyers JS-ben van megírva, így nem kell lefordítania).

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

Általában a sorból WORKDIR ~/appés az alól parancsokat futtat, amelyek felépítik az alkalmazást.

És itt van a tesztek futtatásához használt parancs:

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

Ez a parancs megmondja a Dockernek, hogy fordítsa fel a fájlunkban definiált összetevőket docker-compose.yml. A --buildzászló a Dockerfilefenti tartalom végrehajtásával indítja el a myapp tároló felépítését . Az utasítás --abort-on-container-exitmegmondja a Dockernek, hogy állítsa le a környezetet, amint egy konténer kilép.

Ez jól működik, mivel az egyetlen komponens, amelyből ki kell lépni, a teszttartály myapp-tesztje a tesztek végrehajtása után. Cseresznye a tortán, a docker-composeparancs ugyanazzal a kilépési kóddal fog kilépni, mint a kilépést kiváltó tároló. Ez azt jelenti, hogy a parancssorból ellenőrizhetjük, hogy a tesztek sikeresek-e vagy sem. Ez nagyon hasznos a CI környezetben végzett automatizált építéseknél.

Nem ez a tökéletes tesztbeállítás?

A teljes példa itt található a GitHubon. Klónozhatja a lerakatot és futtathatja a docker compose parancsot:

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

Természetesen telepítenie kell a Dockert. A Docker problémás tendenciája, hogy a dolog letöltésére kényszeríti a fiók regisztrálására. De valójában nem kell. Lépjen a kiadási megjegyzésekhez (link a Windows-hoz és a link Mac-hez), és töltse le nem a legújabb, hanem az előző verziót. Ez egy közvetlen letöltési link.

A tesztek legelső futtatása a szokásosnál hosszabb lesz. Ennek oka, hogy a Dockernek le kell töltenie a tárolók alapképeit, és néhány dolgot gyorsítótárba kell tennie. A következő futások sokkal gyorsabbak lesznek.

A futás naplói az alábbiak szerint fognak kinézni. Láthatja, hogy a Docker elég hűvös ahhoz, hogy az összes komponens naplóit ugyanazon az idővonalon helyezze el. Ez nagyon hasznos, amikor hibákat keres.

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

Láthatjuk, hogy a db a legtovább inicializáló tároló. Van értelme. Miután elkészült, a tesztek megkezdődnek. A laptop teljes futási ideje 16 másodperc. A tesztek tényleges végrehajtásához használt 880 ms-hoz képest ez sok. A gyakorlatban az 1 perc alatt futó tesztek aranyak, mivel szinte azonnali visszajelzés. A 15 másodperces rezsi olyan vásárlási idő, amely állandó lesz, amikor további teszteket ad hozzá. Hozzáadhat több száz tesztet, és még mindig 1 perc alatt tarthatja a végrehajtási időt.

Voálá! Megvan a teszt keretrendszerünk és futunk. Egy valós projektben a következő lépések az API funkcionális lefedettségének növelése további tesztekkel. Tekintsük a lefedett CRUD műveleteket. Itt az ideje, hogy további elemeket vegyünk fel tesztkörnyezetünkbe.

Redis-fürt hozzáadása

Vegyünk fel egy másik elemet az API környezetünkbe, hogy megértsük, mire van szükség. Spoiler riasztás: ez nem sok.

Képzeljük el, hogy az API-nk a felhasználói munkameneteket egy Redis-fürtben tartja. Ha kíváncsi, miért tennénk ezt, képzelje el az API-jának 100 példányát a gyártásban. A felhasználók az egyik vagy másik szervert a körméretes terheléselosztás alapján találják meg. Minden kérést hitelesíteni kell.

Ehhez felhasználói profiladatokra van szükség a jogosultságok és más alkalmazásspecifikus üzleti logika ellenőrzéséhez. Az egyik út az, hogy oda-vissza útra kel az adatbázis, hogy minden alkalommal lekérje az adatokat, amikor csak szüksége van rá, de ez nem túl hatékony. A memóriában lévő adatbázis-fürt használata az összes szerveren elérhetővé teszi az adatokat egy helyi változó beolvasásáért.

Így javíthatja Docker írási tesztkörnyezetét egy további szolgáltatással. Vegyünk fel egy Redis-fürtöt a hivatalos Docker-képből (csak a fájl új részeit tartottam meg):

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

Láthatja, hogy nem sok. Hozzáadtunk egy új konténert, redis néven . A hivatalos minimális redis képet használja redis:alpine. Hozzáadtuk a Redis gazdagép és port konfigurációját az API tárolónkhoz. És elvégeztük a teszteket, hogy a tesztek végrehajtása előtt megvárják a többi tárolót is.

Módosítsuk alkalmazásunkat a Redis-fürt tényleges használatára:

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

Változtassunk most tesztjeinken annak ellenőrzésére, hogy a Redis-fürt megfelelő adatokkal van-e feltöltve. Ezért a myapp-tesztek tárolója a Redis gazdagép és port konfigurációját is beolvassa 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(); }); }); });

Nézze meg, milyen könnyű volt ez. Komplex környezetet építhet ki a tesztjeihez, mint a Lego téglák összeállítása.

Láthatunk egy másik előnyét is ennek a fajta konténeres teljes környezeti tesztelésnek. A tesztek valóban megvizsgálhatják a környezet összetevőit. Tesztjeink nemcsak azt ellenőrizhetik, hogy API-junk visszaadja-e a megfelelő válaszkódokat és adatokat. Azt is ellenőrizhetjük, hogy a Redis-fürt adatainak megfelelő értékei vannak-e. Ellenőrizhettük az adatbázis tartalmát is.

API-modellek hozzáadása

Az API-összetevők közös eleme az, hogy más API-összetevőket hívunk meg.

Tegyük fel, hogy az API-nak a felhasználó létrehozásakor ellenőriznie kell a spam jellegű felhasználói e-maileket. Az ellenőrzés egy harmadik fél szolgáltatásával történik:

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

Most problémánk van bármi tesztelésével. Nem hozhatunk létre felhasználókat, ha a spam jellegű e-mailek észlelésére szolgáló API nem áll rendelkezésre. Ha módosítjuk az API-t, hogy ezt a lépést megkerüljük teszt üzemmódban, a kód veszélyes összezavarása.

Még akkor is, ha igénybe tudnánk venni a valódi harmadik fél szolgáltatását, nem akarjuk ezt megtenni. Általános szabály, hogy tesztjeink nem függhetnek a külső infrastruktúrától. Először is, mert valószínűleg sokat fog futtatni a teszteket a CI-folyamat részeként. Nem olyan klassz egy másik gyártási API-t fogyasztani erre a célra. Másodszor az API átmenetileg leállhat, rossz okokból nem sikerül a tesztje.

A helyes megoldás a külső API-k gúnyolása tesztjeink során.

Nincs szükség semmilyen divatos keretre. ~ 20 kódsorban elkészítünk egy általános modellt a vanília JS-ben. Ez lehetőséget ad számunkra annak ellenőrzésére, hogy mi az API tér vissza a komponensünkhöz. Lehetővé teszi a hiba forgatókönyvek tesztelését.

Most javítsuk tesztjeinket.

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

A tesztek most ellenőrzik, hogy a külső API-t elérték-e a megfelelő adatokkal az API-nk hívása során.

Hozzáadhatunk más teszteket is, amelyek ellenőrzik az API viselkedését a külső API válaszkódok alapján:

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

Természetesen rajtad múlik, hogy hogyan kezeld az alkalmazásodban harmadik féltől származó API-k hibáit. De érted a lényeget.

A tesztek futtatásához meg kell mondanunk a myapp tárolónak, hogy mi a harmadik fél szolgáltatásának alap URL-je:

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

Következtetés és még néhány gondolat

Remélhetőleg ez a cikk ízelítőt adott arról, hogy a Docker írása mit tehet érted az API-tesztelés során. A teljes példa itt található a GitHubon.

A Docker compose használatával a tesztek gyorsan futnak a gyártáshoz közeli környezetben. Nem igényel módosításokat a komponens kódjához. Az egyetlen követelmény a környezeti változók által vezérelt konfiguráció támogatása.

A példa összetevői logikája nagyon egyszerű, de az alapelvek minden API-ra érvényesek. A tesztek csak hosszabbak vagy összetettebbek lesznek. Ez vonatkozik minden olyan technikai veremre is, amelyet egy tartályba lehet helyezni (ez az összes). Ha pedig ott van, egy lépésre van attól, hogy szükség esetén a konténereit gyártásba helyezze.

Ha még nincsenek tesztjei, akkor azt javasoljuk, hogy kezdje el: a tesztelés végétől a végéig a Docker compose segítségével. Olyan egyszerű, hogy az első teszted néhány órán belül lefuthat. Ha kérdése van, vagy tanácsra van szüksége, forduljon hozzám nyugodtan. Szívesen segítek.

Remélem, tetszett neked ez a cikk, és elkezded tesztelni az API-kat a Docker Compose segítségével. Miután elkészült a teszt, futtathatja azokat a dobozból a folyamatos integrációs Fire CI platformunkon.

Egy utolsó ötlet az automatizált tesztelés sikeréhez.

Ami a nagy tesztcsomagokat illeti, a legfontosabb jellemző, hogy a tesztek könnyen olvashatók és érthetőek. Ez kulcsfontosságú a csapat motiválására a tesztek naprakészen tartására. A komplex tesztek keretrendszere hosszú távon aligha fog megfelelően használni.

Az API veremétől függetlenül érdemes megfontolni a chai / mokka használatát tesztek írásához. Szokatlannak tűnhet, ha különböző halmok vannak a futásidejű kódhoz és a tesztkódhoz, de ha elvégzi a munkát ... Amint az a cikk példáiból is kitűnik, a REST API tesztelése a chai / mokával olyan egyszerű, amilyen gyorsan csak . A tanulási görbe nulla közelében van.

Tehát ha egyáltalán nincs tesztje, és rendelkezik egy REST API-val, amelyet Java, Python, RoR, .NET vagy bármilyen más veremben írhat, fontolja meg a chai / mokka kipróbálását.

Ha kíváncsi arra, hogyan lehet egyáltalán kezdeni a folyamatos integrációt, írtam erről egy tágabb útmutatót. Itt van: Hogyan kezdhetjük el a folyamatos integrációt

Eredetileg a Fire CI blogon jelent meg.