SSD Nodes Learn 🎉 VPS $5.50/माह से
गाइड Matt Connorलेखक: Matt Connor

VPS पर API mocking और testing कैसे सेटअप करें

API mocking और testing के बीच के अंतर को समझें। WireMock और Hurl का उपयोग करके अपने VPS पर इन्हें सेटअप करने का तरीका जानें ताकि CI में सटीक रिपोर्ट और डेटा मिल सके।

एक ही repository साझा करने वाले दो कार्य

Self-hosted API mocking और testing दो अलग-अलग कार्य हैं, और इन्हें एक मानने में एक सप्ताह बर्बाद हो जाता है। एक mock server उस dependency की जगह लेता है जिसे आप CI से call नहीं कर सकते: जैसे कि payment provider, partner API, rate limited upstream, या ऐसी service जिसे किसी अन्य टीम ने अभी तक ship नहीं किया है। एक API test runner आपके अपने endpoints को एक निश्चित क्रम में call करता है और responses की पुष्टि करता है, जिसमें एक response से मान (values) को अगले request में ले जाया जाता है।

ये दोनों कार्य आपस में नहीं मिलते हैं। एक mock server कभी भी pass या fail की रिपोर्ट नहीं देता है। एक test runner की इस बात पर कोई राय नहीं होती कि कार्ड decline होने पर payment provider क्या return करता है। अधिकांश टीमें जो पहले से ही एक box किराए पर लेती हैं, वे अंततः दोनों को चलाती हैं, जिन्हें एक ही Docker Compose file द्वारा start किया जाता है और एक ही pull request में review किया जाता है।

API mocking और testing को self-host क्यों करें?

आपके fixtures production के अनुरूप डेटा होते हैं। API test में request body एक वास्तविक customer record होता है, जिसमें या तो नाम बदल दिया जाता है, या फिर नाम नहीं बदला जाता क्योंकि किसी ने जाँच नहीं की। Recorded stubs और भी खराब होते हैं: proxy recording यह स्टोर करती है कि upstream ने वास्तव में क्या return किया था। इसलिए, recording द्वारा बनाई गई stub directory में live tokens और customer email addresses तब तक रहते हैं जब तक कोई हर एक file को न पढ़ ले। किसी hosted service पर वह डेटा किसी और की incident और आपकी disclosure बन जाता है।

दूसरा कारण reachability है। private address पर bound कोई service किसी hosted runner से access नहीं की जा सकती, इसलिए test चल ही नहीं पाता। हर workaround की अपनी कीमत होती है। test करने के लिए API को internet पर publish करने से उस सुरक्षा का उद्देश्य ही खत्म हो जाता है जिसके लिए वह private थी। tunnel या public staging copy को maintain करना एक अतिरिक्त काम है, और staging copy releases के बीच production से अलग (drift) हो जाती है। उसी private network पर मौजूद runner सीधे service को call करता है और उसे इनमें से किसी की आवश्यकता नहीं होती, जो a self-hosted GitHub Actions runner के पीछे का व्यावहारिक तर्क है।

आपको कौन सा self-hosted mock server चलाना चाहिए?

