Panduan lengkap untuk ujian API end-to-end dengan Docker

Ujian adalah kesakitan pada umumnya. Ada yang tidak faham maksudnya. Ada yang melihatnya tetapi menganggapnya sebagai langkah tambahan yang melambatkannya. Kadang-kadang ujian ada tetapi sangat lama untuk dijalankan atau tidak stabil. Dalam artikel ini, anda akan melihat bagaimana anda boleh membuat ujian sendiri dengan Docker.

Kami mahukan ujian yang cepat, bermakna dan boleh dipercayai ditulis dan dikekalkan dengan usaha yang minimum. Ini bermaksud ujian yang berguna untuk anda sebagai pembangun setiap hari. Mereka harus meningkatkan produktiviti anda dan meningkatkan kualiti perisian anda. Mempunyai ujian kerana semua orang mengatakan "anda semestinya mempunyai ujian" tidak baik jika ia melambatkan anda.

Mari kita lihat bagaimana mencapainya dengan tidak banyak usaha.

Contoh yang akan kita uji

Dalam artikel ini kita akan menguji API yang dibangun dengan Node / express dan menggunakan chai / mocha untuk pengujian. Saya telah memilih timbunan JS kerana kodnya sangat pendek dan senang dibaca. Prinsip-prinsip yang digunakan adalah sah untuk setiap timbunan teknologi. Terus membaca walaupun Javascript membuat anda sakit.

Contohnya akan merangkumi sekumpulan titik akhir CRUD yang mudah untuk pengguna. Lebih daripada cukup untuk memahami konsep dan menerapkan logik perniagaan API anda yang lebih kompleks.

Kami akan menggunakan persekitaran yang cukup standard untuk API:

  • Pangkalan data Postgres
  • Sekumpulan Redis
  • API kami akan menggunakan API luaran lain untuk menjalankan tugasnya

API anda mungkin memerlukan persekitaran yang berbeza. Prinsip yang diterapkan dalam artikel ini akan tetap sama. Anda akan menggunakan gambar asas Docker yang berbeza untuk menjalankan komponen apa sahaja yang mungkin anda perlukan.

Mengapa Docker? Dan sebenarnya Docker Compose

Bahagian ini mengandungi banyak hujah yang menyokong penggunaan Docker untuk ujian. Anda boleh melangkauinya jika anda ingin mendapatkan bahagian teknikal dengan segera.

Alternatif yang menyakitkan

Untuk menguji API anda dalam lingkungan produksi yang dekat, anda mempunyai dua pilihan. Anda boleh mengejek persekitaran pada tahap kod atau menjalankan ujian pada pelayan sebenar dengan pangkalan data dll dipasang.

Mengejek segala-galanya pada tahap kod menutup kod dan konfigurasi API kami. Ia juga sering kali tidak menggambarkan bagaimana API akan bertindak dalam pengeluaran. Menjalankan perkara dalam pelayan sebenar adalah berat infrastruktur. Ini banyak penyediaan dan penyelenggaraan, dan tidak mengikut skala. Mempunyai pangkalan data bersama, anda hanya boleh menjalankan 1 ujian pada satu masa untuk memastikan ujian berjalan tidak saling mengganggu.

Docker Compose membolehkan kita mendapatkan yang terbaik dari kedua-dua dunia. Ia mencipta versi "containerized" dari semua bahagian luaran yang kami gunakan. Ini mengejek tetapi di luar kod kami. API kami berpendapat bahawa ia berada dalam persekitaran fizikal yang sebenar. Docker compose juga akan membuat rangkaian terpencil untuk semua kontena untuk ujian dijalankan. Ini membolehkan anda menjalankan beberapa daripadanya secara selari pada komputer tempatan atau host CI anda.

Berlebihan?

Anda mungkin tertanya-tanya jika tidak berlebihan untuk melakukan ujian akhir ke akhir sama sekali dengan Docker mengarang. Bagaimana dengan menjalankan ujian unit sahaja?

