سرویس Payment فیلد amount_irr را به amount تغییر می‌دهد. Unit testهای هر دو تیم سبزند، OpenAPI جدید معتبر است و E2E روی Staging هنوز Build قبلی را می‌بیند. چند ساعت بعد از Deploy، سرویس Refund مقدار undefined می‌خواند. شکست در منطق داخلی نبود؛ دو طرف دربارهٔ پیام مشترک توافق یکسانی نداشتند.

تست قراردادی یا Contract Testing هر Application را جداگانه بررسی می‌کند تا پیام‌هایی که می‌فرستد یا می‌گیرد با یک قرارداد مشترک سازگار باشند. در Pact، Consumer رفتار واقعی Client خود را مقابل Mock Provider تست و نیازهایش را منتشر می‌کند؛ Provider همان Interactionها را روی پیاده‌سازی واقعی خود Verify می‌کند. سپس Pact Matrix نشان می‌دهد کدام نسخه‌های دو طرف واقعاً با هم آزموده شده‌اند.

این راهنما Contract Testing را از Schema، Integration و E2E جدا می‌کند و جریان کامل Pact را از Consumer test، Matcher و Provider State تا Broker، Pending/WIP، can-i-deploy و record-deployment با یک مثال پرداخت ایرانی پیاده می‌کند.

خلاصهٔ اجرایی تست قراردادی با Pact

  • قرارداد، پیام قابل مشاهده در Boundary است؛ جای Business logic، Security، Performance یا E2E را نمی‌گیرد.
  • Consumer و Provider «نقش» هستند؛ یک Application می‌تواند هر دو نقش را داشته باشد.
  • Consumer test باید API client واقعی را اجرا کند، نه اینکه با fetch ساختگی Contract دلخواه بسازد.
  • Interaction فقط وقتی ارزش دارد که حذف آن بتواند یک Breakage واقعی Consumer را پنهان کند.
  • در Response به‌اندازهٔ نیاز Consumer Matcher بگذارید؛ Strictness اضافی قرارداد را شکننده می‌کند.
  • Provider State پیش‌شرط مستقل هر Interaction است؛ Workflow را با وابستگی میان Testها شبیه‌سازی نکنید.
  • Pact file را Artifact ساخته‌شده از Test بدانید، نه JSON دستی که جدا از کد ویرایش شود.
  • Consumer/Provider version را با commit SHA یکتا و branch منتشر کنید؛ Tagهای environment روش قدیمی‌اند.
  • Provider باید Contractهای main، deployed/released و WIP لازم را Verify و Result را منتشر کند.
  • Pending pact مانع شکستن Build Provider با Feature جدید می‌شود، اما Consumer ناسازگار هنوز نباید Deploy شود.
  • پیش از Deploy، can-i-deploy و پس از Deploy/Release، ثبت Environment را اجرا کنید.
  • برای Mobile، چند Consumer version هم‌زمان ممکن است Supported باشد؛ Provider باید با همه سازگار بماند.
  • Async message contract Payload/metadata را می‌سنجد، نه Routing، Ordering، Retry و DLQ واقعی Broker را.

Contract Testing چیست و چه ادعایی می‌سازد؟

Contract مجموعه‌ای از Interactionهای قابل مشاهده میان دو Application است. برای HTTP، Interaction شامل Request مورد انتظار و Response قابل قبول است. برای Messaging، Message payload و Metadata توافق‌شده بررسی می‌شود. ادعای Test محدود است:

نسخهٔ مشخص Consumer، پیام را به شکلی تولید/مصرف می‌کند که نسخهٔ مشخص Provider می‌تواند مطابق Contract آن را ارائه کند.

این ادعا دربارهٔ Availability شبکه، Performance زیر بار، Authorization کامل، ترتیب چند Event، سازگاری Database داخلی یا موفقیت Journey چندسرویسی نیست؛ آن Evidenceها باید در لایه‌های دیگر ساخته شوند.

Consumer و Provider برای HTTP

  • Consumer: Applicationی که HTTP Request را آغاز می‌کند؛ حتی اگر Data flow در Response برگردد.
  • Provider: Applicationی که Request را دریافت و Response را تولید می‌کند.

Consumer و Provider برای Event

  • Consumer: Applicationی که Message را می‌خواند/Handle می‌کند.
  • Provider/Producer: Applicationی که Message را تولید می‌کند.

Queue، Topic یا Event bus واسطه است. Message Pact عمداً روی Message تمرکز دارد و Mock queue عمومی نیست.

Contract فقط Schema نیست

Schema می‌گوید amount_irr integer و status string است. Consumer contract علاوه بر شکل، Interaction مورد استفاده را ثبت می‌کند: Method/Path/Header/Query/Status، Provider precondition و شاخه‌هایی از Response که Consumer واقعاً Handle می‌کند. با این حال Pact نباید به Functional test کامل Provider تبدیل شود.

