یک استقرار اخیر باعث از کار افتادن سه میکروسرویس شد، با وجود اینکه تمام تستهای واحد، تستهای یکپارچهسازی و بررسیهای سرورِ شبیهساز (mock-server) با موفقیت انجام شده بودند. تیم آنها جایگزین کردن Mockهای API خود با تست قرارداد (contract testing) را انتخاب کرد. در عرض شش ماه، تعداد قراردادها از سه به ۴۷ افزایش یافت و نرخ شکستِ یکپارچهسازی ماهانه از دو مورد به صفر رسید.
چرا Mockها نمیتوانند از شما محافظت کنند
یک سرور Mock فقط شکلی را شبیهسازی میکند که مصرفکننده (consumer) انتظار دارد؛ اما هرگز بررسی نمیکند که آیا ارائهدهنده (provider) واقعاً همان شکل را ارائه میدهد یا خیر. اگر ارائهدهنده نام یک فیلد را تغییر دهد — مثلاً از name به display_name — Mock همچنان همان دادههای قدیمی را برمیگرداند، تستهای مصرفکننده سبز (موفق) باقی میمانند و سیستم واقعی از کار میافتد. شکست در محیط عملیاتی در ساعت ۲ بعدازظهر روز سهشنبه دقیقاً همین بود: Mock درباره قرارداد واقعی «دروغ» گفته بود.
تست قرارداد شکاف موجود را پر میکند
تست قرارداد دو طرف یک API را مجبور میکند تا پیش از آنکه هر کدی وارد محیط عملیاتی شود، بر سر یک تعریف مشترک توافق کنند. دو رویکرد رایج وجود دارد:
- Consumer-driven contracts – سرویس مصرفکننده انتظارات خود را مینویسد؛ سرویس ارائهدهنده آنها را اعتبارسنجی میکند. این روش برای میکروسرویسهای داخلی که با هم تکامل مییابند، بسیار مناسب است.
- Provider-driven contracts – ارائهدهنده یک مشخصات (specification) منتشر میکند؛ مصرفکنندگان کد خود را با آن مطابقت میدهند. این الگوی معمول برای APIهای عمومی است.
رویکرد اول معمولاً از شکست در یکپارچهسازیها در داخل یک معماری میکروسرویس جلوگیری میکند.
یک قرارداد مصرفکننده-محور چگونه کار میکند
- مصرفکننده تستی مینویسد که دقیقاً آنچه را از ارائهدهنده نیاز دارد، توصیف میکند.
- اجرای تست یک pact file ایجاد میکند – یک سند JSON که آن انتظارات را ثبت میکند.
- ارائهدهنده سرویس واقعی خود را در خط لوله CI در برابر فایل pact اجرا میکند.
- اگر ارائهدهنده فیلدی را تغییر دهد، اعتبارسنجی با شکست مواجه شده و فرآیند ساخت (build) متوقف میشود.
از آنجایی که اعتبارسنجی روی کد واقعی ارائهدهنده اجرا میشود، هر تغییر مخربی (breaking change) در مراحل اولیه شناسایی میشود، نه پس از استقرار.
جایگاه تستهای قرارداد در هرم تست شما
- Unit tests – سریع، منطق ایزوله را تست میکنند.
- Contract tests – سرعت متوسط، تایید میکنند که توافقات API برقرار هستند.
- End-to-end tests – کند، جریانهای کامل کسبوکار را اجرا میکنند.
با تستهای قرارداد مانند پلی بین بازخورد سریع تستهای واحد و پوشش گسترده مجموعههای تستِ انتها-به-انتها برخورد کنید. نقاط یکپارچهسازی که بیش از همه دچار مشکل میشوند را هدف قرار دهید و با دو یا سه نقطه اتصال (endpoint) حیاتی شروع کنید.
داستان بهکارگیری در دنیای واقعی
تیمی که جرقه این مقاله را زد، با سه قرارداد برای پوشش حساسترین فراخوانیهای خود شروع کرد. شش ماه بعد، آنها ۴۷ قرارداد داشتند که اکثر ترافیک بینسرویسی را پوشش میداد. در طول این دوره، حوادث شکست API از دو مورد در ماه به صفر کاهش یافت.
چه زمانی تست قرارداد ممکن است ارزش انجام دادن نداشته باشد
- شما یک توسعهدهنده تنها هستید که تمام سرویسها را در یک مخزن (repository) واحد نگه میدارید.
- API بسیار پایدار است و سالها تغییر نکرده است.
- در حال ساخت یک پروتوتایپ موقتی هستید که به زودی دور انداخته خواهد شد.
در این سناریوها، هزینه نگهداری قراردادها میتواند از مزیت آن بیشتر باشد.
معایب احتمالی و نحوه کاهش آنها
- قراردادها را همراه با کدی که توصیف میکنند، نسخهبندی کنید.
- برای جلوگیری از قدیمی شدن قراردادها، اعتبارسنجی را در هر اجرای CI خودکار کنید.
- تغییرات قرارداد را در Pull Requestها بررسی کنید تا از شکستهای تصادفی جلوگیری شود.
نتیجهگیری
اگر هنوز برای متقاعد کردن خودتان مبنی بر اینکه سرویسهایتان میتوانند با هم صحبت کنند، به Mockهای دستی تکیه میکنید، در حال شرطبندی روی یک وعده دروغین هستید. تست قرارداد آن شرطبندی را به یک توافق قابل تایید تبدیل میکند، تغییرات مخرب را پیش از رسیدن به محیط عملیاتی شناسایی میکند و همانطور که اعداد این تیم نشان میدهد، میتواند شکستهای یکپارچهسازی را به کلی از بین ببرد.