इनमें से प्रत्येक आपके अपने सर्वर पर एक container के रूप में चलता है। सबसे महत्वपूर्ण प्रश्न यह है कि प्रत्येक tool किसे 'source of truth' मानता है, क्योंकि इसी से तय होता है कि container को फिर से बनाने में आपको कोई मेहनत नहीं करनी पड़ेगी या पूरा दोपहर बर्बाद हो जाएगा।

  • WireMock हर stub को mappings/ directory में एक JSON file के रूप में रखता है, और बड़े response bodies को __files/ में रखता है। इसका image wiremock/wiremock है, container के अंदर इसकी root directory /home/wiremock है, और यह एक recording proxy के रूप में भी काम करता है। डिस्क पर फाइलें होने का मतलब है कि mock किसी अन्य code की तरह ही git में रहता है।
  • Mockoon CLI पूरे mock API को एक ही JSON data file में रखता है। इसे npm install -g @mockoon/cli के साथ install करें और mockoon-cli start --data ./data-file.json के साथ start करें, या उस file को bind mount करके mockoon/cli image चलाएं। desktop app उसी file को edit करती है, इसलिए UI में design करना और परिणाम को commit करना आपस में compatible रहता है।
  • MockServer mockserver/mockserver image से चलता है और port 1080 पर listen करता है। Expectations इसके अपने REST API के माध्यम से आती हैं, जो test code के लिए तो सुविधाजनक है लेकिन deployment के लिए जोखिम भरा है: HTTP call द्वारा बनाई गई expectation container restart होने पर हट जाती है। उन stubs के लिए जो स्थायी होने चाहिए, इसकी JSON initialization file का उपयोग करें।
  • Prism mock को अलग stub files के बजाय आपके OpenAPI document से बनाता है। इसे npm install -g @stoplight/prism-cli के साथ install करें, फिर prism mock openapi.yaml चलाएं। container के अंदर -h 0.0.0.0 जोड़ें, क्योंकि Prism default रूप से localhost पर bind होता है और अन्यथा container के बाहर से पहुंच योग्य नहीं होता।
  • Microcks एक बड़ा विकल्प है: एक web UI जो OpenAPI documents और Postman collections को import करता है, फिर उन्हें mocks के रूप में serve करता है और contract tests चलाता है। पूर्ण install के लिए MongoDB और Keycloak की आवश्यकता होती है, साथ ही इसके async features के लिए Kafka भी चाहिए। all-in-one microcks-uber image में एक in-memory MongoDB शामिल है, जिसे project documentation में केवल अस्थायी उपयोग के लिए उपयुक्त बताया गया है, इसलिए उस UI में बनाई गई किसी भी चीज़ को disposable मानें और source artifacts को git में रखें।

आपको कौन सा self-hosted API test runner इस्तेमाल करना चाहिए?

यहाँ कार्य एक क्रम में होता है: authenticate करना, order बनाना, उसे वापस पढ़ना और यह सुनिश्चित करना कि state बदल गई है। इसके लिए एक response से value capture करके उसे अगले request में इस्तेमाल करने की आवश्यकता होती है। जो tool calls के बीच state को बरकरार नहीं रख सकता, वह केवल health check है, API test नहीं।

  • Hurl एक single binary से HTTP requests की plain text files चलाता है। एक [Captures] section response से values निकालता है, एक [Asserts] section उनकी जाँच करता है, और --test इसे summary और exit code के साथ एक test runner में बदल देता है। अगस्त 2026 तक version 8.0.1 वर्तमान है।
  • Bruno CLI .bru files के एक folder को चलाता है। इसे npm install -g @usebruno/cli से install करें, फिर bru run folder --env Local --reporter-junit results.xml चलाएँ। collection format को जानबूझकर directory में text files के रूप में रखा गया है, ताकि review में diffs आसानी से पढ़े जा सकें।
  • Newman Postman collections को Postman के बाहर चलाता है: 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 होती है जो पुरानी (stale) हो जाती है।
  • Schemathesis एक अलग प्रकार की जाँच है। यह OpenAPI schema को पढ़ता है और ऐसे cases generate करता है जो उन responses को पैदा करने की कोशिश करते हैं जिन्हें आपका schema असंभव बताता है: uvx schemathesis run https://your.api/openapi.json। यह crashes और contract violations को ढूँढता है, और इसे आपके business rules की जानकारी नहीं होती, इसलिए यह scripted suite को replace करने के बजाय उसके साथ काम करता है।
  • Hoppscotch self-hosted एक web UI विकल्प है, और इसके लिए Postgres instance की आवश्यकता होती है। इसे install करने से पहले इस tradeoff को समझें: collections repository में नहीं, बल्कि database में रहते हैं।

एक जिसे नजरअंदाज करना चाहिए। Step CI अभी भी tool roundups में दिखाई देता है और इसका YAML workflow format पढ़ने में अच्छा है, लेकिन repository में आखिरी commit अगस्त 2024 में हुआ था। जो program आपके CI और API के बीच स्थित हो, वह unmaintained code के लिए एक खराब जगह है।

मॉक सर्वर को फायरवॉल के पीछे रखें

नीचे दिया गया सेटअप WireMock को एक पेमेंट प्रोवाइडर के विकल्प के रूप में चलाता है। यदि compose फ़ाइल फॉर्मेट आपके लिए नया है, तो Docker Compose on a VPS उन लाइफसाइकिल कमांड्स को कवर करता है जिन्हें यह सेक्शन आधार मानता है।

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