مرز Contract با تست‌های دیگر

رویکرد سؤال اصلی چه چیزی جا می‌ماند؟
Schema conformance پیام با OpenAPI/JSON Schema سازگار است؟ نیاز واقعی و رفتار Client
Consumer-driven contract Provider نیازهای مشاهده‌شدهٔ Consumer را پشتیبانی می‌کند؟ منطق کامل، شبکه و Workflow
Unit/component منطق داخلی Component درست است؟ Boundary واقعی بین Codebaseها
Integration دو Component واقعی با Transport/Dependency کار می‌کنند؟ ممکن است کند و وابسته به محیط باشد
System/E2E Journey چندبخشی برای کاربر/کسب‌وکار کار می‌کند؟ تشخیص دقیق Boundary و Feedback سریع
Security Authentication/Authorization/Input/Threat کنترل شده؟ سازگاری همه Consumer versionها
Performance/Resilience SLO، Load و Failure mode چگونه است؟ معنای کامل Message برای Consumer

تست API رفتار Functional، Error handling، Security و Performance را گسترده‌تر می‌سنجد. تست یکپارچه‌سازی Transport و چند Component واقعی را پوشش می‌دهد. Contract test بخشی متمرکز از Strategy مرزی است، نه نام تازه‌ای برای تمام تست‌های API.

چه زمانی Pact انتخاب مناسبی است؟

  • Consumer و Provider در Codebase یا Pipelineهای مستقل تغییر می‌کنند.
  • چند Consumer با نیازهای متفاوت از یک Provider استفاده می‌کنند.
  • E2E دیر، پرهزینه یا تشخیص Failure در آن دشوار است.
  • تیم‌ها Deployment مستقل و compatibility gate می‌خواهند.
  • HTTP یا Message interaction مشخص و قابل Isolation است.
  • Provider می‌تواند نسخهٔ واقعی خود را محلی/CI Verify و State لازم را بسازد.
  • نسخه‌های Mobile یا On-prem مدت طولانی در میدان می‌مانند.

چه زمانی Pact به‌تنهایی جواب نمی‌دهد؟

  • Third-party Provider تحت کنترل شما verification اجرا نمی‌کند؛ Consumer test و Sandbox/Schema هنوز مفید است، اما Matrix دوطرفه ناقص می‌ماند.
  • مسئلهٔ اصلی Routing، ACL، Topic config، Retry، ordering یا exactly-once semantics واقعی است.
  • Failure فقط در Transaction چندسرویسی، Database مشترک یا Timing پدیدار می‌شود.
  • Public API باید با Contract رسمی Provider-driven و طیف ناشناخته‌ای از Clientها مدیریت شود.
  • تیم هنوز نسخهٔ Artifact/branch/deployment را قابل ردیابی نمی‌کند؛ Broker دادهٔ گمراه‌کننده خواهد ساخت.

در معماری‌های چندسرویسی، Contract یکی از لایه‌های استراتژی تست میکروسرویس‌ها است؛ جای Saga، idempotency، trace و Production verification را نمی‌گیرد.

واژه‌نامهٔ Pact

اصطلاح معنا
Pacticipant Application دارای Version؛ ممکن است Consumer و Provider باشد
Interaction یک Request/Response یا Message با Description و Provider State
Pact Contract بین یک Consumer و Provider شامل Interactionها
Pact specification فرمت استاندارد Contract برای interoperability میان زبان‌ها
Consumer version نسخهٔ Code/Artifact که Pact را تولید کرده است
Provider version نسخهٔ Code/Artifact که Contract را Verify کرده است
Provider State پیش‌شرط Provider برای Verify یک Interaction
Matcher قاعدهٔ تطبیق Exact/Type/Regex/Array و مانند آن
Pact Broker Exchange قرارداد، Result، Version، Branch، Environment و Matrix
Pact Matrix جدول Result سازگاری جفت Versionهای Consumer/Provider
Pending/WIP قراردادهای جدید/درحال‌کار که هنوز در Branch Provider پشتیبانی نشده‌اند
can-i-deploy پرس‌وجوی Gate از Matrix برای Environment هدف

