دليل كامل لاختبار API من طرف إلى طرف باستخدام Docker

الاختبار هو الألم بشكل عام. البعض لا يرى الهدف. يراها البعض ولكن يعتقد أنها خطوة إضافية تبطئهم. في بعض الأحيان تكون الاختبارات موجودة ولكنها طويلة جدًا للتشغيل أو غير مستقرة. سترى في هذه المقالة كيف يمكنك هندسة الاختبارات بنفسك باستخدام Docker.

نريد اختبارات سريعة وذات مغزى وموثوق بها مكتوبة ومحفوظة بأقل جهد. إنه يعني الاختبارات التي تفيدك كمطور على أساس يومي. يجب أن تعزز إنتاجيتك وتحسن جودة برنامجك. إجراء الاختبارات لأن الجميع يقولون "يجب أن تجري الاختبارات" ليس جيدًا إذا كان يبطئ من أدائك.

دعونا نرى كيف نحقق ذلك دون بذل الكثير من الجهد.

المثال الذي سنختبره

في هذه المقالة ، سنختبر واجهة برمجة تطبيقات مبنية باستخدام Node / Express ونستخدم chai / mocha للاختبار. لقد اخترت مكدس JS'y لأن الشفرة قصيرة جدًا وسهلة القراءة. المبادئ المطبقة صالحة لأي مكدس تقني. استمر في القراءة حتى لو أصابتك جافا سكريبت بالمرض.

سيغطي المثال مجموعة بسيطة من نقاط نهاية CRUD للمستخدمين. يكفي فهم المفهوم وتطبيقه على منطق الأعمال الأكثر تعقيدًا لواجهة برمجة التطبيقات الخاصة بك.

سنستخدم بيئة قياسية جدًا لواجهة برمجة التطبيقات:

  • قاعدة بيانات Postgres
  • مجموعة Redis
  • ستستخدم API الخاصة بنا واجهات برمجة التطبيقات الخارجية الأخرى للقيام بعملها

قد تحتاج API الخاصة بك إلى بيئة مختلفة. ستبقى المبادئ المطبقة في هذه المقالة كما هي. ستستخدم صور قاعدة Docker مختلفة لتشغيل أي مكون قد تحتاجه.

لماذا Docker؟ وفي الحقيقة Docker Compose

يحتوي هذا القسم على الكثير من الحجج لصالح استخدام Docker للاختبار. يمكنك تخطيه إذا كنت تريد الوصول إلى الجزء الفني على الفور.

البدائل المؤلمة

لاختبار API الخاص بك في مكان قريب من بيئة الإنتاج لديك خياران. يمكنك الاستهزاء بالبيئة على مستوى الكود أو تشغيل الاختبارات على خادم حقيقي مع تثبيت قاعدة البيانات وما إلى ذلك.

الاستهزاء بكل شيء على مستوى الكود يفسد كود وتكوين API الخاص بنا. كما أنه غالبًا لا يمثل كثيرًا كيف ستتصرف API في الإنتاج. تشغيل الشيء في خادم حقيقي هو بنية تحتية ثقيلة. هناك الكثير من عمليات الإعداد والصيانة ، ولا تتسع. بوجود قاعدة بيانات مشتركة ، يمكنك تشغيل اختبار واحد فقط في كل مرة للتأكد من أن عمليات التشغيل الاختبارية لا تتداخل مع بعضها البعض.

يتيح لنا Docker Compose الحصول على أفضل ما في العالمين. يقوم بإنشاء إصدارات "حاوية" لجميع الأجزاء الخارجية التي نستخدمها. إنه استهزاء ولكن من خارج كودنا. يعتقد API الخاص بنا أنه في بيئة مادية حقيقية. سيؤدي إنشاء Docker أيضًا إلى إنشاء شبكة معزولة لجميع الحاويات لإجراء اختبار معين. يتيح لك ذلك تشغيل العديد منها بالتوازي على جهاز الكمبيوتر المحلي أو مضيف CI.