पोर्ट पर 127.0.0.1: प्रीफिक्स सबसे महत्वपूर्ण हिस्सा है। एक साधारण 8080:8080 मॉक को हर इंटरफेस पर पब्लिश कर देता है, जिसमें आपका पब्लिक IP भी शामिल है। यह तब भी पहुंच योग्य रहता है जब ufw उस पोर्ट को डिनाई कर रहा हो, क्योंकि Docker अपने नियम सीधे DOCKER iptables चेन में लिखता है और वे ufw के INPUT नियमों से पहले इवैल्यूएट होते हैं। इसके बजाय लूपबैक एड्रेस या किसी प्राइवेट इंटरफेस एड्रेस पर बाइंड करें, जिससे कर्नल बाहर से आने वाले कनेक्शन को कभी स्वीकार न करे।

आपका टेस्ट के अधीन सर्विस फिर मॉक की ओर पॉइंट करता है। जब सर्विस एक ही compose प्रोजेक्ट में चलती है, तो मॉक का बेस URL http://mock-payments:8080 होता है, क्योंकि compose अपने नेटवर्क पर सर्विस नामों को रिज़ॉल्व करता है। जब सर्विस होस्ट पर चलती है, तो यह http://127.0.0.1:8080 होता है। इसे एनवायरनमेंट वेरिएबल के माध्यम से सेट करें, कभी भी कोड में न लिखें, अन्यथा टेस्ट URL प्रोडक्शन में चला जाएगा।

स्टब्स ./mocks/payments/mappings/ में जाते हैं, प्रत्येक के लिए एक JSON फ़ाइल।

{
  "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

--wait तब तक ब्लॉक रहता है जब तक कंटेनर हेल्दी रिपोर्ट नहीं करता। यह इसलिए काम करता है क्योंकि WireMock इमेज अपने /__admin/health एंडपॉइंट के खिलाफ एक HEALTHCHECK भेजती है। mappings कॉल उन सभी स्टब्स को लिस्ट करती है जिन्हें सर्वर ने पढ़ा है। यदि आपके द्वारा लिखा गया कोई स्टब उस लिस्ट में नहीं है, तो वह लोड नहीं हुआ है: जांचें कि फ़ाइल माउंटेड रूट के बजाय mappings/ के अंदर है, और जांचें कि JSON पार्स होता है या नहीं।

जब कोई रिक्वेस्ट आती है और कोई स्टब मैच नहीं करता, तो WireMock 404 के साथ उत्तर देता है, जिसका बॉडी Request was not matched से शुरू होता है, जिसके बाद उसके पास मौजूद सबसे करीबी स्टब के साथ एक डिफ (diff) होता है। कुछ भी बदलने से पहले उस डिफ को पढ़ें, क्योंकि यह उस सटीक फील्ड का नाम बताता है जो अलग है। यह आमतौर पर एक पाथ होता है जिसमें /v1/charge होता है, जबकि स्टब में /v1/charges लिखा होता है।

टेस्ट को एक अनुक्रम के रूप में लिखें जिसमें कॉल्स के बीच स्टेट बनी रहे

Hurl फाइलें प्लेन टेक्स्ट होती हैं। प्रोजेक्ट के releases से deb फाइल इंस्टॉल करें।

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

एक सूट जो आपके अपने API को mock के विरुद्ध टेस्ट करता है, वह 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] ब्लॉक ही वह हिस्सा है जो इसे दो असंबंधित अनुरोधों के बजाय एक API टेस्ट बनाता है। order_id को पहले रिस्पॉन्स से पढ़ा जाता है और दूसरे के URL में इंटरपोलेट किया जाता है। charge_id पर किया गया assertion ही पूरी प्रक्रिया का मुख्य बिंदु है: यह साबित करता है कि आपकी सर्विस ने पेमेंट प्रोवाइडर को कॉल किया और जो रिस्पॉन्स आया उसे स्टोर किया, और जिस वैल्यू से इसकी तुलना की जा रही है, वह वही है जिसे आपने WireMock स्टब में लिखा था। अब एक ही फाइल फ्लो के दोनों हिस्सों को कवर करती है।

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

एक सफल रन प्रत्येक फाइल के लिए एक लाइन और एक सारांश प्रिंट करता है।

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