چرخه کامل Pact از کد تا Deployment

  1. Consumer یک Interaction را از رفتار Client واقعی استخراج می‌کند.
  2. Pact Mock Provider، Request تولیدشده را Check و Response نمونه را برمی‌گرداند.
  3. Consumer assertion می‌کند Client آن Response را درست Handle کرده است.
  4. Pact file فقط در صورت موفقیت Test تولید می‌شود.
  5. Consumer CI آن را با Consumer name، commit SHA و branch به Broker Publish می‌کند.
  6. Provider CI Contractهای لازم را با Version selector دریافت می‌کند.
  7. برای هر Interaction، Provider State مستقل ساخته و Request روی Provider واقعی Replay می‌شود.
  8. Verification result با Provider SHA و branch به Broker Publish می‌شود.
  9. هر دو Pipeline پیش از Deploy، can-i-deploy را برای Environment هدف اجرا می‌کنند.
  10. پس از Deploy یا Release موفق، نسخه در Broker ثبت می‌شود.

اگر مرحلهٔ ۱۰ حذف شود، Broker نمی‌داند واقعاً کدام نسخه در Production است و پاسخ Environment-aware به can-i-deploy قابل اتکا نیست.

مرحله اول: Consumer Test درست بنویسید

API client واقعی را اجرا کنید

Contract باید Side effect یک Unit test خوب برای Client باشد. اگر Test مستقیماً با یک HTTP library عمومی Request می‌سازد ولی Application شما Client دیگری دارد، Contract ممکن است سبز باشد و Serialization/Header/Error handling واقعی Consumer خراب بماند.

Interaction را از Branch رفتار بسازید

اگر Consumer برای approved، rejected و pending رفتار متفاوت دارد، هر سه Contract value دارند. اگر فیلدی را هیچ Code path نمی‌خواند، Exact match کردن آن فقط Coupling می‌سازد.

نمونه Pact JS V4 برای Refund Client

import {
  PactV4,
  MatchersV3,
  SpecificationVersion
} from "@pact-foundation/pact";
import { RefundClient } from "../src/refund-client";

const { like, regex } = MatchersV3;

const pact = new PactV4({
  consumer: "refund-service",
  provider: "payment-api",
  spec: SpecificationVersion.SPECIFICATION_VERSION_V4
});

test("reads a settled payment in IRR", async () => {
  await pact
    .addInteraction()
    .given("a settled payment exists", { paymentId: "pay-100" })
    .uponReceiving("a request for refundable payment")
    .withRequest("GET", "/payments/pay-100", (request) => {
      request.headers({ Accept: "application/json" });
    })
    .willRespondWith(200, (response) => {
      response.headers({ "Content-Type": "application/json" });
      response.jsonBody({
        id: like("pay-100"),
        status: regex("settled|partially_refunded", "settled"),
        refundable_amount_irr: like(150000)
      });
    })
    .executeTest(async (mockServer) => {
      const client = new RefundClient(mockServer.url);
      const payment = await client.getPayment("pay-100");

      expect(payment.currencyUnit).toBe("IRR");
      expect(payment.refundableAmount).toBe(150000);
    });
});

نام Class/DSL میان زبان و نسخه فرق دارد؛ نسخهٔ دقیق Library را Pin و نمونه را با مستندات همان نسخه تطبیق دهید. Contract file را دستی ویرایش نکنید.

Request و Response را متفاوت سخت‌گیر کنید

  • Request: Consumer کنترلش می‌کند؛ Method/Path/Header/Body مهم را دقیق Check کنید.
  • Response: فقط آنچه Consumer برای سازگاری لازم دارد Match کنید؛ اضافه‌شدن Field معمولاً نباید Break باشد.
  • Business constant: اگر مقدار دقیق Branch رفتار را عوض می‌کند، Exact match یا مجموعهٔ محدود لازم است.
  • Dynamic value: برای ID/Time از Type/Regex matcher استفاده کنید، نه مقدار تصادفی.

Random data را وارد Pact نکنید

Broker Contract content را Hash می‌کند. Random UUID/timestamp می‌تواند Contract ظاهراً جدید بسازد، Duplicate detection و Matrix را آلوده کند. Example پایدار و Matcher معنادار استفاده کنید.

Matcher؛ نه خیلی سخت، نه بی‌اثر

نیاز Consumer Matcher مناسب ضدالگو
String با هر مقدار Type/like Exact UUID نمونه
Enum اثرگذار Regex/allowed values یا Interaction جدا هر String
Array همگن eachLike با min لازم Exact length بی‌دلیل
Array دارای Variant array-containing/variant match فرض یک Shape برای همه
Header ضروری Exact/regex مطابق semantics Match تمام Headerهای Infrastructure
عدد مالی Integer/type + Consumer assertion Number آزاد وقتی اعشار Break می‌کند

Matcher بسیار Loose، تغییر شکننده را عبور می‌دهد؛ Matcher بسیار Strict، تغییر سازگار Provider را Block می‌کند. سؤال Review این است: «آیا این Rule از یک وابستگی واقعی Code محافظت می‌کند؟»

Provider State؛ پیش‌شرط مستقل، نه Script سرتاسری

