SSD Nodes Learn 🎉 VPS $5.50/মাস থেকে
নির্দেশিকা Matt Connorদ্বারা Matt Connor

VPS-এ self-hosted API mocking ও testing কীভাবে করবেন

Mock server ও API test runner-এর কাজ আলাদা। VPS-এ WireMock stub Git-এ রাখুন, CI-তে Hurl suite চালান, এবং rebuild-এর পরও টিকে থাকা report সংরক্ষণ করুন।

একটি repository ভাগ করা দুটি কাজ

Self-hosted API mocking এবং testing দুটি আলাদা কাজ। এগুলোকে এক কাজ হিসেবে ধরলে এক সপ্তাহের শ্রম নষ্ট হয়। Mock server এমন কোনো dependency-এর বিকল্প হিসেবে কাজ করে, যাকে CI থেকে কল করা যায় না: payment provider, partner API, rate limited upstream, অথবা অন্য team এখনও release করেনি এমন service। API test runner আপনার নিজের endpoint-গুলো নির্দিষ্ট ক্রমে কল করে এবং response-এর ওপর assertion চালায়। এটি একটি response থেকে পাওয়া value পরবর্তী request-এ ব্যবহার করে।

দুটির কাজের ক্ষেত্র এক নয়। Mock server কখনও pass বা fail রিপোর্ট করে না। Card decline হলে payment provider কী response দেবে, সে বিষয়ে test runner-এর কোনো পূর্বনির্ধারিত ধারণা থাকে না। যেসব team ইতিমধ্যে একটি server ভাড়া নিয়েছে, তাদের অধিকাংশই শেষ পর্যন্ত প্রতিটির একটি করে instance চালায়। এগুলো একই Docker Compose file দিয়ে start করা হয় এবং একই pull request-এ review করা হয়।

নিজস্ব হোস্টে API mocking ও testing কেন করবেন?

আপনার fixtures-এ production-এর আদলের data থাকে। API test-এর request body নাম পরিবর্তন করা একটি বাস্তব customer record হতে পারে। আবার কেউ যাচাই না করলে নামটিও পরিবর্তিত নাও হতে পারে। Recorded stub আরও ঝুঁকিপূর্ণ। Proxy recording upstream বাস্তবে যা ফেরত দিয়েছে, সেটিই সংরক্ষণ করে। ফলে recording দিয়ে তৈরি stub directory-তে কেউ প্রতিটি file না পড়া পর্যন্ত live token এবং customer email address থেকে যায়। Hosted service-এ এই data অন্য পক্ষের security incident এবং আপনার data disclosure-এর কারণ হয়ে দাঁড়ায়।

দ্বিতীয় কারণ হলো reachability। Private address-এ bound করা service hosted runner থেকে reachable নয়। তাই test একেবারেই চালানো যায় না। প্রতিটি workaround-এর কিছু না কিছু খরচ আছে। API test করার জন্য সেটিকে Internet-এ প্রকাশ করলে service-টিকে private রাখার কারণই নষ্ট হয়। Tunnel বা public staging copy রক্ষণাবেক্ষণের জন্য আরেকটি system তৈরি করে। আবার release-এর মাঝখানে staging copy production থেকে আলাদা হয়ে যায়। একই private network-এ থাকা runner service-টিকে সরাসরি call করতে পারে এবং এসবের কোনোটিই প্রয়োজন হয় না। এই বাস্তব কারণেই নিজস্ব হোস্টে GitHub Actions runner ব্যবহার করা হয়।

কোন self-hosted mock server চালাবেন?