مبالغة؟

قد تتساءل عما إذا لم يكن من المبالغة إجراء اختبارات من البداية إلى النهاية على الإطلاق باستخدام Docker. ماذا عن مجرد إجراء اختبارات الوحدة بدلاً من ذلك؟

على مدى السنوات العشر الماضية ، تم تقسيم التطبيقات الضخمة إلى خدمات أصغر (تتجه نحو "الخدمات الصغيرة" الصاخبة). يعتمد مكون API المحدد على المزيد من الأجزاء الخارجية (البنية التحتية أو واجهات برمجة التطبيقات الأخرى). مع تقلص حجم الخدمات ، يصبح التكامل مع البنية التحتية جزءًا أكبر من الوظيفة.

يجب أن تحافظ على فجوة صغيرة بين الإنتاج وبيئات التطوير الخاصة بك. خلاف ذلك ، ستظهر المشاكل عند الذهاب لنشر الإنتاج. بالتعريف تظهر هذه المشاكل في أسوأ لحظة ممكنة. سيؤدي ذلك إلى إصلاحات مستعجلة ، وانخفاض في الجودة ، وإحباط للفريق. لا أحد يريد ذلك.

قد تتساءل عما إذا كانت الاختبارات النهائية باستخدام Docker تعمل لفترة أطول من اختبارات الوحدة التقليدية. ليس صحيحا. سترى في المثال أدناه أنه يمكننا بسهولة إبقاء الاختبارات أقل من دقيقة واحدة ، وبفائدة كبيرة: تعكس الاختبارات سلوك التطبيق في العالم الحقيقي. هذا أكثر قيمة من معرفة ما إذا كان فصلك في مكان ما في منتصف التطبيق يعمل بشكل جيد أم لا.

أيضًا ، إذا لم يكن لديك أي اختبارات في الوقت الحالي ، فإن البدء من البداية إلى النهاية يمنحك فوائد رائعة مقابل القليل من الجهد. ستعرف أن جميع حزم التطبيق تعمل معًا للسيناريوهات الأكثر شيوعًا. هذا بالفعل شيء! من هناك ، يمكنك دائمًا تحسين إستراتيجية لاختبار الوحدة لأجزاء مهمة من تطبيقك.

أول اختبار لنا

لنبدأ بالجزء الأسهل: API وقاعدة بيانات Postgres. ودعنا نجري اختبار CRUD بسيطًا. بمجرد أن يكون لدينا إطار العمل هذا ، يمكننا إضافة المزيد من الميزات لكل من المكون الخاص بنا والاختبار.

هنا هو الحد الأدنى من واجهة برمجة التطبيقات لدينا مع GET / POST لإنشاء وقائمة المستخدمين:

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

ها هي اختباراتنا مكتوبة مع شاي. الاختبارات تنشئ مستخدمًا جديدًا وتجلبه مرة أخرى. يمكنك أن ترى أن الاختبارات لا تقترن بأي شكل من الأشكال برمز API الخاص بنا. و SERVER_URLيحدد متغير نقطة النهاية لاختبار. يمكن أن تكون بيئة محلية أو بعيدة.

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

حسن. الآن لاختبار API الخاص بنا ، دعونا نحدد بيئة إنشاء Docker. docker-compose.ymlسيصف ملف يسمى الحاويات التي يحتاجها 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