Provider State مانند Given است: «Payment با شناسهٔ مشخص settled است». Provider team Handler آن را پیاده می‌کند و قبل از هر Interaction داده/Stub لازم را می‌سازد. هر Interaction باید مستقل باشد و Context از Test قبلی نگیرد.

const stateHandlers = {
  "a settled payment exists": {
    setup: async (params) => {
      await paymentRepo.reset();
      await paymentRepo.insert({
        id: params.paymentId,
        status: "settled",
        amount_irr: 150000
      });
    },
    teardown: async () => {
      await paymentRepo.reset();
    }
  }
};

دادهٔ Versioned و ایزوله برای Stateها مهم است؛ Factory/Manifestهای قابل‌بازتولید در راهنمای تولید داده تست آمده است.

State را پارامتری و دامنه‌ای نام‌گذاری کنید

  • خوب: a settled payment exists با paymentId پارامتر؛
  • ضعیف: insert row 42 into payments؛ Coupling به implementation؛
  • ضعیف: consumer asks for payment؛ State Consumer نیست؛
  • خطرناک: State مشترک و Mutating بدون Teardown.

Dependencyهای Provider را کنترل کنید

Provider verification باید Provider واقعی را اجرا کند، اما Dependency پایین‌دستی آن می‌تواند Stub یا Contract-tested باشد تا Test سریع و deterministic بماند. اگر کل Ecosystem را بالا می‌آورید، Contract test را دوباره به Integration environment شکننده تبدیل کرده‌اید.

مرحله دوم: Provider Verification

import { Verifier } from "@pact-foundation/pact";

await new Verifier({
  provider: "payment-api",
  providerBaseUrl: "http://127.0.0.1:8080",
  pactBrokerUrl: process.env.PACT_BROKER_URL,
  pactBrokerToken: process.env.PACT_BROKER_TOKEN,

  consumerVersionSelectors: [
    { mainBranch: true },
    { deployedOrReleased: true }
  ],
  enablePending: true,
  includeWipPactsSince: "2026-07-01",

  providerVersion: process.env.GIT_SHA,
  providerVersionBranch: process.env.GIT_BRANCH,
  publishVerificationResult: process.env.CI === "true",

  stateHandlers
}).verifyProvider();

این Config نمونه است؛ Support دقیق selector/option را در نسخهٔ Pact client و Broker خود بررسی کنید. Result رسمی را فقط از CI قابل ردیابی Publish کنید، نه Laptop توسعه‌دهنده با Code یا Dependency نامعلوم.

کدام Contractها Verify شوند؟

  • آخرین Contract شاخهٔ اصلی Consumer؛
  • نسخه‌های Consumer که در Environmentها Deploy یا Release و هنوز Supported هستند؛
  • WIP Contractهای جدید در بازهٔ سیاست‌شده؛
  • Contract feature branch در Pipeline مرتبط، در صورت workflow تیم؛
  • همهٔ نسخه‌های Mobile/On-prem پشتیبانی‌شده، نه فقط latest.

Selector ناقص می‌تواند Build سبز و Production ناسازگار بسازد. تعداد Contract بیشتر هدف نیست؛ مجموعه باید مطابق واقعیت Deployment و Support باشد.

Auth و Secret

Token کوتاه‌عمر را داخل Pact file ننویسید. در Provider verification می‌توان Request filter یا Hook محدود استفاده کرد، اما مستندات Pact هشدار می‌دهند تغییر Request هنگام verification ممکن است Contract را عوض کند. فقط داده‌ای را Inject کنید که ذاتاً قابل Persist در Contract نیست و رفتار را Audit کنید.

Pact Broker؛ بیش از مخزن JSON

Broker این داده‌ها را کنار هم قرار می‌دهد:

  • Application/Pacticipant و Version؛
  • Contract content و Consumer version؛
  • Provider verification result و Provider version؛
  • Branch و main branch؛
  • Environment، Deployment، Release و Support end؛
  • Pending/WIP status، Webhook و Matrix؛
  • API documentation حاصل از Contractهای Verifyشده.

Contract content version را Broker مدیریت می‌کند؛ شما باید Application version را درست بسازید.

Version = Codebase reference یکتا

بهترین انتخاب معمولاً commit SHA یا Versionی شامل SHA است. Ruleها:

  • هر Deployable artifact یک Version یکتا دارد.
  • همان Code در نقش Consumer و Provider با همان Version منتشر می‌شود.
  • Version پیش از Release معلوم و به Commit قابل برگشت است.
  • Feature branch نمی‌تواند Version branch دیگر را overwrite کند.
  • Build number تنها، پس از جابه‌جایی CI یا Parallel branch قابل ابهام است.

