با این حال، ارزش عملی خود ابزار جای بحث نداره؛ Quick Tunnels یه جایگزین رایگان و بدون نیاز به اکانت برای ابزارهایی مثل Ngrok حساب میشه، و مستندات رسمی هم صریحاً یادآوری میکنن که این تونلها موقتی هستن و هیچ SLA یا تضمین uptimeای براشون وجود نداره — یعنی برای production نباید بهشون تکیه کرد، فقط برای تست، دمو، و کارهای کوتاهمدت. اگه میخوای همین امروز امتحانش کنی، فقط کافیه سرویس local خودت رو بالا بیاری و دستور تونل رو روی همون پورت اجرا کنی؛ در عرض چند ثانیه یه لینک عمومی میگیری که میتونی همونجا با هرکسی به اشتراک بذاری.
end-to-end test
end-to-end یا E2E test رفتار واقعی کاربر رو توی یه محیط کامل شبیهسازی میکنه — از باز کردن یه صفحهی ساده تا سناریوهای پیچیدهتر مثل پرداخت آنلاین یا دریافت ایمیل تأیید. این تستها خیلی مفیدن، ولی هم گرون هستن و هم نگهداریشون سخته؛ برای همین توصیهی رایج اینه که فقط چند تا سناریوی کلیدی رو با E2E پوشش بدی و بیشتر تکیهت روی یونیت و integration تست باشه که سریعتر مشکل رو نشونت میدن.
acceptance test
acceptance test یه تست رسمیتره که چک میکنه آیا کل سیستم نیازمندیهای کسبوکار رو برآورده میکنه یا نه؛ نیاز داره کل اپلیکیشن در حال اجرا باشه و روی رفتار واقعی کاربر تمرکز داره. بعضی وقتها فراتر هم میره و performance سیستم رو هم اندازه میگیره، و اگه به یه هدف مشخص نرسه، تغییرات رو رد میکنه.
performance test
performance test میسنجه سیستم زیر یه بار مشخص چطور رفتار میکنه — قابلیت اطمینان، سرعت، مقیاسپذیری و responsiveness. مثلاً میتونه زمان پاسخدهی رو زیر تعداد زیادی request بسنجه، یا رفتار سیستم رو با حجم زیادی داده تست کنه. اینجور تستها کمک میکنن گلوگاهها (bottleneck) رو پیدا کنی و پایداری سیستم رو زیر ترافیک اوج بسنجی.
smoke test
smoke test یه تست پایه و سریعه که فقط چک میکنه عملکردهای اصلی سیستم کار میکنن یا نه. هدفش این نیست که همهچیز رو دقیق بررسی کنه، فقط میخواد یه اطمینان اولیه بده. جای خوب استفادهازش درست بعد از یه build جدید (برای تصمیمگیری که آیا ارزش داره تستهای گرونتر رو اجرا کنی یا نه) یا بلافاصله بعد از یه deploy هست، تا مطمئن بشی محیط جدید درست بالا اومده.
اتوماسیون این تستها چطور انجام میشه؟
برای خودکارسازی این تستها، اول باید اونها رو با یه framework مناسب زبانت بنویسی — چیزهایی مثل PHPUnit برای PHP، Mocha برای جاوااسکریپت، یا RSpec برای روبی. بعد از اینکه تستهات از طریق ترمینال و با یه دستور ساده قابل اجرا شدن، میتونی بذاری یه CI server یا سرویس ابری مثل Bitbucket Pipelines هر بار که یه تغییر جدید push میشه، خودکار اجراشون کنه.
exploratory testing، جایی که تست دستی هنوز جواب میده
با اینکه اتوماسیون کلید اصلی مقیاسپذیری تسته، این به این معنی نیست که تست دستی کاملاً بیفایده شده. اینجاست که exploratory testing وارد میشه — یه رویکرد دستی و بدون اسکریپت از پیش نوشتهشده که هدفش پیدا کردن باگهای غیربدیهیه، چیزهایی که هیچ تست خودکاری از قبل براش نوشته نشده چون کسی اصلاً فکرش رو نکرده بود. یه توصیهی عملی برای این نوع تست اینه که هر نشست exploratory بیشتر از دو ساعت طول نکشه و یه محدودهی مشخص داشته باشه، تا تسترها روی یه بخش خاص از سیستم تمرکز کنن.
یه نکتهی مهم دربارهی هدف تست
یه چیزی که خیلی وقتها فراموش میشه اینه که تست فقط برای چک کردن «آیا کاربر میتونه کارشو انجام بده» نیست؛ به همون اندازه مهمه که چک کنی اپلیکیشن با دادهی بد یا رفتار غیرمنتظره خراب نمیشه. باید پیشبینی کنی وقتی کاربر یه تایپو میزنه، یه فرم ناقص رو submit میکنه، یا از یه API به شکل اشتباه استفاده میکنه چه اتفاقی میفته، و اینکه آیا کسی میتونه راحت دادهای رو دستکاری کنه یا به منبعی که نباید بهش دسترسی داشته باشه برسه. یه test suite خوب باید تلاش کنه اپ رو بشکنه، نه فقط ثابت کنه کار میکنه.
یه نکتهی آخر هم هست که ارزش تکرار داره: تستها هم خودشون کد هستن. یعنی موقع code review نباید فراموششون کنی — گاهی همین تستها آخرین دروازهای هستن که جلوی یه باگ رو قبل از رسیدن به production میگیرن.
این تقسیمبندی که اینجا مرور کردیم، بر پایهی یه راهنمای خوب از Atlassian دربارهٔ انواع تست نرمافزاره؛ اگه دوست داری جزئیات بیشتری از دید خود Atlassian ببینی، میتونی این مقاله رو هم بخونی.
نکتهی requestCert: true و rejectUnauthorized: true اینجاست که اگه کلاینت certificate معتبر نداشته باشه، اصلاً اجازهی وصل شدن پیدا نمیکنه — قبل از اینکه حتی یه خط از کد اپلیکیشنت اجرا بشه. برای API های داخلی حساس، mTLS یکی از قویترین گزینههاست. مشکلش اینه که مدیریت certificate ها (تمدید، revoke کردن، rotate کردن) خودش یه پروژهی جداست؛ اگه چند تا سرویس داری و مدیریتش برات سخته، ابزارهایی مثل service mesh (Istio، Linkerd) این کار رو خودکار میکنن.
از JWT کاربر برای ارتباط سرویس به سرویس استفاده نکن
یه اشتباه رایج اینه که همون JWT ای که برای احراز هویت کاربر نهایی صادر شده رو برای ارتباط بین دو تا سرویس backend هم دوبارهکاری میکنن — یعنی چیزی شبیه Authorization: Bearer <user-jwt> رو بین سرویسها پاس میدن. این کار مرز بین «هویت کاربر» و «هویت سرویس» رو از بین میبره و باعث میشه permission های کاربر و سرویس قاطی بشن. به جاش باید یه credential مخصوص machine-to-machine داشته باشی. گزینههای خوب اینجا شامل OAuth 2.0 Client Credentials، mTLS، درخواستهای امضاشده (signed requests)، ترکیب API key و HMAC، و توکنهای کوتاهمدت سرویسمحور هستن.
برای بیشتر زیرساختهای متوسط، ترکیب OAuth 2.0 Client Credentials بههمراه HTTPS انتخاب معقولیه؛ برای ارتباطات داخلی که واقعاً حساس هستن، mTLS بههمراه توکنهای کوتاهمدت گزینهی قویتریه.
هر سرویس هویت خودش رو داشته باشه
یه اشتباه دیگه که خیلی رایجه: یه کلید API واحد و master برای همهچیز. چیزی شبیه یه متغیر محیطی به اسم MASTER_API_KEY که همهی سرویسها ازش استفاده میکنن. مشکل این روش اینه که وقتی یکی از سرویسها compromise بشه، عملاً کل سیستم compromise شده — چون همه از یه کلید استفاده میکردن.
راه درستتر اینه که هر سرویس یه service account مخصوص خودش داشته باشه، و به هر کدوم فقط همون permission هایی که واقعاً بهش نیاز داره داده بشه. مثلاً اگه سرویس A فقط باید کاربر بسازه و بخونه، نباید اصلاً امکان حذف کاربر یا دسترسی به بخش پرداخت رو داشته باشه:
سرویس A → مجاز به: create_user, read_user, update_access
سرویس A → غیرمجاز به: delete_user, access_payments, access_admin, access_database
به این اصل، least privilege میگن: هر جزء از سیستم فقط دسترسیای رو داشته باشه که برای انجام کارش لازمه، نه بیشتر. این سادهترین و در عین حال مؤثرترین اصلی هست که خیلی از تیمها فراموشش میکنن.
احراز هویت با تعیین دسترسی فرق داره
احراز هویت (authentication) به این سؤال جواب میده: «تو کی هستی؟» و تعیین دسترسی (authorization) به این سؤال: «تو اجازهی چه کاری رو داری؟» یه فرض اشتباه رایج اینه که فکر کنیم چون یه request احراز هویت شده، پس قابل اعتماده. این درست نیست. باید هر endpoint جداگانه چک بشه که آیا سرویسی که این درخواست رو فرستاده، مجوز انجام همین عملیات خاص رو داره یا نه. مثلاً POST /users ممکنه نیاز به مجوز user:create داشته باشه، در حالی که DELETE /users/:id ممکنه به یه مجوز کاملاً متفاوت نیاز داشته باشه که خیلی محدودتر بهش داده میشه.
امضای درخواستها با HMAC
برای APIهایی که سطح امنیتی بالاتری میخوان، امضای درخواست (request signing) با HMAC یه لایهی اضافهی خیلی خوبیه. ایده اینه که هر دو سرویس یه secret مشترک دارن، و هر request قبل از ارسال با اون secret امضا میشه:
سمت سرور، همین امضا دوباره از روی method، path، timestamp و body محاسبه میشه و با چیزی که تو header اومده مقایسه میشه. اگه یکی وسط راه request رو بگیره و مثلاً ایمیل داخل body رو عوض کنه، امضا دیگه match نمیکنه و request رد میشه — بدون اینکه اصلاً لازم باشه محتوای request رمزنگاری بشه؛ همین که تغییرناپذیریش تضمین بشه کافیه.
محافظت در برابر replay attack
فقط امضا کردن request کافی نیست. اگه کسی یه request معتبر و امضاشده رو ضبط کنه و بعداً دوباره همونو بفرسته، امضا هنوز معتبره چون چیزی توش عوض نشده. این همون replay attack هست. راهحلش ترکیب سه چیزه: timestamp، یه شناسهی یکتا برای هر request (نانس یا request-id)، و امضا:
منطق سمت سرور این شکلیه: اول چک میشه timestamp توی یه بازهی معقول هست (مثلاً کمتر از پنج دقیقه اختلاف با زمان فعلی سرور)، بعد چک میشه که این request-id قبلاً پردازش نشده (این بخش معمولاً با یه cache سریع مثل Redis پیاده میشه، با یه TTL برابر با همون بازهی زمانی)، و اگه هر دو شرط برقرار بود، امضا verify میشه و در نهایت request-id ذخیره میشه تا دفعهی بعد تکراری تشخیص داده بشه. بدون این مکانیزم، حتی امضای HMAC هم در برابر یه حملهی سادهی «ضبط و پخش دوباره» بیدفاعه.
شبکه رو محدود کن، این شاید مهمترین قدمه
اگه سرویس B فقط قراره از طرف سرویس A صدا زده بشه، هیچ دلیلی نداره که اصلاً از اینترنت عمومی در دسترس باشه. این یکی از نکاتی هست که تأثیرش روی امنیت کلی سیستم از خیلی چیزهای دیگه بیشتره، چون هر لایهی امنیتی دیگهای که بذاری (authentication، rate limit، signature) باز هم در معرض حمله قرار میگیره اگه شبکه بهطور عمومی باز باشه.
ابزارهایی که برای این کار به کار میان شامل قوانین فایروال، security group های ابری، شبکهی خصوصی (VPC)، VPN یا WireGuard، IP allowlist، و DNS داخلی هستن. ایدهآل اینه که ترافیک بین دو سرویس فقط از یه شبکهی خصوصی رد بشه، نه از مسیر عمومی اینترنت که بعد پشت فایروال فیلتر بشه. این دو تا از نظر امنیتی خیلی با هم فرق دارن، حتی اگه نتیجهی نهایی مشابه به نظر برسه.
هیچوقت دسترسی مستقیم دیتابیس نده
یه اشتباه خطرناک که گاهی توی معماریهای عجلهای دیده میشه اینه که یه سرویس بیرونی، مستقیماً credential دیتابیس سرویس دیگه رو داشته باشه. حتی اگه این credential محدود به یه کاربر readonly باشه، این کار لایهی authorization اپلیکیشن رو کاملاً دور میزنه. مسیر درست همیشه از طریق API میگذره: سرویس A فقط باید از طریق HTTPS و API عمومی سرویس B باهاش حرف بزنه، و سرویس B خودش تصمیم میگیره که این عملیات با توجه به authorization چطور روی دیتابیس اجرا بشه. سرویس A هیچوقت نباید بتونه کاری بیشتر از اونی که API صراحتاً exposed کرده انجام بده.
اعتماد کورکورانه به ورودی، حتی از سرویس داخلی
حتی اگه یه request از یه سرویس authenticated بیاد، بازم باید ورودیهاش validate بشن — schema، طول رشتهها، فرمت شناسهها، URL ها، اسم فایلها، enum ها، بازهی عددی، و حجم payload. یه نکتهی خاص اینجا مربوط به SSRF هست: اگه سرور تو بر اساس یه URL که از سمت کاربر یا سرویس دیگه اومده (مثلاً یه فیلد callback_url) یه request جدید میسازه، بدون validate کردن اون آدرس ممکنه یه آسیبپذیری SSRF ایجاد بشه که مهاجم بتونه از طریق سرورت به منابع داخلی شبکه دسترسی پیدا کنه که اصلاً نباید بهشون دسترسی داشته باشه.
rate limit فقط برای ترافیک بیرونی نیست
یه سرویس authenticated لزوماً امن نیست. اگه سرویس A به هر دلیلی — یه باگ، یه حلقهی بینهایت، یه retry logic خراب — شروع کنه به بمباران کردن سرویس B، معتبر بودن token یا امضا هیچ کمکی نمیکنه. گذاشتن rate limit حتی روی ترافیک کاملاً داخلی، چیزیه که خیلی وقتها دیر بهش فکر میکنیم؛ معمولاً بعد از اینکه یه بار یه سرویس داخلی کل کلاستر رو از پا درآورده. منطقیترین کار اینه که برای هر endpoint بر اساس هزینهی محاسباتیش یه سقف جدا تعریف کنی؛ یه request خواندنی ساده و یه عملیات export سنگین طبیعتاً نباید سقف یکسانی داشته باشن.
لاگ کردن، اما نه هر چیزی
وقتی یه چیزی خراب میشه، اولین جایی که سراغش میری لاگهاست. صرفاً لاگ کردن مسیر و متد request (POST /users) کافی نیست؛ چیزهایی که واقعاً کمککننده هستن شامل timestamp، اسم سرویس فرستنده، endpoint، شناسهی یکتای request، نتیجهی عملیات، تصمیم authorization، آدرس IP مبدأ، و latency میشن. نکتهی خیلی مهم اینه که هیچوقت secret ها یا credential های حساس رو توی لاگ ننویسی — این خودش یه منبع رایج نشت اطلاعاته که کمتر بهش توجه میشه.
مقایسهی روشهای ارتباطی
انتخاب پروتکل بین دو سرویس بیشتر به الگوی استفاده بستگی داره تا امنیت، هرچند بعضی گزینهها ذاتاً برای یه سناریوی خاص مناسبترن:
روش
مناسب برای
REST روی HTTPS
APIهای عمومی پلتفرم، انعطافپذیر و پرکاربرد
GraphQL روی HTTPS
کوئریهای منعطف سمت کلاینت
gRPC با mTLS
ارتباط سرویسبهسرویس داخلی، latency پایین
WebSocket
ارتباط real-time دوطرفه
صف پیام (Message Queue)
عملیات async و پسزمینهای
اتصال مستقیم به دیتابیس
باید کاملاً پرهیز بشه
HTTP بدون TLS
هیچوقت
برای ارتباط بین دو پلتفرم مستقل، REST بههمراه HTTPS و OAuth2 (یا mTLS برای موارد حساستر) یه انتخاب کاملاً محکمه. برای میکروسرویسهای داخلی که تعداد زیادی درخواست کوچیک و پرتکرار با هم رد و بدل میکنن، gRPC یا یه message broker معمولاً منطقیتره.
نکتهای که در آخر ارزش تکرار کردن داره: هدف طراحی یه API که «هک کردنش غیرممکنه» نیست — چنین چیزی وجود خارجی نداره. هدف اینه که سیستم رو طوری بچینی که compromise شدن یه جزء، خودکار به compromise شدن همهچیز منجر نشه. ایزوله کردن شبکه، least privilege، credential های جداگانه برای هر سرویس، محدود کردن دسترسی دیتابیس، و دفاع در عمق، در عمل خیلی بیشتر از انتخاب REST یا GraphQL یا هر پروتکل دیگهای روی امنیت واقعی سیستم تأثیر میذارن.
از نظر performance، سرعت Marshal تقریباً همسطح پیادهسازی قبلیه، ولی Unmarshal بهطور محسوسی سریعتر شده.
بهبودهای Runtime
تخصیص حافظهی سریعتر: کامپایلر حالا برای تخصیصهای کوچیک (زیر ۸۰ بایت)، فراخوانیهایی به روتینهای تخصیص size-specialized تولید میکنه؛ این کار هزینهی بعضی از این تخصیصها رو تا ۳۰ درصد کم میکنه. طبق گزارشها، بهبود کلی رو برنامههای واقعی و allocation-heavy حدود ۱ درصده، با هزینهی حدود ۶۰ کیلوبایت حجم اضافهی باینری.
پروفایل نشتی goroutine: قابلیتی که بهصورت آزمایشی تو Go 1.26 معرفی شده بود، حالا به یه پروفایل رسمی تبدیل شده. پکیج runtime/pprof الان یه پروفایل goroutineleak داره که با اجرای یه چرخهی garbage collection، goroutine هایی که برای همیشه بلاک شدن رو شناسایی میکنه؛ ابزار مفیدی برای دیباگ کردن نشتیهای goroutine تو production.
SIMD: از amd64 به arm64 و WebAssembly
پشتیبانی آزمایشی SIMD که از Go 1.26 تو پکیج simd/archsimd شروع شده بود، تو این نسخه گسترش پیدا کرده. علاوه بر بازنگری API مربوط به amd64، حالا از SIMD ۱۲۸ بیتی «Neon» رو arm64 و SIMD ۱۲۸ بیتی رو WebAssembly هم پشتیبانی میشه. این پکیج با فعال کردن متغیر محیطی GOEXPERIMENT=simd موقع build در دسترسه، ولی API ش هنوز پایدار در نظر گرفته نمیشه. یه پکیج جدید دیگه هم به اسم simd معرفی شده که پشتیبانی SIMD قابلحمل و مستقل از سایز بردار ارائه میده؛ رو سختافزارهایی که دسترسی دارن، به دستورات vector واقعی کامپایل میشه و رو بقیه، به emulation خالص Go برمیگرده.
کتابخونهی استاندارد: چند تا اضافهی کاربردی
پکیج uuid: حالا تولید و parse کردن UUID مستقیم تو کتابخونهی استاندارده؛ دیگه نیازی به یه dependency خارجی برای این کار رایج نیست.
crypto/mldsa: پشتیبانی از یه الگوریتم امضای دیجیتال پسا-کوانتومی (ML-DSA) به کتابخونهی استاندارد کریپتوگرافی اضافه شده؛ گامی مهم برای آمادهسازی کدبیسها در برابر تهدید کامپیوترهای کوانتومی.
CutLast: یه تابع جدید که یه slice بایت رو حول آخرین رخداد یه جداکننده برش میده (برخلاف Cut معمولی که اولین رخداد رو در نظر میگیره).
Toolchain و ابزارها
go mod tidy هوشمندتر: برای ماژولهایی که رو Go 1.27 یا بالاتر هستن، این دستور حالا بلوکهای require پراکنده رو تو دو بلوک استاندارد (یکی برای وابستگی مستقیم، یکی غیرمستقیم) ادغام میکنه، در حالی که کامنتهای ضمیمهشده رو هم حفظ میکنه.
go tool trace امنتر: وقتی فقط یه پورت مشخص کنی، رابط trace حالا فقط رو localhost گوش میده، هماهنگ با رفتار go tool pprof؛ برای گوش دادن گستردهتر، باید صراحتاً یه آدرس مشخص بدی.
حذف پشتیبانی Bazaar: دستور go دیگه از سیستم کنترل نسخهی Bazaar (bzr) پشتیبانی نمیکنه.
پشتیبانی Response file: ابزارهای compile، link، asm، cgo، cover، و pack حالا از response file (با فرمت سازگار با GCC) پشتیبانی میکنن؛ کمک بزرگی برای سیستمهای build ای که به محدودیت طول خط فرمان برخورد میکنن.
بهبودهای go fix: این دستور modernizer های جدیدی گرفته که کمک میکنن کد قدیمیتر رو به الگوهای مدرنتر Go آپدیت کنی.
تغییرات پلتفرم
طبق گزارشها، این نسخه الزامات پلتفرمی برای macOS رو هم تغییر داده (جزئیات دقیق تو یادداشتهای رسمی release اومده)، پس اگه رو مک کار میکنی، قبل از آپگرید، حتماً حداقل نسخهی موردنیاز رو چک کن.
چطور همین الان امتحانش کنیم
باینریها و نصبکنندههای Go 1.27 از صفحهی دانلود رسمی (go.dev/dl) در دسترسن. اگه از JetBrains GoLand استفاده میکنی، نسخهی 2026.2 همین الان از ویژگیهای زبانی جدید، modernizer های go fix، و پروفایل جدید goroutine leak پشتیبانی میکنه. تیم Go هم بیستوپنجم آگوست، از ساعت ۱۶:۰۰ تا ۱۷:۳۰ به وقت UTC، یه «Go 1.27 Release Party» آنلاین برگزار میکنه که میشه توش با جزئیات بیشتر این تغییرات آشنا شد.
جمعبندی
Go 1.27 با حدود ۱۶۰۰ کامیت بین این نسخه و Go 1.26، یکی از نسخههایییه که واقعاً چیزی فراتر از بهبودهای معمولی نگهداری داره. متدهای generic، نسل جدید JSON، و بهبود چشمگیر تخصیص حافظه، همه چیزهایی هستن که مستقیم رو کد روزمرهی خیلی از تیمها تأثیر میذارن. با این حال، بهخاطر تغییر پیشفرضهای سختگیرانهی JSON v2 (رد کردن UTF-8 نامعتبر و کلیدهای تکراری)، قبل از آپگرید پروژههای production، ارزش داره تستهات رو کامل اجرا کنی تا مطمئن بشی دادهی ورودیت با این استانداردهای جدید مشکلی نداره.
نگرانی جدی: بدون شرایط مشخص برای داده
مهمترین نکتهای که باید قبل از استفادهی جدی از Origin بهش توجه کنی، مربوط به دادهست. طبق گزارش TechTimes، Origin با enrollment بهصورت opt-out (نه opt-in) برای همهی کاربران پولی فعال شده؛ یعنی بهصورت پیشفرض روشنه، مگه اینکه ادمین سازمانت صراحتاً غیرفعالش کنه. در همون زمان انتشار، کرسر هیچ سند رسمیای دربارهی نگهداری داده (data retention)، افشای subprocessor ها، سیاست استفاده از کد برای آموزش مدل، یا ابزار migration برای کدهایی که مستقیم رو Origin هاست میشن منتشر نکرده بود.
این نگرانی برای تیمهایی که کد اختصاصی، دادهی شخصی کاربر تو فیکسچرهای تست، credential های احتمالی تو تاریخچهی commit، یا کدهای با محدودیتهای قانونی/رگولاتوری دارن، جدیتره؛ چون الان کدشون رو زیرساختی هاست میشه که متعلق به SpaceX است، بدون اینکه سیاست مشخصی دربارهی نحوهی استفاده از اون کد منتشر شده باشه.
دسترسی
Origin فقط برای پلنهای Pro، Teams، و Enterprise در دسترسه، نه پلن رایگان. برای سازمانهای Enterprise، ادمینها میتونن صراحتاً opt-out کنن. برای شروع، باید یه اسم codebase (namespace) رو claim کنی که فعلاً تو دورهی بتا قابل تغییر نیست، و بعد از اون میتونی ریپازیتوری جدید بسازی یا از طریق OAuth، ریپازیتوریهای گیتهابت رو sync کنی.
جمعبندی
Origin نشون میده کرسر (حالا زیر پرچم SpaceX) قصد داره فراتر از یه ادیتور کدنویسی بره و مستقیم وارد قلمروی زیرساخت توسعهی نرمافزار (همون چیزی که گیتهاب و گیتلب سالهاست توش تسلط دارن) بشه. برای تیمهایی که کاملاً درگیر اکوسیستم کرسر هستن و از agent های کدنویسی زیاد استفاده میکنن، یکپارچگی ریپازیتوری/PR/agent تو یه محیط واحد میتونه جذاب باشه. ولی با توجه به اینکه این محصول هنوز early beta است و مهمتر از همه، هیچ سند رسمیای دربارهی شرایط داده منتشر نشده، توصیه میشه قبل از هاست کردن کد حساس یا اختصاصی رو Origin، منتظر انتشار این سیاستها بمونی یا حداقل از تیم پشتیبانی کرسر مستقیم دربارهش سؤال کنی.
یه مثال ساده
فرض کن یه تابع محاسباتی سنگین به Rust داری:
#[no_mangle]
pub fn fibonacci(n: u32) -> u64 {
if n <= 1 {
return n as u64;
}
let mut a = 0u64;
let mut b = 1u64;
for _ in 2..=n {
let temp = a + b;
a = b;
b = temp;
}
b
}
با ابزاری مثل wasm-pack، این کد به یه فایل .wasm compile میشه که میشه مستقیم تو جاوااسکریپت import و صداش زد:
import init, { fibonacci } from './pkg/my_wasm_module.js';
async function run() {
await init();
console.log(fibonacci(50)); // اجرا با سرعت نزدیک به native
}
run();
از دید جاوااسکریپت، fibonacci فقط یه تابع معمولیه، ولی پشت صحنه داره یه باینری کامپایلشده و بهینه اجرا میشه، نه کد تفسیرشدهی جاوااسکریپت.
کجاها واقعاً استفاده میشه
ابزارهای سنگین که به مرورگر آوردن: Figma بخش زیادی از موتور رندرش رو با WebAssembly پیاده کرده تا بتونه performance شبیه یه اپلیکیشن دسکتاپ رو تو مرورگر ارائه بده. AutoCAD نسخهی وب خودش رو با پورت کردن دههها کد C++ به Wasm ساخته.
بازیها و موتورهای گرافیکی: موتورهایی مثل Unity و Unreal Engine امکان export کردن بازی به WebAssembly رو دارن، که یعنی بازیهای نسبتاً سنگین میتونن مستقیم تو مرورگر، بدون نصب هیچ پلاگینی، اجرا بشن.
پردازش تصویر و ویدیو: ابزارهایی مثل ffmpeg.wasm امکان encode/decode کردن ویدیو مستقیم تو مرورگر رو میدن، بدون اینکه فایل به سرور آپلود بشه؛ هم سریعتره، هم حریم خصوصی کاربر حفظ میشه.
اجرای زبونهای دیگه تو مرورگر: پروژههایی مثل Pyodide، پایتون رو کامل به WebAssembly compile کردن، یعنی میتونی کد پایتون واقعی رو مستقیم تو مرورگر، بدون سرور، اجرا کنی.
فراتر از مرورگر: WebAssembly دیگه محدود به مرورگر نیست. با استانداردی به اسم WASI (WebAssembly System Interface)، میشه Wasm رو مستقل از مرورگر، رو سرور یا edge computing هم اجرا کرد. کلادفلر Workers و Fastly Compute دقیقاً از همین قابلیت استفاده میکنن تا کد رو تو صدها دیتاسنتر مختلف، با startup time خیلی کمتر از یه کانتینر معمولی، اجرا کنن.
چیزی که WebAssembly نیست
یه سوءتفاهم رایج اینه که فکر کنیم Wasm قراره جای جاوااسکریپت رو بگیره. اینطور نیست؛ Wasm نمیتونه مستقیم به DOM دسترسی داشته باشه یا event handler ثبت کنه. برای هر تعامل با صفحه (کلیک، تغییر استایل، دستکاری DOM)، هنوز باید از جاوااسکریپت بهعنوان واسط استفاده کنی. Wasm بیشتر مکمل جاوااسکریپته تا جایگزینش؛ جاهایی که محاسبات سنگین لازمه، کار رو به Wasm میسپاری و بقیهی تعامل با کاربر همچنان با جاوااسکریپت انجام میشه.
کِی واقعاً بهش نیاز داری
اگه داری یه اپلیکیشن وب معمولی میسازی (فرم، داشبورد، فروشگاه آنلاین)، به احتمال زیاد هیچوقت مستقیم با WebAssembly سروکار پیدا نمیکنی؛ جاوااسکریپت (یا فریمورکهای روش) کاملاً کافیه. Wasm زمانی وارد بازی میشه که یه بخش خاص از اپلیکیشنت واقعاً محاسبات سنگین داره (پردازش تصویر/صوت/ویدیو، رمزنگاری، فیزیک، parsing فایلهای حجیم) و همون بخش داره performance اپ رو محسوس کند میکنه. تو اون حالت، میتونی فقط همون بخش خاص رو به Rust یا C++ بنویسی و به Wasm compile کنی، بدون اینکه مجبور بشی کل اپلیکیشن رو از جاوااسکریپت خارج کنی.
# Driven port: domain فقط این interface رو میشناسه
from abc import ABC, abstractmethod
class OrderRepository(ABC):
@abstractmethod
def save(self, order: 'Order') -> None:
pass
@abstractmethod
def find_by_id(self, order_id: str) -> 'Order | None':
pass
class NotificationSender(ABC):
@abstractmethod
def send_order_confirmation(self, order: 'Order') -> None:
pass
پیادهسازی: هستهی domain
هستهی domain فقط به این port ها وابستهست، نه به هیچ پیادهسازی خاصی:
class Order:
def __init__(self, order_id: str, customer_email: str, total: float):
self.order_id = order_id
self.customer_email = customer_email
self.total = total
self.status = "pending"
class PlaceOrderService:
def __init__(self, repository: OrderRepository, notifier: NotificationSender):
self.repository = repository
self.notifier = notifier
def place_order(self, order_id: str, customer_email: str, total: float) -> Order:
if total <= 0:
raise ValueError("مبلغ سفارش باید مثبت باشه")
order = Order(order_id, customer_email, total)
self.repository.save(order)
self.notifier.send_order_confirmation(order)
return order
این کلاس PlaceOrderService هیچ ایدهای نداره که آیا OrderRepository واقعاً یه دیتابیس PostgreSQL رو صدا میزنه یا فقط یه dictionary تو حافظهست. برای همینه که تست کردنش خیلی سادهست:
class InMemoryOrderRepository(OrderRepository):
def __init__(self):
self.orders = {}
def save(self, order):
self.orders[order.order_id] = order
def find_by_id(self, order_id):
return self.orders.get(order_id)
class FakeNotifier(NotificationSender):
def __init__(self):
self.sent = []
def send_order_confirmation(self, order):
self.sent.append(order)
def test_place_order_saves_and_notifies():
repo = InMemoryOrderRepository()
notifier = FakeNotifier()
service = PlaceOrderService(repo, notifier)
order = service.place_order("ord_1", "ali@example.com", 150.0)
assert repo.find_by_id("ord_1") == order
assert len(notifier.sent) == 1
آداپترهای واقعی
حالا وقتی زمان استفادهی واقعی میرسه، همون port ها رو با پیادهسازی واقعی adapt میکنیم:
import psycopg2
class PostgresOrderRepository(OrderRepository):
def __init__(self, connection):
self.connection = connection
def save(self, order):
with self.connection.cursor() as cursor:
cursor.execute(
"INSERT INTO orders (id, email, total, status) VALUES (%s, %s, %s, %s)",
(order.order_id, order.customer_email, order.total, order.status)
)
self.connection.commit()
def find_by_id(self, order_id):
with self.connection.cursor() as cursor:
cursor.execute("SELECT * FROM orders WHERE id = %s", (order_id,))
row = cursor.fetchone()
return Order(*row) if row else None
class EmailNotifier(NotificationSender):
def __init__(self, smtp_client):
self.smtp_client = smtp_client
def send_order_confirmation(self, order):
self.smtp_client.send(
to=order.customer_email,
subject="تأیید سفارش",
body=f"سفارش {order.order_id} با موفقیت ثبت شد."
)
خود PlaceOrderService بدون هیچ تغییری، هم با نسخهی تستی کار میکنه و هم با نسخهی واقعی؛ فقط adapter ای که موقع wiring کردن اپلیکیشن بهش تزریق میشه فرق میکنه.
این معماری با Dependency Inversion Principle چه ربطی داره
اگه مقالهی SOLID رو خونده باشی، احتمالاً این الگو برات آشناست؛ معماری هگزاگونال عملاً یه اعمال سیستماتیک از Dependency Inversion Principle تو سطح کل اپلیکیشنه. بهجای اینکه domain به یه پیادهسازی خاص (مثل PostgreSQL) وابسته باشه، هم domain و هم پیادهسازی، به یه abstraction مشترک (port) وابستهن. جهت وابستگی همیشه به سمت داخل (به سمت domain) است؛ هیچوقت برعکس.
کجا این معماری واقعاً ارزش داره
این معماری برای اپلیکیشنهایی معنی داره که منطق کسبوکار پیچیده و قابلتغییری دارن، یا احتمال داره زیرساختهاشون (دیتابیس، پیامرسان، API های خارجی) تو طول زمان عوض بشه. سیستمهای مالی، پلتفرمهای e-commerce با قوانین قیمتگذاری پیچیده، یا هر جایی که تیم میخواد تستهای سریع و ایزوله داشته باشه، از این جداسازی سود میبره.
در مقابل، برای یه CRUD ساده که فقط داره داده رو از دیتابیس میخونه و نمایش میده، بدون منطق کسبوکار پیچیدهای، اضافه کردن لایههای port و adapter فقط پیچیدگی اضافه میکنه بدون فایدهی متناسب. مثل خیلی از الگوهای معماری دیگه، سؤال درست این نیست که «آیا این الگو خوبه؟»، بلکه اینه که «آیا پیچیدگی پروژهم این سرمایهگذاری رو توجیه میکنه؟»
این برعکس روشیه که خیلی از برنامهنویسها عادت دارن: اول کد رو مینویسن، بعد سعی میکنن براش تست بنویسن. تو اون حالت، تستها معمولاً فقط تأیید میکنن که کد چیکار میکنه (نه لزوماً چیزی که باید بکنه)، و چون کد از قبل نوشته شده، خیلی وقتها طراحیش برای تست کردن مناسب نیست؛ مثلاً پر از متدهای استاتیک یا وابستگی مستقیم به دیتابیس.
یه مثال واقعیتر: سرویس ثبتنام کاربر
بیایم یه ویژگی رو با TDD پیش ببریم که واقعاً تو یه پروژهی واقعی هم پیش میاد: سرویسی که یه کاربر جدید رو ثبتنام میکنه. این مثال عمداً یه dependency واقعی (دیتابیس) داره تا نشون بده TDD چطور طراحی رو به سمت قابلتستبودن هول میده.
اول، قبل از نوشتن هیچ کدی، مشخص میکنیم سرویس به چه چیزی نیاز داره: یه repository برای چک کردن و ذخیرهی کاربر. این dependency رو از همون اول بهصورت interface تزریق میکنیم، نه اینکه مستقیم داخل سرویس یه دیتابیس واقعی رو صدا بزنیم:
# قدم اول (Red): تستی برای رد کردن ایمیل تکراری
class FakeUserRepository:
def __init__(self, existing_emails=None):
self.existing_emails = existing_emails or set()
self.saved_users = []
def email_exists(self, email):
return email in self.existing_emails
def save(self, user):
self.saved_users.append(user)
def test_rejects_duplicate_email():
repo = FakeUserRepository(existing_emails={"ali@example.com"})
service = UserRegistrationService(repo)
result = service.register("ali@example.com", "password123")
assert result.success is False
assert result.error == "EMAIL_ALREADY_EXISTS"
assert len(repo.saved_users) == 0
چون UserRegistrationService هنوز وجود نداره، این تست fail میشه. حالا کمترین کد ممکن رو مینویسیم تا سبز بشه:
# قدم دوم (Green)
class RegistrationResult:
def __init__(self, success, error=None):
self.success = success
self.error = error
class UserRegistrationService:
def __init__(self, repository):
self.repository = repository
def register(self, email, password):
if self.repository.email_exists(email):
return RegistrationResult(success=False, error="EMAIL_ALREADY_EXISTS")
self.repository.save({"email": email, "password": password})
return RegistrationResult(success=True)
حالا یه تست جدید برای قانون بعدی اضافه میکنیم؛ رد کردن پسورد ضعیف:
def test_rejects_weak_password():
repo = FakeUserRepository()
service = UserRegistrationService(repo)
result = service.register("new@example.com", "123")
assert result.success is False
assert result.error == "PASSWORD_TOO_WEAK"
assert len(repo.saved_users) == 0
def test_successful_registration_saves_user():
repo = FakeUserRepository()
service = UserRegistrationService(repo)
result = service.register("new@example.com", "password123")
assert result.success is True
assert repo.saved_users[0]["email"] == "new@example.com"
با اضافه شدن این تستها، سرویس رو گسترش میدیم:
# قدم سوم (Green + Refactor)
class UserRegistrationService:
MIN_PASSWORD_LENGTH = 8
def __init__(self, repository):
self.repository = repository
def register(self, email, password):
if self.repository.email_exists(email):
return RegistrationResult(success=False, error="EMAIL_ALREADY_EXISTS")
if len(password) < self.MIN_PASSWORD_LENGTH:
return RegistrationResult(success=False, error="PASSWORD_TOO_WEAK")
self.repository.save({"email": email, "password": password})
return RegistrationResult(success=True)
نکتهی مهم اینجا FakeUserRepository است. چون از همون اول تصمیم گرفتیم دیتابیس رو بهصورت یه dependency قابل تزریق طراحی کنیم (نه اینکه مستقیم داخل سرویس به یه دیتابیس واقعی وصل بشیم)، تونستیم منطق ثبتنام رو کاملاً بدون نیاز به دیتابیس واقعی، سریع و ایزوله تست کنیم. این دقیقاً همون تأثیریه که TDD روی طراحی میذاره: مجبورت میکنه از اول به شکل قابلتست فکر کنی.
مزیتهایی که معمولاً کمتر بهشون اشاره میشه
فراتر از اینکه «باگ کمتر میشه»، TDD یه فایدهی مهم دیگه هم داره: تستها بهعنوان مستندات زنده عمل میکنن. وقتی یه توسعهدهندهی جدید به تیم میپیوندد و میخواد بفهمه یه تابع دقیقاً چه رفتاری داره، خوندن تستهاش خیلی وقتها روشنتر از خوندن کد اصلیه، چون تستها دقیقاً حالتهای مرزی و انتظارات رو مشخص میکنن.
فایدهی دیگه، اعتماد به refactor کردنه. تو کدبیسی که پوشش تست خوبی داره، تغییر دادن یه بخش از کد ترسناک نیست، چون اگه چیزی رو بشکنی، سریع خبردار میشی. بدون این پوشش، خیلی از تیمها از ترس خراب کردن چیزی، از refactor کردن کد قدیمی اجتناب میکنن، حتی وقتی میدونن که کد نیاز به تمیز شدن داره.
TDD موقع ساختن API
وقتی داری یه REST API میسازی، معمولاً با دو لایهی متفاوت طرفی: منطق کسبوکار (business logic) و لایهی HTTP که این منطق رو در قالب endpoint در معرض دید میذاره. اشتباه رایج اینه که TDD رو مستقیم روی endpoint اجرا کنیم؛ یعنی تست بنویسیم که یه request واقعی به /api/users میفرسته و response رو چک میکنه. این کار میشه، ولی اینجور تستها کند هستن (چون سرور واقعی بالا میاد) و وقتی fail میشن، سخته بفهمی مشکل تو لایهی HTTP بوده یا تو منطق کسبوکار.
روش بهتر، جدا کردن این دو لایه و تست کردن هر کدوم جداگانهست. لایهی business logic (مثل همون UserRegistrationService بالا) رو کاملاً بدون درگیر شدن با HTTP، با unit test پیش میبریم. بعد یه لایهی نازک controller/route داریم که فقط مسئول parse کردن request و صدا زدن سرویسه:
اینجا mock_service جای سرویس واقعی نشسته، پس این تستها فقط چک میکنن که controller درست request رو به سرویس پاس میده و status code مناسب رو برمیگردونه؛ نه اینکه منطق ثبتنام درست کار میکنه یا نه (که قبلاً با unit test پوشش دادیمش). این جداسازی باعث میشه هم تستها سریع اجرا بشن، هم وقتی چیزی fail میشه دقیقاً بدونی مشکل کجاست.
برای هر feature تست بنویسیم یا برای کل response؟
این سؤالی است که خیلی وقتها موقع تستنویسی API پیش میاد، و جواب درست «هر دو، ولی تو دو سطح متفاوت» است، نه انتخاب یکی بهجای اون یکی.
بخش اصلی تستها باید سطح feature/قانون کسبوکار باشن؛ یعنی همون چیزی که تو مثال UserRegistrationService دیدیم: یه تست برای ایمیل تکراری، یه تست برای پسورد ضعیف، یه تست برای ثبتنام موفق. این تستها سریع هستن، نیازی به بالا اومدن سرور واقعی ندارن، و وقتی fail میشن دقیقاً مشخصه کدوم قانون شکسته. اگه فقط به تست کردن کل response اکتفا کنی، وقتی یه تست fail بشه فقط میفهمی «یه جای ثبتنام خراب شده»، نه اینکه دقیقاً کدوم قانون.
در کنارش، به تعداد کمتر، به تستهایی هم نیاز داری که واقعاً endpoint رو صدا بزنن و کل response رو چک کنن؛ status code، شکل JSON خروجی، هدرها. این تستها چیزهایی رو میگیرن که unit test نمیتونه: مسیر اشتباه تو routing، خطای serialize شدن، middleware ای که درست کار نمیکنه، یا auth ای که درست وصل نشده. این تستها تضمین میکنن که تکهها واقعاً با هم درست کار میکنن، نه فقط هر کدوم جدا.
یه ترکیب عملی که خوب جواب میده اینه: به ازای هر قانون کسبوکار یا حالت مرزی، یه unit test (که تعدادشون زیاده و ارزون تمومن)، و به ازای هر route فقط چند تا تست سطح endpoint (یه مسیر موفق، و یکی دو تا از رایجترین حالتهای خطا) که فقط تأیید میکنن سیمکشی بین لایهها درسته. این دقیقاً همون ایدهی test pyramid است که چند پاراگراف پایینتر بهش میرسیم؛ یه عالمه تست باریک و سریع تو پایین، و یه لایهی نازکتر از تستهای گستردهتر روش.
علاوه بر این، برای API هایی که قرارداد مشخصی با کلاینت دارن (مثلاً یه فرانتاند جدا یا یه سرویس دیگه که ازش استفاده میکنه)، میتونی از contract test هم استفاده کنی؛ تستی که تضمین میکنه response API همیشه با یه schema مشخص (مثلاً OpenAPI) مطابقت داره، حتی اگه پیادهسازی داخلیش عوض بشه.
TDD موقع ساختن سرویسها و میکروسرویسها
تو معماری میکروسرویس، چالش اصلی TDD اینه که هر سرویس معمولاً به سرویسهای دیگه یا زیرساختهای خارجی (message queue، دیتابیس، یه API خارجی) وابستهست. اگه بخوای برای هر تست، این وابستگیهای واقعی رو بالا بیاری، تستهات هم کند میشن و هم شکننده (چون یه قطعی موقت تو شبکه میتونه تست رو fail کنه، نه لزوماً باگ تو کدت).
راهحل رایج، جدا کردن تستها به چند سطح:
Unit test با fake/mock: درست مثل مثال بالا، منطق داخلی سرویس رو با نسخههای fake از وابستگیهای بیرونی تست میکنیم. این تستها سریعترین و پرتکرارترین بخش هستن.
Integration test محدود: یه لایهی بالاتر، جایی که سرویس واقعاً به یه نسخهی تست از دیتابیس یا message broker وصل میشه (مثلاً با Docker) تا مطمئن بشیم کدی که با اون زیرساخت حرف میزنه (مثل query های SQL یا serialize کردن پیام) درست کار میکنه. این تستها کندترن، پس تعدادشون معمولاً خیلی کمتر از unit test هاست.
Contract test بین سرویسها: وقتی سرویس A از سرویس B استفاده میکنه، بهجای اینکه تو تستهای سرویس A واقعاً سرویس B رو صدا بزنی، یه قرارداد مشخص (مثلاً با Pact) تعریف میکنی که هر دو طرف بهش پایبند میمونن. این باعث میشه بتونی سرویسها رو مستقل از هم توسعه و deploy کنی، بدون اینکه برای هر تغییر کوچیک مجبور باشی کل زنجیره رو بالا بیاری.
این هرم (تعداد زیاد unit test، تعداد متوسط integration test، و تعداد کم contract/end-to-end test) دقیقاً همون چیزیه که بهعنوان test pyramid شناخته میشه، و TDD معمولاً بیشترین کاربردش رو تو پایینترین لایهی این هرم، یعنی unit test ها داره.
کجا TDD بهسختی جواب میده
TDD همیشه ابزار مناسبی نیست. برای کدی که ماهیتش اکتشافیه (مثلاً وقتی داری با یه API خارجی آشنا میشی یا یه پروتوتایپ سریع میسازی تا ببینی یه ایده اصلاً شدنیه یا نه)، نوشتن تست قبل از اینکه بدونی حتی چه شکلی قراره باشه راهحل نهایی، میتونه بیشتر وقتگیر باشه تا مفید. همینطور برای UI هایی که رفتارشون بیشتر بصریه تا منطقی، نوشتن تستهای یونیت خیلی وقتها کمک چندانی نمیکنه و باید سراغ ابزارهای دیگهای مثل تستهای end-to-end یا visual regression رفت.
یه اشتباه رایج دیگه اینه که تیمها سعی میکنن TDD رو صددرصد و برای هر خط کد اجرا کنن، در حالی که هدف اصلی این نیست که هر تابعی حتماً از قبل تست داشته باشه، بلکه اینه که فرآیند فکر کردن قبل از نوشتن کد، طراحی بهتری بسازه. جایی که این فرآیند فایده نداره (مثل کد اکتشافی یا اسکریپت یکبار مصرف)، اصراف روی TDD فقط باعث کندی بدون سودِ متناسب میشه.