Selama 10 tahun terakhir, aplikasi monolit besar telah dipecah menjadi perkhidmatan yang lebih kecil (cenderung ke arah "perkhidmatan mikro" yang sibuk). Komponen API yang diberikan bergantung pada lebih banyak bahagian luaran (infrastruktur atau API lain). Apabila perkhidmatan semakin kecil, integrasi dengan infrastruktur menjadi sebahagian besar pekerjaan.

Anda harus menyimpan jurang kecil antara pengeluaran dan persekitaran pengembangan anda. Jika tidak, masalah akan timbul semasa pengeluaran digunakan. Secara definisi masalah ini muncul pada saat yang paling teruk. Mereka akan membuat perbaikan cepat, penurunan kualiti, dan kekecewaan bagi pasukan. Tidak ada yang mahukan itu.

Anda mungkin tertanya-tanya apakah ujian akhir ke akhir dengan komposisi Docker berjalan lebih lama daripada ujian unit tradisional. Tidak juga. Anda akan melihat dalam contoh di bawah ini bahawa kita dapat menjalani ujian dengan mudah di bawah 1 minit, dan sangat menguntungkan: ujian tersebut menggambarkan tingkah laku aplikasi di dunia nyata. Ini lebih berharga daripada mengetahui sama ada kelas anda di suatu tempat di tengah-tengah aplikasi berfungsi dengan baik atau tidak.

Juga, jika anda tidak mempunyai ujian sekarang, bermula dari hujung hingga akhir memberi anda banyak faedah untuk usaha yang sedikit. Anda akan mengetahui semua timbunan aplikasi berfungsi bersama untuk senario yang paling biasa. Itu sudah menjadi sesuatu! Dari sana anda sentiasa dapat menyempurnakan strategi untuk menguji bahagian penting aplikasi anda.

Ujian pertama kami

Mari mulakan dengan bahagian paling mudah: API kami dan pangkalan data Postgres. Dan mari kita jalankan ujian CRUD yang mudah. Setelah kerangka itu disiapkan, kami dapat menambahkan lebih banyak ciri ke komponen dan ujian kami.

Inilah API minimum kami dengan GET / POST untuk membuat dan menyenaraikan pengguna:

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

Berikut adalah ujian kami yang ditulis dengan chai. Ujian membuat pengguna baru dan mendapatkannya kembali. Anda dapat melihat bahawa ujian tidak digabungkan dengan kod API kami. Yang SERVER_URLberubah-ubah menentukan titik akhir untuk menguji. Ia boleh menjadi persekitaran tempatan atau terpencil.

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

Baik. Sekarang untuk menguji API kami, mari tentukan persekitaran menulis Docker. Fail yang dipanggil docker-compose.ymlakan menggambarkan bekas yang perlu dijalankan oleh Docker.

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

Jadi apa yang kita ada di sini. Terdapat 3 bekas:

  • db memaparkan contoh baru PostgreSQL. Kami menggunakan gambar Postgres awam dari Docker Hub. Kami menetapkan nama pengguna dan kata laluan pangkalan data. Kami memberitahu Docker untuk mendedahkan port 5432 yang akan didengarkan oleh pangkalan data sehingga bekas lain dapat disambungkan
  • myapp adalah wadah yang akan menjalankan API kami. The buildarahan memberitahu Docker untuk benar-benar membina imej bekas dari sumber kami. Selebihnya seperti bekas db: pemboleh ubah persekitaran dan port
  • myapp-tes adalah wadah yang akan melaksanakan ujian kami. Ia akan menggunakan gambar yang sama dengan myapp kerana kodnya sudah ada sehingga tidak perlu membuatnya lagi. Perintah yang node db/init.js && yarn testdijalankan pada wadah akan menginisialisasi pangkalan data (membuat jadual dll) dan menjalankan ujian. Kami menggunakan dockerize untuk menunggu semua pelayan yang diperlukan dapat beroperasi dan berjalan. The depends_onpilihan akan memastikan bahawa bekas bermula dalam susunan tertentu. Ini tidak memastikan bahawa pangkalan data di dalam wadah db sebenarnya siap menerima sambungan. Tidak juga pelayan API kami sudah siap.