Branch، Tag و Environment را مخلوط نکنید

در Brokerهای جاری، Branch و Environment مفهوم مستقل دارند. هنگام Publish، branch واقعی را ثبت کنید؛ پس از Deploy، environment را با record-deployment و برای Artifactهای چندنسخه‌ای مانند Mobile با record-release ثبت کنید. روش قدیمی استفاده از Tag برای branch/environment هنوز ممکن است پشتیبانی شود، اما برای طراحی جدید انتخاب اصلی نیست.

Pending و WIP بدون شکستن تیم Provider

سناریو: Consumer feature branch Interaction تازه‌ای می‌خواهد که Provider main هنوز ندارد. اگر هر Contract جدید فوراً Build Provider را Fail کند، Consumer می‌تواند Delivery مستقل Provider را متوقف کند.

  • Pending: Verification Failure ثبت می‌شود، اما Build معمول Provider به‌دلیل Contract تازه و هنوز پشتیبانی‌نشده Fail نمی‌شود.
  • WIP: Contractهای مرتبطی که Provider صریح انتخاب نکرده ولی نیازمند Verification هستند وارد Build می‌شوند.
  • Consumer gate: Consumer ناسازگار همچنان از can-i-deploy پاسخ منفی می‌گیرد تا Provider آن را پشتیبانی کند.

Pending به معنی «Failure را نادیده بگیر» نیست؛ فقط Ownership و ترتیب Delivery را درست می‌کند. Age قرارداد Pending و Owner آن را پایش کنید.

can-i-deploy چگونه کار می‌کند؟

Matrix جفت Versionهای Consumer/Provider و Result verification آن‌ها را دارد. can-i-deploy برای Environment هدف می‌پرسد آیا Version موردنظر با Versionهای Applicationهای درحال استفاده در آن Environment Result موفق دارد.

pact-broker can-i-deploy   --pacticipant refund-service   --version "$GIT_SHA"   --to-environment production

پس از Deployment موفق و زمانی که نسخهٔ قبلی دیگر فعال نیست:

pact-broker record-deployment   --pacticipant refund-service   --version "$GIT_SHA"   --environment production

برای Mobile/Library/On-prem که نسخه‌های متعدد هم‌زمان Release و Supported هستند، record-release و پایان Support semantics متفاوتی دارد.

چه چیزهایی پاسخ can-i-deploy را بی‌اعتبار می‌کند؟

  • Version تکراری یا غیرقابل نگاشت به Commit؛
  • عدم انتشار verification result؛
  • عدم ثبت Deployment/Release واقعی؛
  • Environment با معنای متفاوت میان تیم‌ها؛
  • Selectorهایی که Consumerهای Production را حذف می‌کنند؛
  • Gate اختیاری یا Bypass بدون Waiver و Expiry؛
  • Deploy ثبت‌شده قبل از اتمام واقعی Rolling deployment.

این Gate بخشی از طراحی Continuous Testing در CI/CD است؛ جای Approval امنیت، Migration و Observability را نمی‌گیرد.

Pipeline پیشنهادی Consumer و Provider

Consumer CI

  1. Unit/consumer Pact test روی Client واقعی؛
  2. Contract lint/review و عدم Secret/Random؛
  3. Publish Pact با SHA و branch؛
  4. Trigger یا انتظار Provider verification؛
  5. can-i-deploy برای Environment؛
  6. Deploy/Release؛
  7. ثبت Deployment/Release فقط پس از موفقیت.

Provider CI

  1. Unit/functional/component tests؛
  2. Start Provider محلی و deterministic؛
  3. دریافت main + deployed/released + WIP Contractها؛
  4. Provider State setup/teardown برای هر Interaction؛
  5. Verification و Publish Result با SHA/branch؛
  6. can-i-deploy؛
  7. Deploy و ثبت Environment؛

Webhook؛ شتاب‌دهنده، نه تنها مسیر

Publish Contract می‌تواند Verification Provider را Trigger کند، اما Provider main pipeline نیز باید Contractهای لازم را Verify کند. Webhook failure، rate limit یا network نباید Matrix را برای مدت نامعلوم stale نگه دارد؛ scheduled/release verification و alert داشته باشید.

تغییر سازگار و ناسازگار در API

تغییر معمولاً شرط
افزودن Response field سازگار Consumer unknown field را تحمل کند
افزودن optional Request field سازگار Provider absence را بپذیرد
حذف/rename field مصرف‌شده ناسازگار Migration مرحله‌ای لازم
محدودکردن Enum ممکن است ناسازگار Consumer چه Valueهایی می‌فرستد؟
افزودن Enum Response ممکن است ناسازگار Consumer unknown/default را Handle می‌کند؟
String→Number ناسازگار حتی اگر مقدار ظاهراً یکی باشد
تغییر Status code وابسته Branch رفتار Consumer را بررسی کنید
تغییر Header/Auth اغلب ناسازگار Request واقعی Client Contract شود
مرتب‌سازی Array وابسته Consumer order را مصرف می‌کند یا نه؟