إذن ماذا لدينا هنا. يوجد 3 حاويات:

  • يقوم db بتدوير نسخة جديدة من PostgreSQL. نستخدم صورة Postgres العامة من Docker Hub. قمنا بتعيين اسم المستخدم وكلمة المرور لقاعدة البيانات. نطلب من Docker الكشف عن المنفذ 5432 الذي ستستمع إليه قاعدة البيانات حتى تتمكن الحاويات الأخرى من الاتصال
  • myapp هو الحاوية التي ستقوم بتشغيل API الخاص بنا. و buildيقول قيادة عامل الميناء لبناء الواقع صورة حاوية من مصدرنا. الباقي مثل حاوية db: متغيرات البيئة والمنافذ
  • اختبارات myapp هي الحاوية التي ستنفذ اختباراتنا. سيستخدم نفس صورة myapp لأن الكود سيكون موجودًا بالفعل ، لذا لا داعي لبنائه مرة أخرى. سيقوم الأمر الذي يتم node db/init.js && yarn testتشغيله في الحاوية بتهيئة قاعدة البيانات (إنشاء جداول وما إلى ذلك) وتشغيل الاختبارات. نحن نستخدم dockerize لانتظار تشغيل جميع الخوادم المطلوبة. و depends_onسوف خيارات تضمن أن الحاويات تبدأ في ترتيب معين. لا يضمن أن قاعدة البيانات الموجودة داخل حاوية db جاهزة بالفعل لقبول الاتصالات. ولا أن خادم API الخاص بنا يعمل بالفعل.

تعريف البيئة يشبه 20 سطرًا من التعليمات البرمجية سهلة الفهم للغاية. الجزء الذهني الوحيد هو تعريف البيئة. يجب أن تكون أسماء المستخدمين وكلمات المرور وعناوين URL متسقة حتى يمكن للحاويات العمل معًا.

شيء واحد يجب ملاحظته هو أن إنشاء Docker سيعين مضيف الحاويات التي ينشئها على اسم الحاوية. لذلك لن تكون قاعدة البيانات متاحة تحت localhost:5432ولكن db:5432. بنفس الطريقة التي سيتم بها تقديم API الخاص بنا myapp:8000. لا يوجد مضيف محلي من أي نوع هنا.

هذا يعني أن API الخاص بك يجب أن يدعم متغيرات البيئة عندما يتعلق الأمر بتعريف البيئة. لا توجد أشياء مضمنة. لكن هذا لا علاقة له بـ Docker أو هذه المقالة. التطبيق القابل للتكوين هو النقطة 3 من بيان تطبيق 12 عاملاً ، لذا يجب أن تفعل ذلك بالفعل.

آخر شيء نحتاج إلى إخبار Docker به هو كيفية إنشاء الحاوية myapp . نحن نستخدم Dockerfile كما هو موضح أدناه. المحتوى خاص بمكدس التكنولوجيا الخاص بك ولكن الفكرة هي تجميع واجهة برمجة التطبيقات الخاصة بك في خادم قابل للتشغيل.

المثال أدناه الخاص بنا Node API يقوم بتثبيت Dockerize ، ويقوم بتثبيت تبعيات API ونسخ رمز API داخل الحاوية (الخادم مكتوب في Raw JS لذلك لا حاجة إلى تجميعه).

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

عادةً من السطر WORKDIR ~/appوأدناه ، يمكنك تشغيل الأوامر التي من شأنها بناء تطبيقك.

وإليك الأمر الذي نستخدمه لإجراء الاختبارات:

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

سيخبر هذا الأمر Docker بتكوينه لتدوير المكونات المحددة في docker-compose.ymlملفنا. و --buildسوف العلم تؤدي إلى بناء حاوية اسم التطبيق من خلال تنفيذ مضمون Dockerfileأعلاه. و --abort-on-container-exitسوف اقول عامل الميناء يؤلف لإيقاف البيئة في أقرب وقت مخارج حاوية واحدة.

يعمل هذا بشكل جيد لأن المكون الوحيد المقصود بالخروج هو حاوية الاختبار myapp-tests بعد تنفيذ الاختبارات. الكرز على الكعكة ، سيخرج docker-composeالأمر بنفس كود الخروج مثل الحاوية التي أدت إلى الخروج. هذا يعني أنه يمكننا التحقق مما إذا كانت الاختبارات قد نجحت أم لا من سطر الأوامر. هذا مفيد جدًا للبنيات المؤتمتة في بيئة CI.