Definisi persekitaran adalah seperti 20 baris kod yang sangat mudah difahami. Satu-satunya bahagian yang bernas adalah definisi persekitaran. Nama pengguna, kata laluan dan URL mesti konsisten supaya kontena benar-benar dapat berfungsi bersama.

Satu perkara yang perlu diperhatikan adalah bahawa komposisi Docker akan menetapkan hos bekas yang dibuatnya ke nama bekas. Jadi pangkalan data tidak akan tersedia di bawah localhost:5432tetapi db:5432. Dengan cara yang sama API kami akan disajikan di bawah myapp:8000. Tidak ada localhost di sini.

Ini bermaksud bahawa API anda mesti menyokong pemboleh ubah persekitaran ketika datang ke definisi persekitaran. Tiada barang keras. Tetapi itu tidak ada kaitan dengan Docker atau artikel ini. Aplikasi yang boleh dikonfigurasikan adalah titik 3 dari manifesto aplikasi 12 faktor, jadi anda semestinya sudah melakukannya.

Perkara terakhir yang perlu kita maklumkan kepada Docker adalah bagaimana sebenarnya untuk membina wadah myapp . Kami menggunakan Dockerfile seperti di bawah. Kandungannya khusus untuk tumpukan teknologi anda tetapi idenya adalah untuk menggabungkan API anda menjadi pelayan yang dapat dijalankan.

Contoh di bawah untuk pemasangan Node API kami Dockerize, memasang kebergantungan API dan menyalin kod API di dalam bekas (pelayan ditulis dalam JS mentah jadi tidak perlu menyusunnya).

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

Biasanya dari baris WORKDIR ~/appdan bawah anda akan menjalankan perintah yang akan membina aplikasi anda.

Dan inilah arahan yang kami gunakan untuk menjalankan ujian:

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

Perintah ini akan memberitahu Docker menyusun untuk memisahkan komponen yang ditentukan dalam docker-compose.ymlfail kami . The --buildbendera akan mencetuskan membina bekas myapp dengan melaksanakan kandungan yang Dockerfiledi atas. Surat --abort-on-container-exitwasiat akan memberitahu Docker menulis untuk mematikan persekitaran sebaik sahaja satu kontena keluar.

Itu berfungsi dengan baik kerana satu-satunya komponen yang dimaksudkan untuk keluar adalah wadah ujian myapp- setelah ujian dijalankan. Cherry on the cake, docker-composeperintah akan keluar dengan kod keluar yang sama dengan wadah yang memicu keluar. Ini bermaksud bahawa kita dapat memeriksa sama ada ujian berjaya atau tidak dari baris perintah. Ini sangat berguna untuk binaan automatik dalam persekitaran CI.

Bukankah itu persediaan ujian yang sempurna?

Contoh penuh terdapat di GitHub. Anda boleh mengklon repositori dan menjalankan perintah mengarang docker:

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

Sudah tentu anda memerlukan Docker dipasang. Docker mempunyai kecenderungan yang menyusahkan memaksa anda untuk mendaftar akaun hanya untuk memuat turun perkara itu. Tetapi anda sebenarnya tidak perlu. Pergi ke nota pelepasan (pautan untuk Windows dan pautan untuk Mac) dan muat turun bukan versi terkini tetapi yang sebelumnya. Ini adalah pautan muat turun langsung.

Ujian pertama akan lebih lama daripada biasa. Ini kerana Docker perlu memuat turun gambar asas untuk bekas anda dan menyimpan beberapa perkara. Larian seterusnya akan menjadi lebih pantas.

Log dari larian akan kelihatan seperti di bawah. Anda dapat melihat bahawa Docker cukup sejuk untuk meletakkan log dari semua komponen pada garis masa yang sama. Ini sangat berguna semasa mencari kesilapan.

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

Kita dapat melihat bahawa db adalah bekas yang memulakan paling lama. Masuk akal. Setelah selesai ujian akan dimulakan. Jumlah masa penggunaan komputer riba saya ialah 16 saat. Berbanding dengan 880ms yang digunakan untuk benar-benar melaksanakan ujian, itu banyak. Dalam praktiknya, ujian yang berlangsung di bawah 1 minit adalah emas kerana ia adalah maklum balas segera. Overhead 15'ish saat adalah masa beli yang akan berterusan apabila anda menambahkan lebih banyak ujian. Anda boleh menambahkan beratus ujian dan masih mengekalkan masa pelaksanaan di bawah 1 minit.