تغییر «افزودنی» همیشه امن نیست

افزودن Enum جدید به Response از دید Schema افزودنی است، اما Consumer با switch بدون default می‌تواند Crash کند. Contract باید Branchهای واقعی Client را بسنجد، نه فقط Shape JSON.

Migration ایمن field در سامانه پرداخت

می‌خواهیم refundable_amount_irr را به ساختار تازهٔ refundable_amount: { value, unit } منتقل کنیم.

مرحله ۱: Consumer تحمل دو شکل را یاد می‌گیرد

Refund service ابتدا Client خود را تغییر می‌دهد تا هر دو Representation را بخواند، اما Contract Production فعلی را حفظ می‌کند. Consumer testهای هر دو مسیر اضافه و Version feature branch Publish می‌شود.

مرحله ۲: Provider شکل تازه را Add می‌کند

Payment API فیلد جدید را کنار قدیمی برمی‌گرداند. Provider verification باید با Contractهای Consumer قدیمی Deploy/Released و Consumer جدید موفق شود. هیچ Fieldی هنوز حذف نمی‌شود.

مرحله ۳: Consumer استفاده از قدیمی را قطع می‌کند

Refund service فقط شکل جدید را مصرف و Deploy می‌شود. Broker Deployment را ثبت می‌کند. سایر Consumerها، مخصوصاً Mobile/Reporting، جدا بررسی می‌شوند.

مرحله ۴: Support window تمام می‌شود

وقتی هیچ Consumer نسخهٔ Supported/Deployed Contract قدیمی را نیاز ندارد، Payment API حذف Field را در branch انجام می‌دهد، Matrix سبز می‌شود و سپس Deploy می‌کند. برای Mobile، صرف گذشت زمان کافی نیست؛ Releaseهای Supported باید پایان Support ثبت‌شده داشته باشند.

چرا این روش از هماهنگی Big Bang بهتر است؟

هر مرحله deployable و rollbackable است، سازگاری forward/backward قابل مشاهده می‌ماند و تیم‌ها لازم نیست دقیقاً هم‌زمان Release کنند.

Contract Testing برای Event-Driven Systems

Pact از Asynchronous message و در برخی Implementationها/Pluginها از Interactionهای دیگر پشتیبانی می‌کند. الگو:

  • Consumer Handler واقعی را با Message نمونه و Matcherها Test می‌کند.
  • Contract payload/metadata تولید و Publish می‌شود.
  • Provider function واقعی برای تولید Message اجرا می‌شود.
  • Message واقعی با Contract Verify و Result Publish می‌شود.

چه چیزهایی هنوز Test نشده‌اند؟

  • Topic/queue name و ACL واقعی؛
  • Serialization تنظیم‌شده در Broker/Connector؛
  • Partitioning و ordering بین Messageها؛
  • Retry/backoff، duplicate delivery و DLQ؛
  • Consumer lag، throughput و retention؛
  • Saga و side effect چندسرویسی.

برای این موارد Integration/Component و System test لازم است. Message Pact را «تست Kafka/RabbitMQ» ننامید؛ Transport واقعی معمولاً حضور ندارد.

امنیت، حریم خصوصی و Pact Broker

  • Token، Cookie، شماره کارت، PII و Secret واقعی را در Pact example ننویسید.
  • Broker token را Secret store نگه دارید و Scope/rotation تعریف کنید.
  • TLS، backup، access control، audit و retention Broker را عملیاتی کنید.
  • Webhook target و credential را محدود و SSRF/egress policy را بررسی کنید.
  • Contract ممکن است Endpoint، Header و مدل دادهٔ حساس را افشا کند؛ دسترسی عمومی فرض نشود.
  • Provider State endpoint یا Handler فقط در Test boundary و با کمترین دسترسی باشد.
  • Request filter نباید Authorization defect را پنهان کند.
  • Contract pass جای تست امنیت را نمی‌گیرد.

اجرای Pact برای تیم‌های ایرانی

  • قبل از وابستگی سازمانی، دسترسی Package registry، Docker image و hosted Broker را از شبکهٔ واقعی PoC کنید.
  • برای Self-hosted Broker، Database backup، upgrade، TLS و monitoring owner داشته باشید.
  • Mirror/cache قابل اعتماد و pin نسخه برای CLI/Library/Rust core آماده کنید.
  • Hosted service را از نظر پرداخت، تحریم، data residency، export و exit plan بسنجید.
  • Contract exampleها را با IRR، fa-IR، Unicode و timezone واقعی Consumer ولی دادهٔ ساختگی بنویسید.
  • Pipeline در قطعی Broker باید fail-closed، retry یا emergency waiver مشخص داشته باشد؛ silently pass ممنوع.
  • Waiver شامل Scope، owner، علت، Risk، expiry و verification بعدی باشد.