أليس هذا هو الإعداد المثالي للاختبار؟

المثال الكامل موجود هنا على GitHub. يمكنك استنساخ المستودع وتشغيل أمر docker compose:

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

بالطبع أنت بحاجة إلى تثبيت Docker. لدى Docker نزعة مزعجة تتمثل في إجبارك على التسجيل للحصول على حساب فقط لتنزيل الشيء. لكنك في الواقع لست مضطرًا لذلك. انتقل إلى ملاحظات الإصدار (رابط لنظام Windows ورابط لنظام Mac) وقم بتنزيل ليس أحدث إصدار ولكن الإصدار السابق مباشرةً. هذا رابط تنزيل مباشر.

ستكون الجولة الأولى للاختبارات أطول من المعتاد. هذا لأن Docker سيتعين عليه تنزيل الصور الأساسية لحاوياتك وذاكرة التخزين المؤقت لبعض الأشياء. ستكون الأشواط القادمة أسرع بكثير.

سوف تبدو السجلات من المدى على النحو التالي. يمكنك أن ترى أن Docker رائع بما يكفي لوضع سجلات من جميع المكونات على نفس المخطط الزمني. هذا مفيد جدًا عند البحث عن الأخطاء.

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

يمكننا أن نرى أن db هي الحاوية التي تهيئ الأطول. من المنطقي. بمجرد الانتهاء ، تبدأ الاختبارات. إجمالي وقت التشغيل على جهاز الكمبيوتر المحمول الخاص بي هو 16 ثانية. بالمقارنة مع 880 مللي ثانية المستخدمة في تنفيذ الاختبارات بالفعل ، فهي كثيرة. من الناحية العملية ، الاختبارات التي تقل عن دقيقة واحدة تعتبر من ذهب لأنها تقريبًا ردود فعل فورية. تعتبر الثواني الـ 15 الإضافية بمثابة شراء للوقت الذي سيكون ثابتًا كلما أضفت المزيد من الاختبارات. يمكنك إضافة مئات الاختبارات مع الاحتفاظ بوقت التنفيذ أقل من دقيقة واحدة.

هاهو! لدينا إطار الاختبار الخاص بنا وتشغيله. في مشروع العالم الحقيقي ، ستكون الخطوات التالية هي تعزيز التغطية الوظيفية لواجهة برمجة التطبيقات الخاصة بك بمزيد من الاختبارات. دعونا ننظر في تغطية عمليات CRUD. حان الوقت لإضافة المزيد من العناصر إلى بيئة الاختبار الخاصة بنا.

إضافة كتلة Redis

دعونا نضيف عنصرًا آخر إلى بيئة API الخاصة بنا لفهم ما يتطلبه الأمر. تنبيه المفسد: ليس كثيرًا.

دعونا نتخيل أن واجهة برمجة التطبيقات الخاصة بنا تحافظ على جلسات المستخدم في مجموعة Redis. إذا كنت تتساءل عن سبب قيامنا بذلك ، فتخيل 100 مثيل لواجهة برمجة التطبيقات الخاصة بك في الإنتاج. يضرب المستخدمون خادمًا أو خادمًا آخر بناءً على موازنة تحميل robin. كل طلب يحتاج إلى مصادقة.

يتطلب هذا بيانات ملف تعريف المستخدم للتحقق من الامتيازات ومنطق عمل آخر محدد للتطبيق. إحدى الطرق للذهاب هي القيام برحلة ذهابًا وإيابًا إلى قاعدة البيانات لجلب البيانات في كل مرة تحتاج إليها ، ولكن هذا ليس فعالًا للغاية. يؤدي استخدام مجموعة قاعدة بيانات في الذاكرة إلى إتاحة البيانات عبر جميع الخوادم مقابل تكلفة قراءة متغير محلي.