Voilà! Kami mempunyai kerangka ujian kami dan berjalan. Dalam projek dunia nyata, langkah seterusnya adalah untuk meningkatkan liputan fungsi API anda dengan lebih banyak ujian. Mari pertimbangkan operasi CRUD yang dilindungi. Sudah tiba masanya untuk menambahkan lebih banyak elemen ke persekitaran ujian kami.

Menambah kluster Redis

Mari tambahkan elemen lain ke persekitaran API kami untuk memahami apa yang diperlukan. Makluman spoiler: tidak banyak.

Mari kita bayangkan bahawa API kami menyimpan sesi pengguna dalam kluster Redis. Sekiranya anda tertanya-tanya mengapa kami melakukannya, bayangkan 100 contoh API anda dalam pengeluaran. Pengguna memukul satu atau pelayan lain berdasarkan pengimbangan beban round robin. Setiap permintaan perlu disahkan.

Ini memerlukan data profil pengguna untuk memeriksa keistimewaan dan logik perniagaan khusus aplikasi lain. Salah satu cara untuk pergi adalah dengan melakukan perjalanan pergi balik ke pangkalan data untuk mengambil data setiap kali anda memerlukannya, tetapi itu tidak begitu efisien. Menggunakan kluster pangkalan data memori menjadikan data tersedia di semua pelayan untuk kos pembacaan pembolehubah tempatan.

Ini adalah bagaimana anda meningkatkan persekitaran ujian menyusun Docker anda dengan perkhidmatan tambahan. Mari tambahkan kluster Redis dari gambar rasmi Docker (saya hanya menyimpan bahagian baru fail):

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

Anda dapat melihat ia tidak banyak. Kami menambah bekas baru yang dipanggil redis . Ia menggunakan gambar rasmi redis minimum yang dipanggil redis:alpine. Kami menambahkan konfigurasi host dan port Redis ke wadah API kami. Dan kami telah membuat ujian menantinya serta bekas lain sebelum menjalankan ujian.

Mari ubah aplikasi kami untuk benar-benar menggunakan kluster 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); } });

Mari ubah ujian kami sekarang untuk memastikan bahawa kluster Redis diisi dengan data yang betul. Itulah sebabnya wadah myapp-tes juga mendapat konfigurasi host dan port 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(); }); }); });

Lihat betapa mudahnya ini. Anda boleh membina persekitaran yang kompleks untuk ujian anda seperti memasang bata Lego.

Kita dapat melihat satu lagi faedah dari pengujian persekitaran penuh seperti ini. Ujian sebenarnya dapat melihat komponen persekitaran. Ujian kami bukan sahaja dapat memeriksa bahawa API kami mengembalikan kod dan data respons yang betul. Kita juga dapat memeriksa bahawa data dalam kluster Redis memiliki nilai yang tepat. Kami juga dapat memeriksa kandungan pangkalan data.

Menambah ejekan API

Elemen umum untuk komponen API adalah memanggil komponen API lain.

Katakanlah API kami perlu memeriksa e-mel pengguna spam semasa membuat pengguna. Pemeriksaan dilakukan menggunakan perkhidmatan pihak ketiga:

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

Sekarang kita mempunyai masalah untuk menguji apa sahaja. Kami tidak dapat membuat pengguna jika API untuk mengesan e-mel spam tidak tersedia. Mengubah API kami untuk memintas langkah ini dalam mod ujian adalah kekacauan kod yang berbahaya.

Walaupun kita dapat menggunakan perkhidmatan pihak ketiga yang sebenarnya, kita tidak mahu melakukannya. Sebagai peraturan umum ujian kami tidak boleh bergantung pada infrastruktur luaran. Pertama sekali, kerana anda mungkin akan menjalankan ujian anda sebagai sebahagian daripada proses CI anda. Tidak semudah itu untuk menggunakan API pengeluaran lain untuk tujuan ini. Kedua dari semua API mungkin dimatikan sementara, gagal ujian anda dengan alasan yang salah.