এগুলোর প্রতিটিই আপনার মালিকানাধীন কোনো server-এ container হিসেবে চলে। গুরুত্বপূর্ণ বিষয় হলো, প্রতিটি tool কোন জিনিসকে source of truth হিসেবে ধরে। কারণ container পুনর্নির্মাণ করলে আপনার কোনো ক্ষতি হবে না, নাকি এতে পুরো একটি বিকেল চলে যাবে, তা এই বিষয়ের ওপর নির্ভর করে।

  • WireMock প্রতিটি stub একটি mappings/ directory-তে JSON file হিসেবে রাখে। বড় response body থাকে __files/-এ। image হলো wiremock/wiremock, container-এর ভেতরে root directory হলো /home/wiremock, এবং এটি recording proxy হিসেবেও চলে। File disk-এ থাকলে mock-টি অন্য যেকোনো code-এর মতো git-এ রাখা যায়।
  • Mockoon CLI একটি সম্পূর্ণ mock API-কে একটি JSON data file-এ রাখে। npm install -g @mockoon/cli দিয়ে এটি install করুন এবং mockoon-cli start --data ./data-file.json দিয়ে চালু করুন। অথবা ওই file-টি bind mount করে mockoon/cli image চালান। desktop app একই file edit করে। তাই UI-তে design করা এবং ফলাফল commit করা—দুটি পরস্পরের সঙ্গে সামঞ্জস্যপূর্ণ থাকে।
  • MockServer mockserver/mockserver image থেকে চলে এবং port 1080-এ listen করে। এর নিজস্ব REST API-এর মাধ্যমে expectation পাঠানো হয়। Test code থেকে এটি সুবিধাজনক, কিন্তু deployment-এর ক্ষেত্রে ঝুঁকিপূর্ণ। HTTP call-এর মাধ্যমে তৈরি করা expectation container restart হলে মুছে যায়। স্থায়ী stub-এর জন্য এর JSON initialization file ব্যবহার করুন।
  • Prism আলাদা stub file-এর পরিবর্তে আপনার OpenAPI document থেকে mock তৈরি করে। npm install -g @stoplight/prism-cli দিয়ে এটি install করুন, তারপর prism mock openapi.yaml চালান। Container-এর ভেতরে -h 0.0.0.0 যোগ করুন। কারণ Prism ডিফল্টভাবে localhost-এ bind করে এবং তা না হলে container-এর বাইরে থেকে এতে access করা যায় না।
  • Microcks হলো বড় পরিসরের option। এতে একটি web UI থাকে, যা OpenAPI document এবং Postman collection import করে। এরপর সেগুলো mock হিসেবে serve করে এবং contract test চালায়। সম্পূর্ণ install-এর জন্য MongoDB ও Keycloak প্রয়োজন। Async feature-এর জন্য Kafka-ও লাগে। all in one microcks-uber image একটি in memory MongoDB bundle করে। Project documentation অনুযায়ী এটি ephemeral use-এর জন্য উপযোগী। তাই ওই UI-তে তৈরি যেকোনো কিছু disposable হিসেবে বিবেচনা করুন এবং source artifact-গুলো git-এ রাখুন।

কোন self-hosted API test runner চালাবেন?

এখানে কাজটি একটি ধারাবাহিকতা: authenticate করা, একটি order তৈরি করা, সেটি আবার পড়া এবং state পরিবর্তিত হয়েছে কি না যাচাই করা। এর জন্য একটি response থেকে value সংগ্রহ করে পরের request-এ ব্যবহার করতে হয়। যে tool call-গুলোর মধ্যে state বহন করতে পারে না, সেটি health check, API test নয়।

  • Hurl একটি binary থেকে HTTP request-এর plain text file চালায়। একটি [Captures] section response থেকে value বের করে, একটি [Asserts] section সেই value পরীক্ষা করে এবং --test এটিকে summary ও exit code-সহ test runner-এ রূপান্তর করে। August 2026 অনুযায়ী Version 8.0.1 সর্বশেষ।
  • Bruno CLI একটি folder-এ থাকা .bru file চালায়। npm install -g @usebruno/cli দিয়ে install করুন, তারপর bru run folder --env Local --reporter-junit results.xml চালান। Collection format ইচ্ছাকৃতভাবে একটি directory-র text file হিসেবে রাখা হয়, তাই review-এর সময় diff পড়া সহজ।
  • Newman Postman-এর বাইরে Postman collection চালায়: npm install -g newman, তারপর newman run collection.json -r cli,junit --reporter-junit-export results.xml। সমস্যা format-এ। Collection একটি exported JSON blob, তাই editing Postman-এ করতে হয় এবং git-এ থাকা file-টি একটি copy হয়ে যায়, যা পরে পুরোনো হয়ে পড়ে।
  • Schemathesis ভিন্ন ধরনের check। এটি একটি OpenAPI schema পড়ে এমন case তৈরি করে, যা schema অনুযায়ী অসম্ভব response তৈরি করার চেষ্টা করে: uvx schemathesis run https://your.api/openapi.json। এটি crash ও contract violation খুঁজে পায়, কিন্তু আপনার business rule সম্পর্কে কিছু জানে না। তাই scripted suite-এর পাশাপাশি এটি ব্যবহার করুন, তার বিকল্প হিসেবে নয়।
  • Hoppscotch self hosted web UI-ভিত্তিক option এবং এর জন্য একটি Postgres instance প্রয়োজন। Install করার আগে এই tradeoff বুঝে নিন: collection আপনার repository-তে নয়, একটি database-এ থাকে।