एक विफलता फाइल और लाइन नंबर के साथ error: Assert failure प्रिंट करती है, फिर वह वैल्यू जो प्राप्त हुई और जो अपेक्षित थी, और hurl नॉन-जीरो एग्जिट कोड देता है ताकि CI रुक जाए। यदि status में pending पढ़ा जाता है जहाँ आपको paid की अपेक्षा थी, तो आपकी सर्विस ने mock के रिस्पॉन्स को प्रोसेस नहीं किया। इसके बाद अगला कदम /__admin/requests पर WireMock रिक्वेस्ट जर्नल को पढ़ना है, जो यह दिखाता है कि क्या कॉल वास्तव में mock तक पहुँची थी।

अपने स्वयं के CI runner से suite को ट्रिगर करें

उसी मशीन पर पंजीकृत runner के साथ, workflow संक्षिप्त हो जाता है। runner host पर एक सामान्य process है, इसलिए उस host पर docker और hurl का इंस्टॉल होना अनिवार्य है। hosted image से कुछ भी inherit नहीं होता है।

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 step पर if: always() महत्वपूर्ण है। इसके बिना, एक विफल test run copy को छोड़ देता है, जिससे आप वह रिपोर्ट खो देते हैं जिसे आप पढ़ना चाहते थे। copy को workspace के बाहर भी जाना चाहिए, क्योंकि runner अगले job से पहले workspace को साफ कर देता है और रिपोर्ट भी उसके साथ हट जाती हैं।

परिणामों को सुरक्षित रखें, केवल अंतिम रन को नहीं

प्रति commit एक JUnit XML फ़ाइल केवल एक प्रश्न का उत्तर देती है: क्या यह पास हुआ। यह इस प्रश्न का उत्तर नहीं देती कि कोई endpoint कब धीमा होना शुरू हुआ, क्योंकि एक बार जब आप उन फ़ाइलों को देखना बंद कर देते हैं, तो उन्हें कोई नहीं पढ़ता। रुझान (trend) देखने के लिए, उसी मशीन पर एक छोटे डेटाबेस में प्रति रन एक पंक्ति जोड़ें। commit SHA, फ़ाइल नाम, पास काउंट, फेल काउंट और अवधि रखने वाली एक एकल तालिका पर्याप्त है, और VPS पर SQLite का उपयोग इसे रखने के लिए एक उचित स्थान है: एक फ़ाइल, कोई सर्वर प्रक्रिया नहीं, और पूरा इतिहास आपके द्वारा पहले से लिए जा रहे बैकअप के साथ सुरक्षित रहता है। JUnit XML के बजाय Hurl के --report-json आउटपुट को पार्स करें, क्योंकि दोनों में से यही मशीन-पठनीय प्रारूप है।

कंटेनर को फिर से बनाने (rebuild) पर क्या सुरक्षित रहना चाहिए

Mock परिभाषाएँ और test suites सोर्स कोड होते हैं। इन्हें उस सर्विस के साथ रिपॉजिटरी में होना चाहिए जिसका ये वर्णन करते हैं, और इन्हें उसी pull request में बदला जाना चाहिए जो किसी endpoint को बदलता है। वेब UI में संपादित किया गया stub, या रनटाइम पर REST API के माध्यम से MockServer पर भेजी गई expectation, केवल उस कंटेनर की मेमोरी या उस टूल के डेटाबेस में मौजूद रहती है। docker compose down चलाएं और यह गायब हो जाता है, और किसी को तब तक पता नहीं चलता जब तक कि कोई टेस्ट गलत कारण से पास न होने लगे। यदि आपकी रिपॉजिटरी आपके अपने हार्डवेयर पर भी चलती है, तो एक self-hosted git server फिक्स्चर और सर्विस को एक ही ट्रस्ट बाउंड्री के भीतर रखता है।

अब व्यावहारिक नियम। इमेज टैग को पिन करें, क्योंकि latest आपकी रिपॉजिटरी में बिना किसी बदलाव के यह बदल सकता है कि आपका मॉक अनुरोधों (requests) का मिलान कैसे करता है, और उस विफलता के मूल कारण का पता लगाना बहुत कठिन होता है। जब टूल को stub डायरेक्टरी में लिखने की आवश्यकता न हो, तो उन्हें read-only मोड में माउंट करें। किसी मॉक के stubs को कभी भी named Docker volume में न रखें, क्योंकि तब वह वॉल्यूम सत्य का स्रोत (source of truth) बन जाता है और git में मौजूद कॉपी चुपचाप गलत हो जाती है।