هذه هي الطريقة التي يمكنك بها تحسين بيئة اختبار إنشاء Docker الخاصة بك من خلال خدمة إضافية. دعنا نضيف مجموعة Redis من صورة Docker الرسمية (احتفظت فقط بالأجزاء الجديدة من الملف):

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

يمكنك أن ترى أنه ليس كثيرًا. أضفنا حاوية جديدة تسمى redis . يستخدم الحد الأدنى من صورة redis الرسمية المسماة redis:alpine. أضفنا Redis host and port configuration إلى حاوية API الخاصة بنا. وقد أجرينا الاختبارات انتظارًا لها وكذلك الحاويات الأخرى قبل إجراء الاختبارات.

دعنا نعدل تطبيقنا لاستخدام مجموعة 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); } });

دعنا الآن نغير اختباراتنا للتحقق من أن مجموعة Redis مليئة بالبيانات الصحيحة. لهذا السبب تحصل حاوية اختبارات myapp أيضًا على مضيف 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(); }); }); });

انظر كيف كان هذا سهلا. يمكنك بناء بيئة معقدة لاختباراتك مثل تجميع مكعبات الليغو.

يمكننا أن نرى فائدة أخرى لهذا النوع من اختبارات البيئة الكاملة بالحاويات. يمكن أن تبحث الاختبارات في الواقع في مكونات البيئة. لا يمكن لاختباراتنا فقط التحقق من أن API الخاص بنا يقوم بإرجاع رموز وبيانات الاستجابة المناسبة. يمكننا أيضًا التحقق من أن البيانات الموجودة في مجموعة Redis تحتوي على القيم المناسبة. يمكننا أيضًا التحقق من محتوى قاعدة البيانات.

مضيفا API mocks

أحد العناصر الشائعة لمكونات API هو استدعاء مكونات API الأخرى.

لنفترض أن واجهة برمجة التطبيقات الخاصة بنا تحتاج إلى التحقق من رسائل البريد الإلكتروني العشوائية للمستخدم عند إنشاء مستخدم. يتم الفحص باستخدام خدمة جهة خارجية:

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

الآن لدينا مشكلة في اختبار أي شيء. لا يمكننا إنشاء أي مستخدمين إذا كانت واجهة برمجة التطبيقات لاكتشاف رسائل البريد الإلكتروني العشوائية غير متوفرة. يعد تعديل واجهة برمجة التطبيقات الخاصة بنا لتجاوز هذه الخطوة في وضع الاختبار تشويشًا خطيرًا للشفرة.

حتى لو تمكنا من استخدام خدمة الطرف الثالث الحقيقية ، فإننا لا نريد القيام بذلك. كقاعدة عامة ، يجب ألا تعتمد اختباراتنا على البنية التحتية الخارجية. بادئ ذي بدء ، لأنك على الأرجح ستجري اختباراتك كثيرًا كجزء من عملية CI. ليس من الرائع استخدام واجهة برمجة تطبيقات إنتاج أخرى لهذا الغرض. ثانيًا ، قد تكون واجهة برمجة التطبيقات معطلة مؤقتًا ، وتفشل في اختباراتك لأسباب خاطئة.

الحل الصحيح هو الاستهزاء بواجهات برمجة التطبيقات الخارجية في اختباراتنا.

لا حاجة لأي إطار خيالي. سنقوم ببناء محاكاة عامة في Vanilla JS في حوالي 20 سطرًا من التعليمات البرمجية. سيعطينا هذا فرصة للتحكم في ما ستعود إليه API إلى مكوننا. يسمح باختبار سيناريوهات الخطأ.

الآن دعنا نعزز اختباراتنا.

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

تتحقق الاختبارات الآن من أن واجهة برمجة التطبيقات الخارجية قد تعرضت للبيانات المناسبة أثناء الاتصال بواجهة برمجة التطبيقات الخاصة بنا.