একটি tool এড়িয়ে চলুন। Step CI এখনও tool roundup-এ দেখা যায় এবং এর YAML workflow format পড়তে সুবিধাজনক। তবে repository-তে সর্বশেষ commit পড়েছে August 2024-এ। আপনার CI এবং API-এর মাঝখানে থাকা কোনো program-এর ক্ষেত্রে unmaintained code ব্যবহার করা ঝুঁকিপূর্ণ।

ফায়ারওয়ালের পেছনে mock server রাখুন

নিচের সেটআপে payment provider-এর বিকল্প হিসেবে WireMock চালানো হয়। compose file format আপনার কাছে নতুন হলে, VPS-এ Docker Compose এই section-এ ব্যবহৃত lifecycle command-গুলো ব্যাখ্যা করেছে।

services:
  mock-payments:
    image: wiremock/wiremock:3.13.2
    command: ["--verbose"]
    volumes:
      - ./mocks/payments:/home/wiremock
    ports:
      - "127.0.0.1:8080:8080"
    restart: unless-stopped

Port-এর 127.0.0.1: prefix-টিই গুরুত্বপূর্ণ। শুধু 8080:8080 ব্যবহার করলে mock-টি আপনার public IP-সহ প্রতিটি interface-এ প্রকাশিত হয়। Port-টি deny করতে ufw ব্যবহার করলেও এটি reachable থাকে, কারণ Docker নিজের rule DOCKER iptables chain-এ লেখে এবং সেগুলো ufw-এর INPUT rule-এর আগে মূল্যায়ন করা হয়। এর পরিবর্তে loopback address বা private interface address-এ bind করুন। তাহলে kernel বাইরের সংযোগ গ্রহণ করবে না।

এরপর test করা service-টি mock-এর দিকে নির্দেশ করুন। Service-টি একই compose project-এ চললে mock-এর base URL হবে http://mock-payments:8080, কারণ compose নিজের network-এ service name resolve করে। Service-টি host-এ চললে URL হবে http://127.0.0.1:8080। এটি environment variable-এর মাধ্যমে সেট করুন, code-এর মধ্যে নয়। নইলে test URL production-এ চলে যেতে পারে।

Stub-গুলো ./mocks/payments/mappings/-এ রাখুন। প্রতিটি stub-এর জন্য একটি JSON file রাখুন।

{
  "request": {
    "method": "POST",
    "urlPath": "/v1/charges",
    "bodyPatterns": [{ "matchesJsonPath": "$.amount" }]
  },
  "response": {
    "status": 201,
    "headers": { "Content-Type": "application/json" },
    "jsonBody": { "id": "ch_test_001", "status": "succeeded", "amount": 4200 }
  }
}

এটি চালু করার পর কী লোড হয়েছে তা পরীক্ষা করুন।

docker compose up -d --wait mock-payments
curl -fsS http://127.0.0.1:8080/__admin/mappings

Container healthy হওয়ার কথা না জানানো পর্যন্ত --wait অপেক্ষা করে। এটি কাজ করে, কারণ WireMock image-এ তার /__admin/health endpoint-এর জন্য একটি HEALTHCHECK রয়েছে। mappings call server যে সব stub পড়েছে তার তালিকা দেখায়। আপনি তৈরি করা কোনো stub যদি সেই তালিকায় না থাকে, সেটি লোড হয়নি। File-টি mounted root-এর মধ্যে নয়, mappings/-এর অধীনে আছে কি না পরীক্ষা করুন। JSON parse হচ্ছে কি না তাও পরীক্ষা করুন।