انتخاب Pact در Strategy اتوماسیون

Contract suite باید Owner و Maintenance economics داشته باشد. در استراتژی اتوماسیون تست، Contract را در لایه‌ای بگذارید که قبل از E2E و در زمان تصمیم Release Signal بدهد. اگر تیم Tool را هنوز انتخاب نکرده است، Port زبان، Specification support، Plugin maturity، Broker compatibility، CI و self-host/hosted را طبق فرایند PoC ابزار تست بسنجید.

معیارهای PoC Pact

معیار آزمایش
Client language support یک HTTP و یک Error interaction واقعی
Provider integration State setup/teardown و Auth کوتاه‌عمر
Async/Protocol Message/plugin مورد نیاز با metadata
Version/branch دو feature branch و main بدون overwrite
Matrix Compatible/incompatible pair و can-i-deploy
Migration add→consume→remove field با نسخه‌های قدیمی
Operations Broker backup/restore/upgrade و outage
Security token scope، no-secret scan، TLS و audit
Iran viability install/mirror/payment/export/offline path

متریک‌های سالم Contract Testing

  • Pre-deploy incompatibility detection: ناسازگاری‌های واقعی که قبل از Environment هدف گرفته شدند.
  • Verification freshness: سن آخرین Result موفق برای Versionهای Deployed/Released.
  • Pending/WIP age: زمان Contract پشتیبانی‌نشده با Owner.
  • can-i-deploy bypass: تعداد Waiverها، Scope و expiry؛ هدف پنهان‌کردن صفر نیست.
  • Provider state failure: Data/fixture failure جدا از Contract mismatch.
  • Contract churn: تغییر محتوا بدون تغییر نیاز Consumer؛ علامت Random/Brittle test.
  • Unsupported consumer exposure: Consumerهای میدان که Contract/Support status نامعلوم دارند.
  • Time to compatible: از Publish نیاز تازه تا Verification موفق Provider.
  • Escaped integration break: Incidentهایی که Contract باید می‌گرفت و علت Gap.

تعداد Interaction یا درصد Endpoint contract-tested هدف مناسبی نیست؛ یک Endpoint ممکن است ده‌ها Contract کم‌ارزش داشته باشد و یک Boundary مالی مهم بدون Contract بماند.

برنامه ۳۰ روزه استقرار Pact

هفته اول: Boundary و Pilot

  • یک Consumer/Provider با Failure history و Ownership روشن انتخاب کنید.
  • Interactionهای Bug-catcher و خارج از Scope را بنویسید.
  • Version=SHA، branch، naming و no-secret rule را استاندارد کنید.

هفته دوم: Consumer و Provider

  • سه Branch رفتار Client را با Matcher مناسب Contract کنید.
  • Provider Stateهای پارامتری، مستقل و cleanupدار بسازید.
  • Verification محلی و CI را روی Provider واقعی اجرا کنید.

هفته سوم: Broker و Matrix

  • Publish Consumer/Provider Result با SHA/branch را فعال کنید.
  • main/deployedOrReleased selector، Pending و WIP را Pilot کنید.
  • can-i-deploy را Dry-run و سپس Gate کنید.
  • Deployment/Release را پس از موفقیت ثبت کنید.

هفته چهارم: Migration و Operations

  • یک تغییر additive→consumer adoption→removal را تمرین کنید.
  • Broker outage، rollback، backup/restore و token rotation را تمرین کنید.
  • Metrics اولیه، Runbook، owner و Scale/Stop criteria را ثبت کنید.

ضدالگوهای رایج Pact

  • Contract JSON دستی: از رفتار Client واقعی جدا می‌شود.
  • تست با fetch خام: Consumer code اصلاً آزمایش نشده است.
  • Exact match همه Response: تغییر سازگار Provider Buildها را می‌شکند.
  • Matcher برای همه چیز: تغییر شکننده عبور می‌کند.
  • Functional suite در Pact: Interactionها زیاد، کند و بی‌معنا می‌شوند.
  • Provider State زنجیره‌ای: ترتیب Test و Shared state Flakiness می‌سازد.
  • Random data: Contract churn و Matrix noise ایجاد می‌کند.
  • فقط latest Consumer: Mobile/Production version قدیمی می‌شکند.
  • Tag=branch=environment: Broker semantics مبهم می‌شود.
  • Version دستی تکراری: Result برای Code اشتباه Attribution می‌شود.
  • Publish از Laptop: Verification غیرقابل ردیابی وارد Matrix می‌شود.
  • can-i-deploy بدون record-deployment: Environment هدف اشتباه فهمیده می‌شود.
  • Pending یعنی ignore: Consumer ناسازگار بی‌صدا Deploy می‌شود.
  • Message Pact مساوی Broker test: Routing/DLQ/ordering پوشش نمی‌گیرد.
  • Contract pass مساوی Safe release: Security، Data، Migration و Runtime evidence حذف می‌شود.

