Contract Testing یکی از تکنیکهای مهم در تست نرم افزار است که برای بررسی سازگاری و تعامل صحیح بین سرویسها و اجزای مختلف یک سیستم استفاده میشود. اگر در حال یادگیری تست نرم افزار هستید، آشنایی با این تکنیک میتواند درک بهتری از تست در معماریهای مدرن به شما بدهد.
این موضوع بهخصوص در معماریهایی مانند Microservices اهمیت پیدا میکند؛ جایی که سرویسهای مختلف بهصورت مستقل توسعه داده میشوند، ممکن است توسط تیمهای متفاوتی نگهداری شوند و حتی بهصورت مستقل Deploy شوند.
در چنین محیطی، یک تغییر کوچک در API یا رفتار یک سرویس میتواند باعث شود سرویس دیگری که به آن وابسته است، دیگر نتواند بهدرستی کار کند.
Contract Testing با ایجاد یک قرارداد قابل تست بین سرویسها کمک میکند این نوع مشکلات را زودتر شناسایی کنیم؛ ترجیحاً قبل از اینکه تغییرات به محیطهای بالاتر یا Production برسند.
Contract Testing چیست؟ 🧩
برای درک Contract Testing ابتدا باید مفهوم Contract را در یک سیستم نرمافزاری بشناسیم.
فرض کنید یک سرویس به سرویس دیگری درخواست ارسال میکند و انتظار دارد پاسخ مشخصی دریافت کند.
مثلاً:
Order Service
↓
GET /users/123
↓
User Service
Expected Response:
{
"id": 123,
"name": "John"
}
در اینجا Order Service به رفتار مشخصی از User Service وابسته است.
Contract Testing بررسی میکند که این توافق یا قرارداد بین دو طرف همچنان برقرار است.
به بیان ساده:
Contract Testing بررسی میکند که Consumer و Provider همچنان میتوانند مطابق قراردادی که بین آنها وجود دارد با یکدیگر تعامل کنند.
Consumer و Provider چه هستند؟ 👥
در Contract Testing معمولاً با دو نقش اصلی سروکار داریم:
- Consumer: سرویسی که از API یا قابلیت سرویس دیگری استفاده میکند.
- Provider: سرویسی که API یا قابلیت موردنیاز Consumer را ارائه میدهد.
مثلاً در این معماری:
Order Service
↓
User Service
اگر Order Service از User Service اطلاعات دریافت کند:
Consumer → Order Service
Provider → User Service
بنابراین Contract بین این دو سرویس مشخص میکند که Consumer چه چیزی از Provider انتظار دارد و Provider باید چه رفتاری را ارائه کند.
Contract دقیقاً شامل چه چیزی است؟ 📄
Contract میتواند بخشهای مختلفی از تعامل بین Consumer و Provider را مشخص کند؛ برای مثال:
- HTTP Method
- Endpoint
- Request Parameters
- Request Body
- Response Status Code
- Response Body
- Data Types
- برخی رفتارهای مورد انتظار
برای مثال:
GET /users/123
Expected:
Status: 200
Response:
{
"id": 123,
"name": "John",
"status": "ACTIVE"
}
در این حالت Consumer انتظار دارد Provider بتواند این Interaction را مطابق Contract پاسخ دهد.
چرا Contract Testing در Microservices مهمتر میشود؟ 🏗️
در یک Monolith ساده، بسیاری از اجزای سیستم در یک Application قرار دارند و معمولاً تغییرات در یک Repository و یک Deployment مدیریت میشوند.
Application
└── Everything Together
اما در معماری Microservices، سیستم به سرویسهای مختلفی تقسیم میشود که ممکن است هرکدام Repository، تیم، Release و Version مستقل خود را داشته باشند.
Service A ──► Service B
│ │
▼ ▼
Service C ──► Service D
در چنین معماریای، هر سرویس ممکن است:
- Repository جداگانه داشته باشد.
- توسط تیم متفاوتی توسعه داده شود.
- بهصورت مستقل Release شود.
- Version مستقل داشته باشد.
- حتی با تکنولوژی متفاوتی توسعه داده شده باشد.
در نتیجه، Compatibility بین سرویسها اهمیت بسیار بیشتری پیدا میکند.
Contract Testing در Distributed Systems 🌐
در یک Distributed System، بسیاری از مشکلات در مرز بین سیستمها و سرویسهای مختلف اتفاق میافتند.
برای مثال، فرض کنید یک Consumer انتظار دارد مقدار amount از نوع عدد باشد، اما Provider مقدار آن را بهصورت String ارسال کند.
Service A
│
│ Expected:
│ amount = number
▼
Service B
│
│ Actual:
│ amount = string
▼
Compatibility Failure
ممکن است هر دو سرویس بهتنهایی بدون مشکل کار کنند، اما زمانی که با یکدیگر ارتباط برقرار میکنند، Integration دچار مشکل شود.
Contract Testing دقیقاً برای شناسایی چنین مشکلاتی در مرز بین Consumer و Provider بسیار مناسب است.
آیا Contract Testing همه مشکلات Integration را پیدا میکند؟ ⚠️
خیر. این یکی از نکات مهمی است که باید درباره Contract Testing بدانیم.
فرض کنید Contract بین Consumer و Provider کاملاً موفق باشد:
Consumer
↓
Provider
↓
Contract
↓
PASS ✅
با این حال ممکن است در زمان اجرای واقعی سیستم مشکلات دیگری رخ دهند.
Consumer
↓
Provider
↓
Database Timeout
یا ممکن است یک سرویس وابسته به یک Third-party API باشد که در محیط واقعی در دسترس نباشد.
Provider
↓
Third-party API unavailable
همچنین مشکلاتی مانند Network Latency و Timeout نیز لزوماً توسط Contract Testing شناسایی نمیشوند.
بنابراین Contract Testing باید بخشی از یک استراتژی تست جامع باشد و نباید انتظار داشته باشیم که تمام مشکلات Runtime و Integration را بهتنهایی پیدا کند.
بهترین نگاه به Contract Testing 🎯
بهتر است Contract Testing را بهعنوان «بهترین نوع تست برای Microservices» در نظر نگیریم.
Contract Testing یکی از بهترین تکنیکها برای مدیریت ریسک Compatibility بین سرویسهای مستقل است.
این تعریف دقیقتر است، زیرا Contract Testing قرار نیست جایگزین تمام تکنیکهای دیگر Testing شود؛ بلکه یک لایه مشخص از ریسک را هدف قرار میدهد: ناسازگاری بین اجزایی که با یکدیگر ارتباط دارند.
🧪 نتیجه برای QA Engineer
یک QA Engineer حرفهای نباید فقط بداند:
«چطور Pact را اجرا کنم؟»
بلکه باید بتواند تشخیص دهد:
چه چیزی را باید Contract کنیم؟
و حتی مهمتر:
چه چیزی را نباید Contract کنیم؟
این تفاوت بین Tool User و Test Engineer است.
ابزار فقط نحوه اجرای تست را مشخص میکند؛ اما QA باید درباره Risk، Scope، Coverage و Test Strategy تصمیم بگیرد.
جمعبندی تفاوت Contract Testing با سایر تستها 🎯
Contract Testing:
- ❌ جای تست واحد (Unit Testing) نیست.
- ❌ جای تست یکپارچهسازی (Integration Testing) نیست.
- ❌ جای API Testing نیست.
- ❌ جای E2E Testing نیست.
بلکه در کنار آنها قرار میگیرد.
تمرکز اصلی آن:
اطمینان از Compatibility بین Consumer و Provider
است.
در معماریهای Distributed، Microservices و Event-Driven، این مسئله میتواند یکی از مهمترین ریسکهای سیستم باشد.
بنابراین یک Testing Strategy مناسب ممکن است چنین ترکیبی داشته باشد:
Unit
↓
Component
↓
Contract
↓
API / Integration
↓
E2E
البته این ترتیب یک Pipeline اجباری نیست؛ هر تست باید در جایی قرار گیرد که بیشترین Feedback و Risk Reduction را با کمترین هزینه ایجاد کند.
Pact Broker چیست؟ 🗂️
در پروژههای کوچک ممکن است Contractها بدون Broker نیز مدیریت شوند. اما وقتی تعداد Consumerها و Providerها افزایش پیدا میکند، مدیریت Contractها دشوارتر میشود.
اینجاست که Pact Broker اهمیت پیدا میکند.
Consumer A ──┐
Consumer B ──┤
Consumer C ──┤
▼
Pact Broker
▲
Provider A ───┤
Provider B ───┘
Broker میتواند Contractها و نتایج Verification را نگهداری کند.
این موضوع در سازمانهایی که تعداد زیادی Microservice دارند بسیار مهم است.
مزیت مهم Pact 🎯
یکی از نقاط قوت Pact این است که Contract را از نیاز واقعی Consumer ایجاد میکند.
فرض کنید Provider چنین Response بزرگی دارد:
{
"id": 15,
"name": "John",
"email": "john@example.com",
"phone": "123456",
"address": "...",
"preferences": {},
"lastLogin": "...",
"metadata": {}
}
اما Consumer فقط به این موارد نیاز دارد:
idnameemail
Contract میتواند فقط همین نیاز را منعکس کند.
در نتیجه Consumer به جزئیاتی که واقعاً به آنها وابسته نیست، وابسته نمیشود.
2. Spring Cloud Contract ☕️
اگر پروژه بر پایه Spring Boot / Java ساخته شده باشد، یکی از گزینههای مهم Spring Cloud Contract است.
این ابزار بیشتر در اکوسیستم Spring شناخته میشود و امکان تعریف Contract بین سرویسها را فراهم میکند.
Consumer
↓
Contract
↓
Provider
↓
Verification
یکی از ویژگیهای جالب آن این است که Contract میتواند به تولید تستهایی برای Provider کمک کند.
بنابراین بخشی از فرآیند تست میتواند بر اساس Contract بهصورت خودکار ایجاد شود.
Pact یا Spring Cloud Contract؟ 🤔
اگر پروژه Java/Spring داشته باشیم، هر دو گزینه میتوانند مطرح باشند.
اما تصمیم نباید فقط بر اساس زبان برنامهنویسی گرفته شود.
باید مواردی مانند موارد زیر را نیز در نظر گرفت:
- معماری پروژه
- نوع Contract
- نیاز به Consumer-Driven Contract
- ابزارهای CI/CD
- تجربه تیم
- نیاز به Broker
- تعداد سرویسها
بهخصوص اگر سازمان از قبل اکوسیستم Pact را در پروژههای مختلف خود استفاده کرده باشد، انتخاب Pact میتواند مزایای عملی بیشتری داشته باشد.
3. OpenAPI-based Contract Testing 📄
یک روش دیگر استفاده از OpenAPI Specification بهعنوان مرجع Contract است.
فرض کنید API با OpenAPI تعریف شده است:
/users/{id}:
get:
responses:
'200':
content:
application/json:
schema:
type: object
properties:
id:
type: integer
name:
type: string
در این حالت OpenAPI مشخص میکند API چه ساختاری دارد.
ابزارهای مختلفی میتوانند بر اساس OpenAPI مواردی مانند موارد زیر را بررسی کنند:
- Request Validation
- Response Validation
- Schema Validation
- API Conformance
آیا OpenAPI همان Contract Testing است؟ ⚠️
نه، این دو مفهوم را نباید کاملاً یکی بدانیم.
OpenAPI میتواند API Contract را توصیف کند، اما Contract Testing لزوماً فقط بررسی Documentation یا Schema نیست.
OpenAPI
↓
What API should look like
در حالی که Contract Testing میتواند سؤال دقیقتری بپرسد:
Consumer
↓
What does this Consumer actually require?
↓
Does Provider satisfy that requirement?
بنابراین OpenAPI و Contract Testing میتوانند در کنار یکدیگر استفاده شوند.
4. Contract Testing برای GraphQL 🔷
GraphQL با REST تفاوت دارد.
در REST معمولاً چند Endpoint داریم:
GET /users
GET /users/15
POST /orders
اما در GraphQL معمولاً یک Endpoint اصلی داریم و Consumer مشخص میکند چه دادهای میخواهد.
query {
user(id: 15) {
id
name
email
}
}
در اینجا Contract میتواند حول Query و دادهای که Consumer انتظار دارد شکل بگیرد.
اما GraphQL چالشهای خاص خودش را دارد؛ زیرا صرفاً بررسی Schema کلی GraphQL همیشه به این معنا نیست که تمام Queryهای Consumerها بدون مشکل باقی خواهند ماند.
بنابراین باید Consumer Usage را نیز در نظر گرفت.
5. Contract Testing در Kafka و Event-Driven Systems 📨
در معماریهای Event-Driven، Contract معمولاً حول Message یا Event شکل میگیرد.
Producer
↓
OrderCreated
↓
Kafka
↓
Consumer
ساختار Event ممکن است چنین باشد:
{
"eventType": "OrderCreated",
"orderId": 12345,
"customerId": 15,
"totalAmount": 250
}
در اینجا Contract میتواند مشخص کند:
- Event چه نامی دارد.
- چه Fieldهایی دارد.
- Type هر Field چیست.
- کدام Fieldها Required هستند.
- چه تغییراتی Backward Compatible هستند.
Schema Registry چه نقشی دارد؟ 🗃️
در سیستمهایی مانند Kafka ممکن است از Schema Registry استفاده شود.
Producer
↓
Schema
↓
Schema Registry
↓
Kafka
↓
Consumer
6. QA / Tester در Contract Testing چه نقشی دارد؟ 🧪
یکی از سؤالهای مهم درباره Contract Testing این است که آیا QA یا Tester باید Contract Testing را انجام دهد؟
پاسخ کوتاه این است:
در بسیاری از پروژهها بله، اما نه لزوماً به این معنا که Tester باید تمام Contractها را شخصاً بنویسد.
نقش Tester در Contract Testing به ساختار تیم، معماری پروژه، سطح Automation و نحوه توسعه و Release سرویسها بستگی دارد.
آیا QA باید Pact را خودش بنویسد؟
این موضوع در تیمهای مختلف میتواند متفاوت باشد.
- در بعضی تیمها Developerها Consumer Contract را مینویسند و QA روی Coverage و کیفیت آن نظارت میکند.
- در بعضی تیمها QA Automation Engineer مستقیماً Contract Testها را توسعه میدهد.
- در تیمهای Mature ممکن است مسئولیت بین Developer و QA تقسیم شود.
Developer
↓
Implementation
+
Consumer Contract
QA
↓
Risk
Coverage
Verification
Quality Strategy
بنابراین Contract Testing الزاماً یک فعالیت فقط مخصوص QA یا فقط مخصوص Developer نیست.
چه زمانی QA میتواند نقش پررنگی داشته باشد؟ 🎯
Contract Testing برای QA اهمیت بیشتری پیدا میکند وقتی شرایطی مانند موارد زیر وجود داشته باشد:
- QA Automation در تیم وجود دارد.
- API Testing بخش مهمی از پروژه است.
- Microservices متعدد هستند.
- Consumer و Provider توسط تیمهای مختلف توسعه داده میشوند.
- CI/CD بخش جدی فرآیند توسعه است.
- Regression Integration هزینه زیادی دارد.
- تیم از Shift-Left Testing استفاده میکند.
در چنین پروژهای QA میتواند مسئول یا شریک اصلی در فعالیتهایی مانند موارد زیر باشد:
- Contract Strategy
- Contract Test Design
- Verification
- CI/CD Integration
- Failure Analysis
- Risk Assessment
چه زمانی Developer نقش بیشتری دارد؟ 👨💻
در بعضی تیمها Contract بسیار نزدیک به Implementation است.
Developer
↓
Consumer Code
↓
Consumer Contract
در چنین ساختاری منطقی است که Developer Contract را همراه با Feature توسعه دهد.
QA نیز میتواند بررسی کند که:
- سناریوهای مهم پوشش داده شدهاند.
- Contract بیش از حد محدود یا بیش از حد گسترده نیست.
- Error Caseهای مناسب پوشش داده شدهاند.
- Breaking Changeها بهدرستی مدیریت میشوند.
بهترین مدل چیست؟ 🤝
در بسیاری از تیمهای Mature، بهترین رویکرد Shared Responsibility است.
| فعالیت | Developer | QA |
|---|---|---|
| شناخت API | ✅ | ✅ |
| نوشتن Consumer Contract | ✅ | ممکن است |
| Provider Verification | ✅ | ✅ |
| طراحی سناریوهای Risky | ✅ | ✅ |
| بررسی Coverage | ممکن است | ✅ |
| CI/CD | ✅ | ✅ |
| تحلیل Failure | ✅ | ✅ |
| Business Risk | ممکن است | ✅ |
بنابراین مهمتر از اینکه Contract Test دقیقاً توسط چه کسی نوشته میشود، این است که مسئولیت کیفیت Contract، Coverage و Compatibility بین تیمها بهدرستی مدیریت شود.
7. Contract Testing در CI/CD ⚙️🚀
اگر Contract Testing فقط روی سیستم Developer اجرا شود، بخش مهمی از ارزش آن از بین میرود.
هدف اصلی این است که Contract Testing وارد CI/CD Pipeline شود تا هر زمان که Consumer یا Provider تغییر میکند، سیستم بهصورت خودکار بررسی کند:
آیا این تغییر با Consumerها یا Providerهای وابسته سازگار است؟
یک Pipeline ساده میتواند چیزی شبیه این باشد:
Code Change
↓
Build
↓
Unit Tests
↓
Contract Tests
↓
Integration Tests
↓
Compatibility Check
↓
Deploy
اما در یک معماری Microservices، فرآیند میتواند کمی پیچیدهتر شود.
یک سناریوی واقعی 🔍
فرض کنیم سه بخش داریم:
Order Service
↓
User Service
↓
Database
Order Service یک Consumer است و User Service یک Provider.
حالا Developer در User Service تغییر ایجاد میکند:
User Service v3.2
↓
API Change
قبل از Deploy باید بدانیم:
آیا این تغییر Contract مربوط به Order Service را میشکند؟
بدون Contract Testing چه اتفاقی ممکن است بیفتد؟ ⚠️
در یک فرآیند سنتی ممکن است روند به این شکل باشد:
Developer Change
↓
Build
↓
Unit Tests
↓
Deploy
↓
Integration Environment
↓
E2E Tests
↓
Failure ❌
مشکل اینجاست که Failure ممکن است بعد از Deploy مشخص شود.
حتی در بعضی سازمانها ممکن است مشکل تا Production نیز کشف نشود.
با Contract Testing چه اتفاقی میافتد؟ ✅
حالا همان تغییر را در نظر بگیریم:
Developer Change
↓
Build
↓
Unit Tests
↓
Contract Verification
↓
FAIL ❌
↓
Deployment Blocked
در این حالت Breaking Change قبل از رسیدن به محیطهای بعدی شناسایی میشود.
در نتیجه Developer میتواند مشکل را قبل از Deployment اصلاح کند.
Contract Testing چگونه به Shift-Left کمک میکند؟ 🧠
یکی از مزیتهای مهم Contract Testing این است که میتواند به Shift-Left Testing کمک کند.
بدون Contract Testing ممکن است فرآیند به این شکل باشد:
Code Change
↓
Deploy
↓
Integration Environment
↓
E2E Test
↓
Failure ❌
اما با Contract Testing:
Code Change
↓
Contract Verification
↓
Failure ❌
یعنی مشکل میتواند بسیار زودتر شناسایی شود.
- هزینه رفع Bug کاهش پیدا میکند.
- Feedback سریعتر دریافت میشود.
- وابستگی به Environment کاهش پیدا میکند.
- Release Confidence افزایش پیدا میکند.
اما باید توجه داشت که Contract Testing فقط زمانی میتواند این مزایا را بهطور مؤثر ایجاد کند که واقعاً در فرآیند CI/CD قرار گرفته باشد.
Consumer Pipeline 🔄
فرض کنیم Order Service تغییر کرده است.
Pipeline میتواند به شکل زیر باشد:
Order Service
↓
Commit
↓
Build
↓
Unit Tests
↓
Consumer Contract Tests
↓
Publish Contract
↓
Pact Broker
در این مرحله Consumer مشخص میکند:
«من با Provider این Interactionها را نیاز دارم.»
Provider Pipeline 🔍
حالا فرض کنیم User Service تغییر کرده است.
Pipeline آن میتواند چنین باشد:
User Service
↓
Commit
↓
Build
↓
Unit Tests
↓
Fetch Contracts
↓
Provider Verification
↓
PASS / FAIL
اگر Verification موفق باشد:
Provider Verification
↓
PASS
↓
Deploy ✅
اگر Contract شکسته باشد:
Provider Verification
↓
FAIL
↓
Deploy Blocked ❌
Pact Broker در CI/CD 🗂️
Pact Broker در این معماری نقش مهمی دارد. میتوان آن را بهعنوان نقطهای برای هماهنگی Contractها و نتایج Verification در نظر گرفت.
Order Service
│
│ Contract
▼
┌───────────────┐
│ Pact Broker │
└───────┬───────┘
│
│ Verification
▼
User Service
Broker میتواند اطلاعاتی مانند موارد زیر را نگهداری کند:
- Consumer Version
- Provider Version
- Contract
- Verification Result
مفهوم مهم: Can I Deploy? 🚦
یکی از قابلیتهای مهم در اکوسیستم Pact، مفهوم Can I Deploy? است.
سؤال اصلی بسیار ساده است:
آیا این نسخه را میتوانم بدون شکستن Consumerها یا Providerهای مرتبط Deploy کنم؟
فرض کنید:
Order Service
v4.2.0
میخواهد Deploy شود.
سیستم بررسی میکند:
Order Service v4.2.0
↓
Compatible with required Providers?
↓
YES ✅
در این حالت:
Deploy Allowed 🚀
اما اگر Compatibility وجود نداشته باشد:
Compatible?
↓
NO ❌
↓
Deployment Blocked
این مفهوم در محیطهای Continuous Delivery بسیار ارزشمند است.
چرا Can I Deploy مهم است؟ 🎯
فرض کنید 50 Microservice داریم و هر سرویس ممکن است چند Consumer و Provider داشته باشد.
اگر هر تیم بخواهد قبل از Release بهصورت دستی از تیمهای دیگر سؤال کند:
«آیا تغییر من با سرویس شما سازگار است؟»
فرآیند Release بسیار کند و خطاپذیر میشود.
اما با Contract Testing:
Compatibility
↓
Automated
↓
CI/CD
↓
Deploy Decision
بخش زیادی از این تصمیمگیری میتواند بهصورت خودکار انجام شود.
Consumer Version و Provider Version 🔢
در سیستمهای واقعی، Versioning اهمیت زیادی دارد.
در یک سیستم بزرگ، ممکن است چند Version از یک سرویس یا Contract در زمانهای مختلف وجود داشته باشد. بنابراین CI/CD باید بتواند تشخیص دهد کدام نسخهها با یکدیگر Compatible هستند.
Consumer Version و Provider Version 🔢
در سیستمهای واقعی Versioning اهمیت زیادی دارد.
مثلاً:
Consumer:
Order Service
v2.4.0
و:
Provider:
User Service
v3.1.0
ممکن است Verification به این شکل ثبت شود:
Order 2.4.0
↓
User 3.1.0
↓
✅ Compatible
بعد Provider نسخه جدیدی منتشر میکند:
User 3.2.0
و نتیجه میشود:
Order 2.4.0
↓
User 3.2.0
↓
❌ Incompatible
حالا تیم میداند که Deploy نسخه جدید User Service میتواند مشکل ایجاد کند.
آیا هر Failure باید Deployment را متوقف کند؟ 🤔
نه لزوماً.
این موضوع به Strategy سازمان بستگی دارد.
Contractهای Critical
FAIL
↓
Block Deployment ❌
Contractهای Experimental
FAIL
↓
Warning ⚠️
↓
Continue
بنابراین بهتر است Quality Gateها نیز Risk-Based طراحی شوند.
Contract Testing در Pull Request 🔀
یکی از بهترین مکانها برای اجرای Contract Test، Pull Request است.
فرض کنیم Developer میخواهد تغییر زیر را Merge کند:
feature/change-user-api
↓
Pull Request
CI میتواند Contract Test را اجرا کند:
Pull Request
↓
Build
↓
Contract Test
↓
❌ Breaking Change
در این حالت Developer قبل از Merge متوجه مشکل میشود.
این دقیقاً همان چیزی است که Shift-Left Testing به دنبال آن است.
Feedback سریعتر ⚡
یکی از مزایای مهم اجرای Contract Testing در CI این است که Feedback سریع میشود.
بدون آن:
Code
↓
Merge
↓
Deploy
↓
E2E
↓
Failure
ممکن است چند ساعت طول بکشد.
با Contract Testing:
Code
↓
Pull Request
↓
Contract Test
↓
Failure
Developer سریعتر متوجه مشکل میشود.
Contract Testing در چند Repository 📦
در سازمانهای بزرگ ممکن است هر Microservice Repository جداگانه داشته باشد:
Git Repository
├── order-service
├── user-service
├── payment-service
├── inventory-service
└── notification-service
در این حالت Contract Testing اهمیت بیشتری پیدا میکند، چون تغییر در یک Repository میتواند روی Repositoryهای دیگر اثر بگذارد.
Broker میتواند نقش هماهنگکننده را داشته باشد:
Repo A ──┐
Repo B ──┤
Repo C ──┼──► Contract Broker
Repo D ──┘
Contract Testing و Feature Flag 🚩
در بعضی پروژهها Breaking Change بهصورت تدریجی منتشر میشود.
مثلاً:
Old API
+
New API
یا:
Feature Flag
↓
Old Behavior / New Behavior
Contract Testing میتواند کمک کند اطمینان حاصل کنیم تغییر جدید همچنان Contractهای لازم را رعایت میکند.
البته Feature Flag و Contract Testing دو تکنیک متفاوت هستند و نباید یکی را جایگزین دیگری دانست.
یک سناریوی Deployment واقعی 🚀
فرض کنیم User Service میخواهد نسخه جدیدی منتشر کند.
مرحله 1
Developer تغییر را Commit میکند:
User Service v3.2.0
مرحله 2
CI اجرا میشود:
Build
↓
Unit Tests
مرحله 3
Provider Verification اجرا میشود:
Contract Verification
مرحله 4
فرض کنیم نتیجه:
Order Service → PASS
Payment Service → PASS
Reporting Service → FAIL
حالا سیستم متوجه میشود که نسخه جدید User Service با Reporting Service سازگار نیست.
آیا Deploy باید انجام شود؟
اگر Reporting Service یک Dependency حیاتی باشد:
FAIL
↓
🚫 Block
اما اگر Contract مربوط به یک Consumer قدیمی و در حال حذف شدن باشد، ممکن است تصمیم متفاوتی گرفته شود.
اینجا دوباره مفهوم Risk و Lifecycle Management اهمیت پیدا میکند.
اینجا QA چه نقشی دارد؟ 🧪
QA میتواند در طراحی این Quality Gateها نقش مهمی داشته باشد.
برای مثال تعیین کند:
Critical Contract
Payment
Order
Inventory
باید:
PASS → Deploy
FAIL → Block
Low Risk Contract
ممکن است:
FAIL → Warning
باشد.
بنابراین QA میتواند به تعیین Release Risk Policy کمک کند.
⚠️ یک خطر مهم: Flaky Contract Tests
Contract Testها نیز میتوانند Flaky شوند.
مثلاً اگر تست به موارد زیر وابسته باشد، ممکن است بدون تغییر واقعی Contract Fail شود:
- Environment ناپایدار
- Database خارجی
- سرویس واقعی
- Network
- دادههای غیرقابل پیشبینی
نتیجه:
False Failure
↓
Developer Ignore
↓
Trust ↓
این برای Contract Testing بسیار خطرناک است.
Contract Test باید قابل اعتماد باشد
اصل مهم:
8. یک مثال واقعی از Contract Testing در پروژه E-Commerce 🛒
تا اینجا درباره مفهوم Contract Testing، مزایا و محدودیتها، ابزارها، نقش QA و CI/CD صحبت کردیم.
اما برای اینکه موضوع کاملاً روشن شود، بهتر است همه این مفاهیم را در یک پروژه واقعی کنار هم قرار دهیم.
فرض کنید در حال توسعه یک پلتفرم فروشگاه اینترنتی هستیم که از چند Microservice تشکیل شده است.
معماری پروژه 🏗️
سیستم ما شامل سرویسهای زیر است:
┌──────────────┐
│ Frontend │
└──────┬───────┘
│
▼
┌──────────────┐
│ Order Service│
└──────┬───────┘
│
┌─────────────┼─────────────┐
▼ ▼ ▼
┌────────────┐ ┌────────────┐ ┌──────────────┐
│User Service│ │Payment │ │Inventory │
│ │ │Service │ │Service │
└────────────┘ └────────────┘ └──────────────┘
هر سرویس توسط یک تیم یا Sub-Team مستقل توسعه داده میشود.
همچنین سرویسها ممکن است مستقل از یکدیگر Deploy شوند.
این دقیقاً همان شرایطی است که Contract Testing میتواند ارزش زیادی ایجاد کند.
یک Order چگونه ایجاد میشود؟ 🛒
فرض کنیم کاربر میخواهد یک سفارش ثبت کند.
Order Service باید چند کار انجام دهد:
Create Order
│
├── Get User
│
├── Check Inventory
│
├── Process Payment
│
└── Create Order
بنابراین Order Service به سه Provider وابسته است:
Order Service
│
├────► User Service
│
├────► Inventory Service
│
└────► Payment Service
در اینجا Order Service یک Consumer است.
و سه سرویس دیگر در این تعامل نقش Provider دارند.
اولین Contract: User Service 👤
فرض کنیم Order Service برای ایجاد سفارش ابتدا اطلاعات کاربر را دریافت میکند:
GET /users/123
Consumer فقط به این اطلاعات نیاز دارد:
{
"id": 123,
"name": "John",
"status": "ACTIVE"
}
بنابراین Contract میتواند مشخص کند:
Request:
GET /users/123
Expected Response:
Status = 200
Body:
id → integer
name → string
status → string
نکته مهم این است که اگر User Service اطلاعات بیشتری داشته باشد، الزاماً نباید همه آنها وارد Contract شوند.
{
"id": 123,
"name": "John",
"status": "ACTIVE",
"email": "john@example.com",
"phone": "...",
"address": "...",
"preferences": {}
}
Consumer ممکن است اصلاً به phone یا address وابسته نباشد.
پس Contract باید تا حد امکان روی Consumer’s Actual Needs متمرکز باشد.
Consumer Test چه چیزی را بررسی میکند؟ 🧪
در Consumer Side، Test بررسی میکند که اگر User Service مطابق Contract رفتار کند، Order Service میتواند با آن تعامل کند.
Order Service
↓
Expected Interaction
↓
GET /users/123
↓
200 OK
↓
Expected Fields
این تست در واقع میگوید:
«Order Service به این رفتار از User Service وابسته است.»
حالا Provider چه کاری انجام میدهد؟ 🔍
تیم User Service باید بررسی کند که Implementation واقعی آنها Contract را رعایت میکند.
Contract
↓
User Service
↓
Real Implementation
↓
Verification
↓
PASS / FAIL
Provider Verification چه چیزی را مشخص میکند؟ 🔍
تیم User Service باید بررسی کند که Implementation واقعی آنها Contract را رعایت میکند.
Consumer Contract
↓
User Service
↓
Provider Verification
اگر User Service واقعاً چنین Responseای برگرداند:
{
"id": 123,
"name": "John",
"status": "ACTIVE"
}
Contract با موفقیت Verify میشود. ✅
حالا یک Breaking Change اتفاق میافتد 💥
فرض کنید تیم User Service تصمیم میگیرد نام فیلد status را تغییر دهد.
قبلاً:
{
"id": 123,
"name": "John",
"status": "ACTIVE"
}
اما در نسخه جدید:
{
"id": 123,
"name": "John",
"accountStatus": "ACTIVE"
}
فیلد status حذف شده و با accountStatus جایگزین شده است.
از دید Provider ممکن است این تغییر کاملاً منطقی باشد؛ اما از دید Consumer:
Order Service
↓
expects: status
↓
Provider:
accountStatus
↓
❌ Contract Broken
بدون Contract Testing چه اتفاقی ممکن است بیفتد؟ ⚠️
اگر User Service بهصورت مستقل Deploy شود:
User Service v3.2
↓
Deploy ✅
↓
Order Service
↓
Runtime Error ❌
ممکن است این مشکل در Integration Test یا حتی در Production شناسایی شود.
با Contract Testing چه اتفاقی میافتد؟ ✅
Provider قبل از Deploy، Contract را Verify میکند:
User Service Change
↓
Provider Verification
↓
Expected:
status
↓
Actual:
accountStatus
↓
❌ FAIL
در این حالت Pipeline میتواند Deployment را متوقف کند:
Contract Verification
↓
FAIL
↓
Deployment Blocked 🚫
این دقیقاً یکی از ارزشهای اصلی Contract Testing است: شناسایی Breaking Change قبل از اینکه به Runtime برسد.
چه کسی باید این مشکل را پیدا کند؟ 🤔
آیا باید QA این مشکل را پیدا کند؟ یا Developer؟
در یک تیم Mature، بهتر است پاسخ این باشد:
سیستم تست خودکار باید مشکل را پیدا کند؛ نه اینکه منتظر باشیم یک فرد آن را بهصورت دستی کشف کند.
اما مسئولیت افراد میتواند متفاوت باشد.
Developer 👨💻
- تغییر API را ایجاد میکند.
- Provider Verification را اجرا میکند.
- Failure را بررسی و اصلاح میکند.
QA 🧪
- ریسک تغییر را بررسی میکند.
- Coverage مربوط به Contract را ارزیابی میکند.
- اطمینان حاصل میکند سناریوهای مهم جا نماندهاند.
- در صورت نیاز Testing Strategy را اصلاح میکند.
Contract دوم: Inventory Service 📦
حالا Order Service باید بررسی کند که محصول موردنظر موجود است یا خیر.
GET /inventory/products/500
Provider ممکن است چنین Responseای داشته باشد:
{
"productId": 500,
"available": true,
"quantity": 12
}
اما Consumer ممکن است فقط به این دو مقدار وابسته باشد:
availablequantity
بنابراین Contract میتواند مشخص کند:
GET /inventory/products/500
Expected:
200 OK
Response:
available → boolean
quantity → integer
یک تغییر ظاهراً ساده ⚠️
فرض کنید Developer در Inventory Service تصمیم میگیرد نوع داده quantity را از Integer به String تغییر دهد.
Before:
"quantity": 12
After:
"quantity": "12"
از نظر Business ممکن است مقدار همچنان همان 12 باشد؛ اما از نظر Contract، نوع داده تغییر کرده است و Consumer ممکن است دیگر نتواند آن را به شکل قبلی پردازش کند.
Contract سوم: Payment Service 💳
حالا حساسترین قسمت سیستم را در نظر بگیریم.
Order Service به Payment Service درخواست پرداخت میفرستد:
POST /payments
Request:
{
"orderId": 10025,
"amount": 250
}
Payment Service انتظار دارد:
orderId → integer
amount → number
و ممکن است Response زیر را برگرداند:
{
"paymentId": 9001,
"status": "SUCCESS"
}
Contract مشخص میکند Order Service به چه چیزی وابسته است.
چرا Payment API اهمیت بیشتری دارد؟ 🔴
همه Contractها از نظر Risk یکسان نیستند.
اگر Notification Service خراب شود، ممکن است سفارش همچنان ثبت شود.
اما اگر Payment Service دچار Compatibility Problem شود:
Payment Failure
↓
Order Creation Failure
↓
Revenue Impact
↓
Customer Impact
پس Payment Contract باید Risk بالاتری داشته باشد.
این همان جایی است که Risk-Based Contract Testing اهمیت پیدا میکند.
همه Contractها برابر نیستند ⚖️
میتوانیم Dependencyها را مثلاً اینگونه طبقهبندی کنیم:
| Dependency | Business Risk | Contract Priority |
|---|---|---|
| Payment | 🔴 High | بسیار بالا |
| Inventory | 🔴 High | بسیار بالا |
| User | 🟠 Medium | بالا |
| Notification | 🟢 Low | متوسط |
این به این معنا نیست که Notification را اصلاً تست نکنیم.
بلکه منابع تست را متناسب با Risk تخصیص میدهیم.
حالا CI/CD را وارد کنیم ⚙️
فرض کنیم Developer در Payment Service تغییر ایجاد میکند.
Pipeline:
Code Change
↓
Build
↓
Unit Tests
↓
Provider Contract Verification
↓
Can I Deploy?
اگر همه Contractها Pass شوند:
PASS ✅
↓
Deploy
اگر Payment Contract Fail شود:
FAIL ❌
↓
Block Deployment
یک سناریوی واقعیتر: چند Consumer 👥
حالا فرض کنیم Payment Service فقط توسط Order Service استفاده نمیشود.
┌── Order Service
│
Payment Service ───┼── Subscription Service
│
└── Mobile App Backend
حالا یک تغییر در Payment API میتواند سه Consumer را تحت تأثیر قرار دهد.
اگر Payment Service بخواهد Deploy شود:
Payment Service v4
↓
Contract Verification
│
├── Order → PASS ✅
├── Subscription → PASS ✅
└── Mobile → FAIL ❌
حالا تیم میداند:
نسخه جدید Payment Service حداقل با یکی از Consumerهای موجود سازگار نیست.
اینجا Pact Broker چه کمکی میکند؟ 🗂️
در چنین پروژهای تعداد روابط بهسرعت زیاد میشود.
Broker میتواند وضعیت Compatibility را قابل مشاهدهتر کند:
Pact Broker
│
┌──────────┼──────────┐
▼ ▼ ▼
Order Subscription Mobile
│ │ │
└──────────┼──────────┘
▼
Payment API
تیم میتواند متوجه شود:
- چه Consumerهایی وجود دارند.
- چه Contractهایی منتشر شدهاند.
- کدام Verificationها Pass شدهاند.
- کدام Versionها با یکدیگر Compatible هستند.
یک Breaking Change دیگر: حذف Endpoint 🚨
فرض کنید Payment Service این Endpoint را دارد:
POST /payments
و تیم تصمیم میگیرد آن را حذف کند.
اگر هیچ Consumerی از آن استفاده نکند، حذف Endpoint احتمالاً مشکلی ایجاد نمیکند.
اما اگر حتی یک Consumer فعال از آن استفاده کند، حذف آن میتواند یک Breaking Change باشد.
Contract Testing و Refactoring 🔧
Contract Testing فقط برای جلوگیری از تغییرات API نیست.
فرض کنید تیم میخواهد Provider را Refactor کند.
ساختار قبل:
Controller
↓
Service
↓
Repository
و بعد از Refactoring:
Controller
↓
New Service Layer
↓
Repository
اگر رفتار API تغییر نکند، Contractها باید همچنان Pass شوند.
بنابراین Contract Testing میتواند بهعنوان یک Safety Net برای Refactoring نیز عمل کند. 🛡️
Contract Testing و API Versioning 🔢
گاهی یک Breaking Change اجتنابناپذیر است.
در این شرایط ممکن است API جدیدی ایجاد شود:
API v1
↓
Existing Consumers
API v2
↓
New Consumers
برای مدتی ممکن است هر دو نسخه همزمان وجود داشته باشند:
┌── Consumer A → v1
Provider ─────┤
└── Consumer B → v2
بعد از اینکه Consumerها به نسخه جدید مهاجرت کردند:
All Consumers
↓
v2
↓
Retire v1
Contract Testing میتواند در این فرآیند Migration نیز نقش مهمی داشته باشد.
QA در این پروژه دقیقاً چه کار میکند؟ 🧪
حالا بیایید نقش QA را در همین پروژه ملموستر کنیم.
QA الزاماً نباید برای هر API بهصورت دستی Contract بنویسد؛ اما میتواند فرآیندی مانند زیر داشته باشد.
مرحله 1 — Risk Identification
مشخص کند کدام Integrationها Critical هستند.
مرحله 2 — Contract Review
بررسی کند Contract واقعاً نیاز Consumer را پوشش میدهد.
مرحله 3 — Negative Scenarios
بررسی کند Error Responseهای مهم نیز در صورت نیاز در Contract پوشش داده شدهاند.
مرحله 4 — CI/CD
اطمینان حاصل کند Contract Verification در Pipeline اجرا میشود.
مرحله 5 — Failure Analysis
در صورت Failure مشخص کند مشکل از کدام نوع است:
Real Breaking Change?
or
Test Problem?
or
Environment Problem?
مرحله 6 — Coverage Review
بررسی کند آیا Dependencyهای مهم تحت پوشش Contract Testing هستند یا خیر.
آیا Manual Tester هم باید Contract Testing انجام دهد؟ 🤔
اینجا باید بین نقشهای مختلف QA تفاوت قائل شویم.
اگر منظور از Manual Tester کسی باشد که عمدتاً Test Caseهای دستی اجرا میکند، UI را بررسی میکند و API را بهصورت دستی تست میکند، Contract Testing معمولاً فعالیت اصلی او نیست.
Contract Testing ذاتاً یک فعالیت Automation-oriented است.
اما یک Manual Tester با دانش مناسب API و معماری سیستم میتواند در بخشهایی مانند موارد زیر مشارکت کند:
- شناسایی Dependencyها
- تحلیل Risk
- بررسی Contract
- طراحی سناریو
- تحلیل Failure
در مقابل، QA Automation Engineer / SDET معمولاً میتواند مستقیماً Contract Testها را پیادهسازی و در CI/CD ادغام کند.
یک اشتباه مهم در تعریف نقش QA ⚠️
نباید بگوییم:
«Contract Testing چون Testing است، پس کاملاً مسئولیت QA است.»
این نگاه میتواند باعث ایجاد یک Testing Silo شود.
در واقع Contract Testing به چند حوزه متصل است:
Contract Testing
│
┌───────────┼───────────┐
▼ ▼ ▼
Dev QA DevOps
│ │ │
Provider Risk/Quality CI/CD
Consumer Coverage Pipeline
بنابراین موفقیت Contract Testing به همکاری تیمی وابسته است. 🤝
🧠 مهمترین نکته این مثال
در این پروژه Contract Testing قرار نیست همه چیز را تست کند.
ما همچنان به انواع دیگری از تست نیاز داریم:
Unit Testing
Integration Testing
Contract Testing
API Testing
End-to-End Testing
Performance Testing
Security Testing
هر کدام مسئله متفاوتی را هدف قرار میدهند.
Contract Testing عمدتاً روی این سؤال تمرکز دارد:
9. Contract Testing در مقایسه با سایر انواع تست 🧪
یکی از رایجترین سوءتفاهمها درباره Contract Testing این است که تصور کنیم Contract Testing جایگزین یکی از انواع تستهای موجود است.
مثلاً:
«اگر Contract Testing داشته باشیم، دیگر Integration Testing لازم نیست.»
«Contract Testing همان API Testing است.»
«Contract Testing نوعی E2E Testing است.»
هیچکدام از این برداشتها کاملاً درست نیستند.
Contract Testing مسئله متفاوتی را هدف قرار میدهد و در کنار سایر انواع تست معنا پیدا میکند.
Contract Testing دقیقاً چه چیزی را تست میکند؟ 🎯
بهصورت ساده، Contract Testing میخواهد بررسی کند:
آیا دو سیستم یا دو سرویس که با یکدیگر ارتباط دارند، همچنان مطابق توافق مشترک خود با یکدیگر سازگار هستند؟
Consumer
│
│ HTTP Request
▼
Provider
│
│ HTTP Response
▼
Consumer
Contract Testing روی همین تعامل و توافق بین دو طرف تمرکز دارد.
مقایسه کلی انواع تست
برای درک بهتر، اجازه دهید ابتدا یک نمای کلی داشته باشیم:
| نوع تست | سؤال اصلی |
|---|---|
| Unit Testing | آیا یک واحد کوچک از کد درست کار میکند؟ |
| Integration Testing | آیا اجزای واقعی سیستم درست با هم کار میکنند؟ |
| API Testing | آیا API رفتار مورد انتظار را دارد؟ |
| Contract Testing | آیا Consumer و Provider با Contract خود سازگارند؟ |
| E2E Testing | آیا یک جریان کامل Business از ابتدا تا انتها کار میکند؟ |
این تفاوت بسیار مهم است.
1. Contract Testing vs Unit Testing
در Unit Testing معمولاً کوچکترین واحد منطقی کد را بهصورت مستقل تست میکنیم.
calculateTotal()
↓
Input
↓
Expected Output
فرض کنید تابع زیر را داریم:
calculateDiscount(price, customerType)
Unit Test میتواند بررسی کند:
price = 100
customerType = Premium
Expected:
discount = 20
در اینجا هیچ Consumer یا Providerای وجود ندارد.
Contract Testing چه تفاوتی دارد؟
در Contract Testing مسئله بین دو Component است:
Order Service
↓
User Service
ما دیگر نمیپرسیم:
آیا تابع
calculateDiscount()درست کار میکند؟
بلکه میپرسیم:
آیا User Service همان Interface و Data Structureای را ارائه میدهد که Order Service انتظار دارد؟
بنابراین Contract Testing و Unit Testing دو هدف متفاوت دارند و مکمل یکدیگر هستند.
بنابراین:
Unit Test
↓
Internal Logic
Contract Test
↓
Service Interaction
آیا Contract Testing جای Unit Testing را میگیرد؟ ❌
خیر.
حتی اگر تمام Contractها Pass شوند، ممکن است Business Logic داخل Provider کاملاً اشتباه باشد.
Payment Service
↓
Contract = PASS ✅
↓
Payment Calculation = WRONG ❌
Contract Testing نمیتواند جای Unit Testing را بگیرد.
2. Contract Testing vs Integration Testing 🔗
این مهمترین مقایسه مقاله است.
در Integration Testing معمولاً چند Component واقعی را در کنار هم اجرا میکنیم.
Order Service
↓
Payment Service
↓
Database
و بررسی میکنیم که Integration واقعی درست کار میکند.
Contract Testing چه تفاوتی دارد؟
در Contract Testing هدف اصلی این نیست که تمام سیستمهای واقعی را با هم اجرا کنیم.
تمرکز روی Compatibility است.
Consumer
↓
Expected Contract
↓
Provider Verification
در نتیجه Contract Test میتواند بسیار سریعتر و مستقلتر از یک Integration Test بزرگ باشد.
یک مثال
فرض کنید Order Service به Payment Service درخواست زیر را میفرستد:
POST /payments
با:
{
"orderId": 1001,
"amount": 250
}
Contract Testing بررسی میکند که Provider بتواند این Interaction را مطابق Contract پشتیبانی کند.
اما Integration Testing ممکن است واقعاً این مسیر را اجرا کند:
Order Service
↓
Payment Service
↓
Payment Database
↓
Transaction
↓
Response
پس Integration Test وابستگیهای واقعی بیشتری دارد.
مزیت Contract Testing نسبت به Integration Testing
یکی از مزایای مهم Contract Testing این است که معمولاً:
- سریعتر است.
- Dependencyهای کمتری دارد.
- برای CI/CD مناسبتر است.
- Failure آن میتواند مشخصتر باشد.
- میتواند بدون اجرای کل سیستم Compatibility را بررسی کند.
اما Integration Testing نیز چیزی را بررسی میکند که Contract Testing الزاماً نمیتواند:
آیا Integration واقعی در Runtime درست کار میکند؟
بنابراین هر دو لازم هستند، اما برای اهداف متفاوت.
3. Contract Testing vs API Testing 🌐
این دو مفهوم خیلی راحت با یکدیگر اشتباه گرفته میشوند.
API Testing معمولاً رفتار API را بررسی میکند.
GET /users/123
↓
200 OK
↓
Correct Response
یا:
POST /orders
↓
201 Created
API Testing میتواند شامل موارد زیادی باشد:
- Functional Testing
- Validation
- Authentication
- Authorization
- Error Handling
- Boundary Testing
- Business Rules
Contract Testing محدودتر و تخصصیتر است
Contract Testing روی توافق بین Consumer و Provider تمرکز دارد.
Consumer expects:
status → string
id → integer
Provider باید این Contract را رعایت کند.
API Testing
↓
Does API behave correctly?
Contract Testing
↓
Does API remain compatible with its Consumer?
این دو سؤال کاملاً یکسان نیستند.
آیا میتوان API Test را Contract Test در نظر گرفت؟ 🤔
معمولاً نه.
ممکن است یک API Test بهصورت اتفاقی بخشی از Contract را بررسی کند، اما این بهتنهایی آن را به یک Contract Test تبدیل نمیکند.
Contract Testing معمولاً دارای مفهوم مشخصی از موارد زیر است:
Consumer
Provider
Contract
Verification
Compatibility
4. Contract Testing vs End-to-End Testing 🏁
E2E Testing معمولاً کل جریان Business را بررسی میکند.
مثلاً در یک فروشگاه اینترنتی:
Login
↓
Search Product
↓
Add to Cart
↓
Checkout
↓
Payment
↓
Order Confirmation
در E2E ممکن است تمام سرویسهای واقعی درگیر شوند:
Frontend
↓
API Gateway
↓
Order
↓
Payment
↓
Inventory
↓
Database
مشکل E2E در معماری Microservices ⚠️
هرچه سیستم بزرگتر شود، E2E Testها میتوانند:
- کندتر شوند.
- پیچیدهتر شوند.
- Flaky شوند.
- به Environment وابسته شوند.
- نگهداری دشوارتری داشته باشند.
مثلاً اگر Payment Service در دسترس نباشد:
E2E Test
↓
Payment unavailable
↓
FAIL ❌
اما آیا مشکل واقعاً در Order Service بوده است؟
ممکن است اصلاً اینطور نباشد.
Contract Testing در اینجا چه کمکی میکند؟
Contract Testing میتواند Compatibility بین سرویسها را زودتر بررسی کند:
Order Service
↓
Contract
↓
Payment Service
↓
Compatible? ✅
بنابراین میتوان بخشی از ریسک Integration را بدون اجرای کل User Journey بررسی کرد.
آیا Contract Testing جای E2E را میگیرد؟
❌ خیر.
فرض کنید تمام Contractها Pass هستند:
User ↔ Order ✅
Order ↔ Payment ✅
Order ↔ Inventory ✅
آیا تضمین میشود که کل Business Flow درست کار میکند؟
خیر.
Payment = SUCCESS
Inventory = SUCCESS
But...
Order Creation Logic = WRONG ❌
Contract Testing این مشکل را لزوماً پیدا نمیکند.
بنابراین E2E همچنان برای سناریوهای Critical Business لازم است.
5. Contract Testing vs Component Testing 🧩
Component Testing معمولاً یک Component را در سطحی بالاتر از Unit Test بررسی میکند.
Order Service
├── Controller
├── Service
├── Repository
└── Business Logic
میتوان کل Order Service را بهعنوان یک Component تست کرد.
در حالی که Contract Testing بیشتر روی مرز ارتباطی بین Componentها تمرکز دارد:
Order Service
│
│ Contract
▼
Payment Service
پس:
Component Testing
↓
Is this Component working correctly?
Contract Testing
↓
Is this Component compatible with another Component?
6. Contract Testing vs Smoke Testing 💨
Smoke Testing معمولاً برای بررسی سریع سلامت کلی سیستم انجام میشود.
Application starts
↓
API available
↓
Login works
↓
Basic flow works
Smoke Test میتواند بسیار سطحی باشد.
Contract Testing هدف کاملاً متفاوتی دارد و روی Compatibility تمرکز میکند.
بنابراین این دو قابل جایگزینی نیستند.
7. Contract Testing vs Regression Testing 🔄
Regression Testing میخواهد بررسی کند که تغییرات جدید، قابلیتهای قبلی را خراب نکرده باشند.
Contract Testing میتواند یکی از ابزارهای Regression Strategy باشد.
Provider Change
↓
Existing Contracts
↓
Verification
↓
Compatibility
در این حالت Contract Testing به Regression Testing کمک میکند.
اما تمام Regression Testها Contract Test نیستند.
یک مثال بسیار ساده برای درک تفاوتها 🚗
فرض کنید یک ماشین داریم.
Unit Testing
آیا موتور بهدرستی کار میکند؟
Engine → Test
Component Testing
آیا سیستم ترمز بهدرستی کار میکند؟
Brake System → Test
Integration Testing
آیا موتور و گیربکس درست با هم کار میکنند؟
Engine ↔ Transmission
Contract Testing
آیا رابط اتصال موتور و گیربکس مطابق مشخصات مورد توافق است؟
Engine
↕
Interface Contract
↕
Transmission
E2E Testing
آیا خودرو از نقطه A به B بهدرستی حرکت میکند؟
Start
↓
Drive
↓
Stop
این مثال نشان میدهد که هر نوع تست یک سطح و هدف متفاوت دارد.
یک جدول جامع برای مقایسه 🧠
برای جمعبندی تفاوتها، میتوان انواع تست را به شکل زیر مقایسه کرد:
| ویژگی | Unit | Integration | API | Contract | E2E |
|---|---|---|---|---|---|
| تمرکز اصلی | Logic | Integration | API Behavior | Compatibility | Business Flow |
| سرعت | بسیار بالا | متوسط | بالا | بالا | پایینتر |
| Dependency | کم | زیاد | متوسط | کم | زیاد |
| مناسب برای CI/CD | بسیار مناسب | مناسب | مناسب | بسیار مناسب | محدودتر |
| بررسی Compatibility | ❌ | تا حدی | تا حدی | ✅ | تا حدی |
| بررسی Business Flow | ❌ | ❌ | ❌ | ❌ | ✅ |
جدول کاملتر مقایسه تستها به شکل زیر است:
| ویژگی | Unit | Integration | API | Contract | E2E |
|---|---|---|---|---|---|
| تمرکز اصلی | Logic | Integration | API Behavior | Compatibility | Business Flow |
| Scope | کوچک | متوسط | API | Service Boundary | کل سیستم |
| سرعت | بسیار بالا | متوسط | بالا | بالا | پایین |
| Dependency واقعی | کم/هیچ | زیاد | معمولاً متوسط | کم | بسیار زیاد |
| Consumer/Provider | ❌ | ممکن است | معمولاً ❌ | ✅ | ممکن است |
| مناسب CI | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐ |
| مناسب Microservices | محدود | بالا | بالا | بسیار بالا | بالا اما پرهزینه |
| جایگزین E2E؟ | ❌ | ❌ | ❌ | ❌ | — |
| جایگزین Integration؟ | ❌ | — | ❌ | ❌ | ❌ |
پس در یک پروژه واقعی چه تستهایی داشته باشیم؟ 🤔
فرض کنیم یک سیستم Microservice داریم.
بهتر است به جای انتخاب فقط یک نوع تست، از ترکیبی از آنها استفاده کنیم:
E2E
▲
/ \
/ \
Integration API
▲ ▲
│ │
Contract │
▲ │
└────┬────┘
│
Unit Tests
اما این هرم یک قانون ثابت برای همه پروژهها نیست.
تعداد و نوع تستها باید بر اساس موارد زیر تعیین شود:
- Architecture
- Business Risk
- Failure Cost
- Release Strategy
- Team Structure
چرا Contract Testing در Microservices مهمتر میشود؟ 🏗️
در یک Monolith ساده:
Application
└── Everything Together
تغییرات معمولاً در یک Repository و یک Deployment انجام میشوند.
اما در Microservices:
Service A ──► Service B
│ │
▼ ▼
Service C ──► Service D
هر سرویس ممکن است:
- Repository جدا داشته باشد.
- Team جدا داشته باشد.
- Release جدا داشته باشد.
- Version جدا داشته باشد.
- تکنولوژی متفاوت داشته باشد.
در نتیجه Compatibility اهمیت بسیار بیشتری پیدا میکند.
Contract Testing در Distributed Systems 🌐
در یک سیستم Distributed، بسیاری از مشکلات در مرز بین سیستمها اتفاق میافتند.
Service A
│
│ Expected:
│ amount = number
▼
Service B
│
│ Actual:
│ amount = string
▼
Compatibility Failure
Contract Testing دقیقاً برای چنین مرزهایی بسیار مناسب است.
یک نکته مهم: Contract Testing همه مشکلات Integration را پیدا نمیکند ⚠️
فرض کنیم Contract کاملاً Pass است.
اما در Production ممکن است مشکلات دیگری رخ دهند:
Consumer
↓
Provider
↓
Database Timeout
Provider
↓
Third-party API unavailable
Network latency
↓
Timeout
Contract Testing الزاماً این مشکلات Runtime را پیدا نمیکند.
بنابراین Contract Testing باید بخشی از یک Testing Strategy جامع باشد.
بهترین نگاه به Contract Testing 🎯
به جای اینکه بگوییم:
«Contract Testing بهترین نوع تست برای Microservices است.»
بهتر است بگوییم:
Contract Testing یکی از بهترین تکنیکها برای مدیریت ریسک Compatibility بین سرویسهای مستقل است.
این تعریف دقیقتر است.
🎯 یک تصمیمگیری ساده
«آیا Unit Test لازم است؟»
تقریباً همیشه بله.
«آیا Integration Test لازم است؟»
برای Integrationهای واقعی، معمولاً بله.
«آیا API Test لازم است؟»
برای APIهای مهم، معمولاً بله.
«آیا E2E Test لازم است؟»
برای Business Flowهای Critical، معمولاً تعدادی E2E لازم است.
«آیا Contract Test لازم است؟»
اینجا پاسخ:
بستگی به معماری و ریسک دارد.
اگر:
- Microservices زیاد دارید.
- Consumer و Provider مستقل هستند.
- تیمها جدا هستند.
- Releaseها مستقل هستند.
- Breaking Change پرهزینه است.
Contract Testing میتواند ارزش بسیار زیادی داشته باشد. ⭐
🧪 نتیجه برای QA Engineer
یک QA Engineer حرفهای نباید فقط بداند:
«چطور Pact را اجرا کنم؟»
بلکه باید بتواند تشخیص دهد:
چه چیزی را باید Contract کنیم؟
و حتی مهمتر:
چه چیزی را نباید Contract کنیم؟
این تفاوت بین Tool User و Test Engineer است.
ابزار فقط نحوه اجرای تست را مشخص میکند؛ اما QA باید درباره Risk، Scope، Coverage و Test Strategy تصمیم بگیرد.
جمعبندی نهایی این مقایسه
Contract Testing:
- ❌ جای Unit Testing نیست.
- ❌ جای Integration Testing نیست.
- ❌ جای API Testing نیست.
- ❌ جای E2E Testing نیست.
بلکه در کنار آنها قرار میگیرد.
تمرکز اصلی آن:
اطمینان از Compatibility بین Consumer و Provider
است.
و در معماریهای Distributed، Microservices و Event-Driven، این مسئله میتواند یکی از مهمترین ریسکهای سیستم باشد.
بنابراین یک Testing Strategy مناسب ممکن است چنین ترکیبی داشته باشد:
Unit
↓
Component
↓
Contract
↓
API / Integration
↓
E2E
البته این ترتیب یک Pipeline اجباری نیست؛ هر تست باید در جایی قرار گیرد که بیشترین Feedback و Risk Reduction را با کمترین هزینه ایجاد کند.
جمعبندی نهایی 🎯
Contract Testing یکی از تکنیکهای مهم برای کاهش ریسک Integration در سیستمهایی است که چند سرویس یا Application با یکدیگر ارتباط دارند. هدف اصلی آن این نیست که عملکرد کامل Provider یا کل Business Flow سیستم را بررسی کند؛ بلکه تمرکز آن بر این است که Consumer و Provider همچنان مطابق توافق مشترک خود با یکدیگر سازگار باشند.
این موضوع در معماریهای Microservices، Distributed Systems و Event-Driven اهمیت بیشتری پیدا میکند؛ زیرا سرویسها معمولاً توسط تیمهای مختلف و با Release Cycleهای مستقل توسعه داده میشوند و یک تغییر کوچک در Interface میتواند چند Consumer را تحت تأثیر قرار دهد.
Contract Testing میتواند این نوع مشکلات را زودتر و معمولاً با هزینه کمتر از برخی Integration یا E2E Testهای گسترده شناسایی کند. در رویکردهایی مانند Pact، Consumer نیازهای خود را در قالب Interactionها تعریف میکند، Contract تولید میشود و Provider در مرحله Verification بررسی میکند که آیا میتواند این انتظارات را برآورده کند یا خیر.
Consumer
↓
Contract
↓
Provider Verification
↓
Compatibility ✅
با این حال، Contract Testing نباید بهعنوان جایگزین Unit، Integration، API یا E2E Testing در نظر گرفته شود. هر کدام از این تکنیکها سؤال متفاوتی را پاسخ میدهند و یک Testing Strategy مناسب باید بر اساس Risk، Architecture، Business Impact و Cost of Failure ترکیبی از آنها را انتخاب کند.
از طرف دیگر، ارزش Contract Testing فقط در ایجاد چند تست خودکار خلاصه نمیشود. زمانی که Contract Verification در CI/CD قرار میگیرد و وضعیت Compatibility بین نسخههای مختلف Consumer و Provider مدیریت میشود، تیم میتواند قبل از Deployment متوجه شود که آیا یک تغییر احتمالاً Integrationهای موجود را میشکند یا خیر. Pact Broker دقیقاً برای مدیریت Contractها و Verification Resultها و استفاده از آنها در فرآیند Deployment طراحی شده است.
بنابراین مهمترین سؤال برای یک QA Engineer این نیست که فقط «چطور Contract Test بنویسم؟»؛ بلکه سؤال مهمتر این است که:
کدام Integrationها بیشترین Risk را دارند و چگونه میتوان با کمترین هزینه، Compatibility آنها را بهصورت قابل اعتماد بررسی کرد؟
اگر Contract Testing با همین نگاه استفاده شود، میتواند به جای افزایش بیدلیل تعداد تستها، به ایجاد یک Testing Strategy سریعتر، قابلاعتمادتر و متناسب با معماری سیستم کمک کند. ⭐
در نهایت، Contract Testing یک تکنیک مستقل از سایر تستها نیست؛ بلکه بخشی از یک استراتژی بزرگتر برای ایجاد Fast Feedback، کاهش Integration Risk و جلوگیری از Breaking Changes است.
Unit
↓
Component
↓
Contract
↓
API / Integration
↓
E2E
هدف:
Maximum Risk Reduction
with
Minimum Testing Cost
منابع و مراجع 📚
- Pact Documentation — مستندات رسمی Pact درباره Contract Testing، Consumer و Provider، نحوه ایجاد Contract و فرآیند Verification. [Pact Documentation]?
- Pact — Consumer Testing — راهنمای رسمی برای ایجاد Contract از سمت Consumer و تعریف Interactionها. [Pact Consumer Testing]
- Pact — Provider Testing — مستندات رسمی درباره Verification کردن Contract در Provider. [Pact Provider Testing]
- Pact Broker Documentation — نحوه مدیریت Contractها، Verification Resultها و استفاده از Contract Testing در CI/CD. [Pact Broker]()
- Martin Fowler — Contract Test — توضیح مفاهیم Contract Testing و Consumer-Driven Contracts. [Martin Fowler — Contract Test])
منابع فوق برای مطالعه بیشتر و بررسی جزئیات فنی Contract Testing و Pact پیشنهاد میشوند.