কোনো request এলে এবং কোনো stub-এর সঙ্গে match না করলে WireMock 404 status দিয়ে উত্তর দেয়। Response body Request was not matched দিয়ে শুরু হয়। এরপর server-এর কাছে থাকা সবচেয়ে কাছের stub-এর সঙ্গে একটি diff দেখানো হয়। কিছু পরিবর্তন করার আগে সেই diff পড়ুন। এতে ঠিক কোন field ভিন্ন তা উল্লেখ থাকে। সাধারণত সেটি /v1/charge-সহ একটি path, যেখানে stub-এ /v1/charges লেখা আছে।

কলগুলোর মধ্যে state বহাল রেখে test-টি একটি sequence হিসেবে লিখুন

Hurl file-গুলো plain text। প্রকল্পের releases থেকে deb package install করুন।

VERSION=8.0.1
curl --location --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/$VERSION/hurl_${VERSION}_amd64.deb
sudo apt update && sudo apt install ./hurl_${VERSION}_amd64.deb

mock-এর বিরুদ্ধে আপনার নিজস্ব API পরীক্ষা করে এমন একটি suite আছে tests/checkout.hurl-এ।

POST {{base_url}}/orders
Content-Type: application/json
{
  "sku": "ssd-1tb",
  "amount": 4200
}
HTTP 201
[Captures]
order_id: jsonpath "$['id']"

GET {{base_url}}/orders/{{order_id}}
HTTP 200
[Asserts]
jsonpath "$.status" == "paid"
jsonpath "$.charge_id" == "ch_test_001"

[Captures] block-ই এটিকে দুটি সম্পর্কহীন request-এর পরিবর্তে একটি API test তৈরি করে। order_id প্রথম response থেকে পড়ে দ্বিতীয়টির URL-এ interpolated হয়। charge_id-এর assertion-ই পুরো কাজটির মূল বিষয়: এটি প্রমাণ করে যে আপনার service payment provider-কে call করেছে এবং ফিরে আসা data সংরক্ষণ করেছে। এটি যে value-এর সঙ্গে তুলনা করে, সেটিই আপনি WireMock stub-এ লিখেছিলেন। এখন একটি file-এই flow-এর উভয় অংশ রয়েছে।

hurl --test --variable base_url=http://127.0.0.1:3000 \
  --report-junit reports/junit.xml \
  --report-json reports/json \
  tests/

সফল run প্রতিটি file-এর জন্য একটি করে line এবং শেষে একটি summary দেখায়।

tests/checkout.hurl: Success (2 request(s) in 61 ms)
Executed files:    1
Executed requests: 2 (30.1/s)
Succeeded files:   1 (100.0%)
Failed files:      0 (0.0%)
Duration:          64 ms

ব্যর্থ হলে file ও line number-সহ error: Assert failure দেখায়। এরপর এটি পাওয়া value এবং প্রত্যাশিত value পাশাপাশি দেখায়। hurl non zero status-এ exit করে, তাই CI থেমে যায়। আপনি যেখানে paid প্রত্যাশা করেছিলেন, সেখানে যদি status, pending পড়ে, তাহলে আপনার service mock-এর response process করেনি। এরপর /__admin/requests-এ থাকা WireMock request journal দেখুন। সেখানে বোঝা যাবে call-টি mock-এ আদৌ পৌঁছেছিল কি না।

নিজের CI runner থেকে suite চালান

একই সার্ভারে runner নিবন্ধিত থাকলে workflow সংক্ষিপ্ত হয়। Runner host-এ একটি সাধারণ process হিসেবে চলে। তাই docker এবং hurl ওই host-এ ইনস্টল করা থাকতে হবে। Hosted image থেকে কোনো কিছু উত্তরাধিকারসূত্রে পাওয়া যায় না।

name: api-tests
on: [push]
jobs:
  hurl:
    runs-on: self-hosted
    steps:
      - uses: actions/checkout@v4
      - name: Start the mock
        run: docker compose up -d --wait mock-payments
      - name: Run the suite
        run: hurl --test --variable base_url=http://127.0.0.1:3000 --report-junit reports/junit.xml tests/
      - name: Archive the reports
        if: always()
        run: install -d /srv/api-tests/reports/$GITHUB_SHA && cp -r reports/. /srv/api-tests/reports/$GITHUB_SHA/
      - name: Stop the mock
        if: always()
        run: docker compose down