Penyelesaian yang tepat adalah mengejek API luaran dalam ujian kami.

Tidak memerlukan kerangka kerja mewah. Kami akan membina tiruan generik dalam vanilla JS dalam ~ 20 baris kod. Ini akan memberi kita peluang untuk mengawal apa yang API akan kembali ke komponen kita. Ia memungkinkan untuk menguji senario ralat.

Sekarang mari kita meningkatkan ujian kami.

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

Ujian sekarang memeriksa bahawa API luaran telah terkena data yang sesuai semasa panggilan ke API kami.

Kami juga dapat menambahkan ujian lain yang memeriksa bagaimana API kami berkelakuan berdasarkan kod respons API luaran:

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

Cara anda menangani kesilapan dari API pihak ketiga dalam aplikasi anda tentu saja bergantung kepada anda. Tetapi anda mendapat maksudnya.

Untuk menjalankan ujian ini, kami perlu memberitahu myapp kontena apakah URL asas perkhidmatan pihak ketiga:

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

Kesimpulan dan beberapa pemikiran lain

Semoga artikel ini memberi anda gambaran tentang apa yang dapat dikarang oleh Docker untuk anda ketika menjalani ujian API. Contoh penuh terdapat di GitHub.

Menggunakan komposisi Docker menjadikan ujian berjalan pantas di persekitaran yang hampir dengan pengeluaran. Ia tidak memerlukan penyesuaian pada kod komponen anda. Satu-satunya syarat adalah untuk menyokong konfigurasi yang didorong oleh pemboleh ubah persekitaran.

Logik komponen dalam contoh ini sangat mudah tetapi prinsipnya berlaku untuk API apa pun. Ujian anda akan lebih lama atau lebih rumit. Mereka juga berlaku untuk setiap timbunan teknologi yang boleh dimasukkan ke dalam bekas (itu semua). Dan setelah anda berada di sana, anda selangkah lagi untuk menyebarkan bekas anda ke pengeluaran jika perlu.

Sekiranya anda tidak mempunyai ujian sekarang, ini adalah bagaimana saya mengesyorkan anda memulakan: ujian akhir ke akhir dengan mengarang Docker. Sangat mudah anda dapat menjalankan ujian pertama anda dalam beberapa jam. Jangan ragu untuk menghubungi saya jika anda mempunyai pertanyaan atau memerlukan nasihat. Saya dengan senang hati akan membantu.

Saya harap anda menikmati artikel ini dan akan mula menguji API anda dengan Docker Compose. Setelah ujian siap, anda boleh menjalankannya di luar kotak di platform integrasi berterusan Fire CI kami.

Satu idea terakhir untuk berjaya dengan ujian automatik.

Untuk mengekalkan suite ujian yang besar, ciri yang paling penting ialah ujian mudah dibaca dan difahami. Ini adalah kunci untuk memotivasi pasukan anda untuk memastikan ujian terkini. Kerangka ujian kompleks tidak mungkin digunakan dengan betul dalam jangka masa panjang.

Terlepas dari tumpukan untuk API anda, anda mungkin ingin mempertimbangkan untuk menggunakan chai / mocha untuk menulis ujian untuknya. Nampaknya tidak biasa mempunyai tumpukan yang berbeza untuk kod runtime dan kod ujian, tetapi jika ia berjaya dilakukan ... Seperti yang anda lihat dari contoh dalam artikel ini, menguji REST API dengan chai / mocha semudah mendapat . Keluk pembelajaran hampir dengan sifar.

Oleh itu, jika anda tidak mempunyai ujian sama sekali dan mempunyai API REST untuk ujian yang ditulis dalam Java, Python, RoR, .NET atau apa sahaja timbunan lain, anda mungkin boleh mencuba chai / mocha.

Sekiranya anda tertanya-tanya bagaimana untuk memulakan dengan integrasi berterusan, saya telah menulis panduan yang lebih luas mengenainya. Inilah dia: Bagaimana memulakan dengan Integrasi Berterusan

Asalnya diterbitkan di Fire CI Blog.