एक और बात, जो अक्सर लोगों को फँसाती है। यदि आप प्रॉक्सी के माध्यम से वास्तविक ट्रैफ़िक को रिकॉर्ड करके stubs बनाते हैं, तो कमिट करने से पहले हर जनरेट की गई फ़ाइल को पढ़ें। एक रिकॉर्डिंग में ठीक वही होता है जो अपस्ट्रीम ने वापस भेजा है, जिसमें bearer tokens और ग्राहकों के ईमेल पते शामिल हो सकते हैं। इसे कमिट करने से यह आपकी रिपॉजिटरी में स्थायी रूप से आ जाता है, क्योंकि git हटाए गए कंटेंट को इतिहास में सुरक्षित रखता है।

FAQ

API mock server और API test runner में क्या अंतर है?

एक mock server requests का उत्तर देता है। यह एक ऐसी dependency की जगह लेता है जिसे आप CI से call नहीं कर सकते, और यह कभी भी pass या fail की रिपोर्ट नहीं देता। एक API test runner आपकी अपनी service को requests भेजता है, responses पर assertions लागू करता है, एक call से values को अगली call में ले जाता है, और assertion fail होने पर non-zero exit code देता है। ये अलग-अलग समस्याओं का समाधान करते हैं, और एक सामान्य setup में दोनों एक साथ चलते हैं: runner आपकी service को call करता है जबकि आपकी service mock को call करती है।

क्या मैं किसी hosted CI runner से internal API को test कर सकता हूँ?

इसे expose किए बिना नहीं। एक hosted runner आपके network के बाहर स्थित होता है, इसलिए यह private address पर bind की गई service तक नहीं पहुँच सकता। आपके पास API को publish करने, tunnel चलाने, या public staging copy बनाए रखने के विकल्प हैं, और हर विकल्प एक ऐसी प्रणाली जोड़ता है जो fail हो सकती है या leak कर सकती है। उसी private network पर स्थित एक runner सीधे service को call करता है, जो कि टीमों द्वारा इस काम को self-host करने का मुख्य व्यावहारिक कारण है।

Mock stubs और API test suites कहाँ होने चाहिए?

Git में, उस service के बगल में जिसका वे वर्णन करते हैं। जो tools definitions को files के रूप में store करते हैं, जैसे कि WireMock की mappings/ directory, Mockoon की data file, Hurl files और Bruno का .bru folder, वे आपको code review और container rebuild की सुविधा देते हैं जिसमें कोई अतिरिक्त लागत नहीं आती। जो tools definitions को database या web UI में store करते हैं, उन्हें backup plan और export step की आवश्यकता होती है, और export वह हिस्सा है जिसे लोग तब तक भूल जाते हैं जब तक container पहले ही नष्ट न हो जाए।

Stub सही दिखने के बावजूद मेरा mock 404 क्यों return करता है?

WireMock केवल exact match होने पर ही stub serve करता है। एक unmatched request को 404 मिलता है जिसका body Request was not matched से शुरू होता है, जिसके बाद सबसे करीबी stub के साथ एक diff होता है, और वह diff उस field का नाम बताता है जो अलग है। इसके सामान्य कारणों में path के अंत में trailing slash, एक Content-Type header जिसे stub को आवश्यकता है लेकिन आपके client ने नहीं भेजा, variable segment के लिए stub को urlPathPattern की आवश्यकता होने पर urlPath का उपयोग, और एक body matcher जो payload के साथ मेल नहीं खाता, शामिल हैं। यह पुष्टि करने के लिए कि request mock तक पहुँची भी है या नहीं, सबसे पहले /__admin/requests की जाँच करें।

यदि मेरे पास staging environment है, तो क्या मुझे अभी भी mocks की आवश्यकता है?

हाँ, दो कारणों से। जिस upstream को आप control नहीं करते, उसकी staging copy भी down हो सकती है और आप पर rate limit लगा सकती है, इसलिए आपकी suite उन कारणों से fail हो जाती है जिनका आपके code से कोई लेना-देना नहीं है। यह उन responses को भी उत्पन्न नहीं कर सकती जिनकी आपको test करने के लिए सबसे अधिक आवश्यकता होती है, जैसे कि declined card या gateway timeout। एक mock उन्हें local network speed पर मांग पर return करता है, जो sandbox के मुकाबले मिनटों में चलने वाली suite को सेकंडों में बदल देता है। Release से पहले अंतिम जाँच के लिए staging रखें और CI में mocks का उपयोग करें।