Archive ধাপে if: always() গুরুত্বপূর্ণ। এটি না থাকলে test run ব্যর্থ হলে copy ধাপটি এড়িয়ে যায়। ফলে যে report পড়তে চেয়েছিলেন, সেটিই হারিয়ে যায়। Copy-টির গন্তব্য workspace-এর বাইরে হতে হবে। কারণ পরবর্তী job-এর আগে runner workspace পরিষ্কার করে এবং report-গুলোও তার সঙ্গে মুছে যায়।

শুধু সর্বশেষ রান নয়, ফলাফল সংরক্ষণ করুন

প্রতি commit-এর জন্য একটি JUnit XML ফাইল একটি প্রশ্নের উত্তর দেয়: এটি সফল হয়েছে কি না। কিন্তু কোনো endpoint কখন ধীর হতে শুরু করেছে, তার উত্তর এটি দেয় না, কারণ আপনি ফাইলগুলো খোলা বন্ধ করার পর সেগুলো আর কেউ পড়ে না। প্রবণতা বোঝার জন্য একই box-এর একটি ছোট database-এ প্রতি run-এর জন্য একটি row যোগ করুন। commit SHA, file name, pass count, fail count এবং duration রাখা একটি single table-ই যথেষ্ট। এটি রাখার জন্য VPS-এ production-এ SQLite একটি যুক্তিসংগত স্থান: একটি file, কোনো server process নয়, এবং আপনার নেওয়া backup-এর সঙ্গেই পুরো history সংরক্ষিত থাকে। JUnit XML-এর পরিবর্তে Hurl-এর --report-json output parse করুন, কারণ দুটির মধ্যে এটিই machine-readable format।

কনটেইনার rebuild-এর পর কী টিকে থাকতে হবে

Mock definition এবং test suite হলো source code। এগুলো যে service-এর বর্ণনা দেয়, সেই service-এর পাশের repository-তে রাখুন। কোনো endpoint পরিবর্তন করলে একই pull request-এ এগুলোও পরিবর্তন করুন। কোনো web UI-তে সম্পাদিত stub, অথবা runtime-এ REST API ব্যবহার করে MockServer-এ পাঠানো কোনো expectation শুধু ওই container-এর memory বা সেই tool-এর database-এ থাকে। docker compose down চালালে সেগুলো হারিয়ে যায়। কেউ তা বুঝতে পারে না, যতক্ষণ না কোনো test ভুল কারণে pass করতে শুরু করে। আপনার repository-গুলো যদি নিজেদের hardware-এও চালান, তবে একটি self-hosted git server fixture এবং service-কে একই trust boundary-এর মধ্যে রাখে।

এবার ব্যবহারিক নিয়মগুলো দেখুন। Image tag নির্দিষ্ট করে দিন। কারণ latest repository-তে কোনো পরিবর্তন ছাড়াই mock কীভাবে request match করবে তা বদলে দিতে পারে। এই ব্যর্থতার সঙ্গে এর কারণের সম্পর্ক খুঁজে পাওয়া খুব কঠিন। Tool-এর কোনো directory-তে লেখার প্রয়োজন না থাকলে সেটি read-only হিসেবে mount করুন। কোনো mock-এর stub named Docker volume-এ রাখবেন না। তা হলে volume-ই source of truth হয়ে যায় এবং git-এ থাকা copy নীরবে ভুল হয়ে যায়।

আরেকটি বিষয় আছে, যা অনেককে সমস্যায় ফেলে। Proxy-এর মাধ্যমে real traffic record করে stub তৈরি করলে commit করার আগে প্রতিটি generated file পড়ে দেখুন। Recording-এ upstream যে content ফেরত পাঠিয়েছে, ঠিক সেটিই থাকে। এর মধ্যে bearer token এবং customer email address-ও থাকতে পারে। এটি commit করলে content repository-তে স্থায়ীভাবে থেকে যায়। কারণ git মুছে দেওয়া content-ও history-তে রেখে দেয়।

FAQ

API mock server এবং API test runner-এর মধ্যে পার্থক্য কী?