چک‌لیست نهایی Contract Testing

Consumer

  • API client/handler واقعی Test می‌شود.
  • هر Interaction از Breakage واقعی محافظت می‌کند.
  • Request دقیق و Response فقط به‌اندازه نیاز Strict است.
  • Provider State دامنه‌ای، پارامتری و مستقل است.
  • Example پایدار و بدون Secret/PII/Random است.
  • Pact با SHA یکتا و branch منتشر می‌شود.

Provider

  • Provider واقعی محلی/CI اجرا و Dependencyها کنترل شده‌اند.
  • State setup/teardown ایزوله و قابل بازتولید است.
  • main، deployed/released و WIP لازم Verify می‌شود.
  • Result فقط از CI با SHA/branch منتشر می‌شود.
  • Contract mismatch از State/Data/Environment failure جداست.

Broker و Delivery

  • main branch و naming Pacticipant درست است.
  • Version به Commit/Artifact واحد نگاشت می‌شود.
  • Pending/WIP policy و Age/Owner وجود دارد.
  • can-i-deploy پیش از هر Deploy/Release Gate است.
  • Deployment/Release/Support end پس از رخداد واقعی ثبت می‌شود.
  • Mobile/On-prem versionهای Supported در Matrix هستند.
  • Broker امنیت، backup، upgrade، monitoring و exit plan دارد.
  • Bypass fail-closed و Waiver زمان‌دار است.

جمع‌بندی

Pact وقتی ارزش می‌سازد که یک «فایل قرارداد» نباشد، بلکه یک حلقهٔ Version-aware باشد: Consumer code نیاز واقعی را Test می‌کند، Provider همان Interaction را روی Implementation خود Verify می‌کند، Broker جفت Versionها و Environment را می‌فهمد و Pipeline پیش از Deploy سازگاری را می‌پرسد.

از یک Boundary مالی کوچک شروع کنید. Contract را Loose-but-safe بنویسید، State را مستقل کنید، SHA/branch/deployment را درست ثبت کنید و یک Field migration را تا حذف امن تمرین کنید. اگر Matrix نتواند بگوید کدام Code با کدام Code و در کدام Environment آزموده شده، هنوز Contract Testing عملیاتی نشده است.

سؤالات متداول

تست قراردادی API چیست؟

تکنیکی است که Consumer و Provider را جدا آزمایش می‌کند تا Request/Response یا Messageهایشان با فهم مشترک ثبت‌شده در Contract سازگار باشد. این Test منطق کامل، شبکه و Journey سرتاسری را تضمین نمی‌کند.

تفاوت Pact با OpenAPI چیست؟

OpenAPI یک Contract/Schema عمدتاً Provider-facing برای توصیف API است. Pact از Test رفتار Client واقعی، نیازهای Consumer را تولید و روی Provider Verify می‌کند و Compatibility نسخه‌ها را در Matrix نگه می‌دارد. این دو می‌توانند مکمل باشند.

آیا Pact جای Integration و E2E را می‌گیرد؟

خیر. می‌تواند بسیاری از Testهای Boundary سنگین را کم کند، اما Transport واقعی، Database integration، Routing، Saga، Security، Performance و Journeyهای حیاتی هنوز Evidence مستقل می‌خواهند.

Pact Broker چیست و چرا can-i-deploy مهم است؟

Broker Contract، Verification، Version، Branch، Environment و Matrix را نگه می‌دارد. can-i-deploy قبل از Deploy بررسی می‌کند Version جدید با Versionهای واقعاً موجود/Supported در Environment هدف Result موفق دارد؛ به شرط اینکه Deployment/Releaseها درست ثبت شده باشند.

برای تغییر ناسازگار API چه کنیم؟

Migration را Expand→Adopt→Contract انجام دهید: Provider شکل جدید را کنار قدیمی اضافه کند، Consumerها آن را بپذیرند و Deploy/Release شوند، سپس بعد از پایان Support همه نسخه‌های نیازمند، شکل قدیمی حذف شود. Matrix باید در هر مرحله سبز بماند.

منابع معتبر برای مطالعه بیشتر

دیدگاهتان را بنویسید