يمكننا أيضًا إضافة اختبارات أخرى تتحقق من سلوك واجهة برمجة التطبيقات الخاصة بنا بناءً على أكواد استجابة 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(); }); }); });

إن كيفية معالجة الأخطاء من واجهات برمجة التطبيقات التابعة لجهات خارجية في تطبيقك أمر متروك لك بالطبع. لكنك حصلت على النقطة.

لإجراء هذه الاختبارات ، نحتاج إلى إخبار الحاوية myapp بعنوان URL الأساسي لخدمة الطرف الثالث:

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

الخلاصة وبعض الأفكار الأخرى

نأمل أن تعطيك هذه المقالة لمحة عن ما يمكن أن يفعله Docker من أجلك عندما يتعلق الأمر باختبار API. المثال الكامل موجود هنا على GitHub.

يؤدي استخدام إنشاء Docker إلى إجراء الاختبارات بسرعة في بيئة قريبة من الإنتاج. لا يتطلب أي تعديلات على كود المكون الخاص بك. المطلب الوحيد هو دعم التكوين المدفوع بمتغيرات البيئة.

منطق المكون في هذا المثال بسيط للغاية ولكن المبادئ تنطبق على أي API. ستكون اختباراتك أطول أو أكثر تعقيدًا. تنطبق أيضًا على أي مكدس تقني يمكن وضعه داخل حاوية (هذا كلهم). وبمجرد وصولك إلى هناك ، تكون على بعد خطوة واحدة من نشر حاوياتك في الإنتاج إذا لزم الأمر.

إذا لم يكن لديك اختبارات الآن ، فهذه هي الطريقة التي أوصيك بها أن تبدأ: اختبار من النهاية إلى النهاية باستخدام Docker. الأمر بسيط للغاية بحيث يمكنك إجراء الاختبار الأول لك في غضون ساعات قليلة. لا تتردد في التواصل معي إذا كانت لديك أسئلة أو كنت بحاجة إلى نصيحة. يسعدني تقديم المساعدة.

أتمنى أن تكون قد استمتعت بهذه المقالة وأن تبدأ في اختبار واجهات برمجة التطبيقات الخاصة بك باستخدام Docker Compose. بمجرد أن تصبح الاختبارات جاهزة ، يمكنك إجراؤها خارج الصندوق على منصة التكامل المستمر Fire CI.

فكرة أخيرة للنجاح مع الاختبار الآلي.

عندما يتعلق الأمر بالحفاظ على مجموعات اختبار كبيرة ، فإن أهم ميزة هي أن الاختبارات سهلة القراءة والفهم. هذا هو المفتاح لتحفيز فريقك على تحديث الاختبارات باستمرار. من غير المحتمل أن يتم استخدام أطر الاختبارات المعقدة بشكل صحيح على المدى الطويل.

بغض النظر عن المكدس الخاص بواجهة برمجة التطبيقات الخاصة بك ، قد ترغب في التفكير في استخدام chai / mocha لكتابة اختبارات لها. قد يبدو من غير المعتاد وجود أكوام مختلفة لرمز وقت التشغيل ورمز الاختبار ، ولكن إذا تم إنجاز المهمة ... كما ترى من الأمثلة في هذه المقالة ، فإن اختبار واجهة برمجة تطبيقات REST باستخدام chai / mocha أمر بسيط بقدر ما . منحنى التعلم قريب من الصفر.

لذلك إذا لم تكن لديك اختبارات على الإطلاق ولديك واجهة برمجة تطبيقات REST لاختبارها مكتوبة بلغة Java أو Python أو RoR أو .NET أو أي مكدس آخر ، فقد تفكر في تجربة chai / mocha.

إذا كنت تتساءل عن كيفية البدء بالتكامل المستمر على الإطلاق ، فقد كتبت دليلًا أوسع حول هذا الموضوع. ها هو: كيف تبدأ مع التكامل المستمر

نُشر في الأصل على مدونة Fire CI.