একটি mock server অনুরোধের উত্তর দেয়। এটি এমন dependency-এর বিকল্প হিসেবে কাজ করে, যেটিতে CI থেকে কল করা যায় না, এবং এটি কখনো pass বা fail রিপোর্ট করে না। একটি API test runner আপনার নিজস্ব service-এ অনুরোধ পাঠায়, response-এর ওপর assertion চালায়, একটি call-এর মান পরের call-এ ব্যবহার করে, এবং কোনো assertion ব্যর্থ হলে non zero status নিয়ে শেষ হয়। এরা ভিন্ন সমস্যা সমাধান করে। সাধারণ setup-এ দুটিই একসঙ্গে চলে: runner আপনার service-এ call করে, আর আপনার service mock-এ call করে।

Hosted CI runner থেকে কি internal API test করা যায়?

API-টি বাইরে প্রকাশ না করলে যায় না। একটি hosted runner আপনার network-এর বাইরে থাকে, তাই private address-এ bind করা service-এ পৌঁছাতে পারে না। আপনার বিকল্প হলো API publish করা, একটি tunnel চালানো, অথবা public staging copy বজায় রাখা। প্রতিটি বিকল্পের সঙ্গে এমন একটি অতিরিক্ত system যুক্ত হয়, যা ব্যর্থ হতে বা তথ্য ফাঁস করতে পারে। একই private network-এ থাকা runner সরাসরি service-এ call করতে পারে। দলগুলো এই কাজ নিজস্ব পরিকাঠামোতে চালানোর প্রধান বাস্তব কারণ এটিই।

Mock stub এবং API test suite কোথায় রাখা উচিত?

git-এ, যে service-কে এগুলো বর্ণনা করে তার পাশে রাখা উচিত। WireMock-এর mappings/ directory, Mockoon-এর data file, Hurl file এবং Bruno-এর .bru folder-এর মতো file হিসেবে definition সংরক্ষণকারী tool code review এবং কোনো অতিরিক্ত খরচ ছাড়াই container rebuild করার সুবিধা দেয়। Database বা web UI-তে definition সংরক্ষণকারী tool-এর জন্য backup plan এবং export step দরকার। মানুষ সাধারণত container ইতিমধ্যে চলে যাওয়ার পর export-এর গুরুত্ব মনে করে।

Stub সঠিক মনে হলেও আমার mock কেন 404 ফেরত দেয়?

WireMock কেবল exact match হলে stub পরিবেশন করে। কোনো request-এর match না হলে 404 ফেরত আসে। এর body Request was not matched দিয়ে শুরু হয় এবং তার পরে সবচেয়ে কাছের stub-এর সঙ্গে একটি diff থাকে। এই diff-এ কোন field আলাদা, তা উল্লেখ করা হয়। সাধারণ কারণগুলোর মধ্যে আছে path-এর শেষে trailing slash থাকা, stub-এ প্রয়োজনীয় Content-Type header client থেকে না পাঠানো, variable segment-এর জন্য stub-এ urlPathPattern প্রয়োজন হলেও urlPath ব্যবহার করা, এবং payload-এর সঙ্গে না মেলা body matcher। প্রথমে /__admin/requests পরীক্ষা করে নিশ্চিত করুন যে request-টি mock-এ পৌঁছেছিল কি না।

Staging environment থাকলেও কি mock প্রয়োজন?

হ্যাঁ, দুটি কারণে। আপনার নিয়ন্ত্রণে নেই এমন upstream-এর staging copy বন্ধ হয়ে যেতে পারে এবং আপনাকে rate limit করতে পারে। ফলে আপনার code-এর সঙ্গে সম্পর্কহীন কারণে suite ব্যর্থ হয়। এটি test করার জন্য সবচেয়ে প্রয়োজনীয় response-ও তৈরি করতে পারে না, যেমন declined card বা gateway timeout। mock চাহিদা অনুযায়ী local network speed-এ এই response ফিরিয়ে দেয়। ফলে sandbox-এর বিরুদ্ধে কয়েক মিনিট লাগা suite কয়েক সেকেন্ডে শেষ হয়। Release-এর আগে final check-এর জন্য staging রাখুন এবং CI-তে mock ব্যবহার করুন।