ویرگول
ورودثبت نام
محمد سعید صدیقی
محمد سعید صدیقی
محمد سعید صدیقی
محمد سعید صدیقی
خواندن ۴ دقیقه·۲ روز پیش

قابل‌اعتماد کردن Monkey Patching در تست‌های Go

غیرفعال کردن بهینه‌سازی‌های کامپایلر برای کار کردن درست gomonkey

مقدمه

تست نوشتن یکی از مهم‌ترین بخش‌های توسعه نرم‌افزار است. تست‌ها کمک می‌کنند باگ‌ها را زودتر پیدا کنیم، رفتارهای غیرمنتظره را کنترل کنیم و با خیال راحت‌تر تغییرات جدید اضافه کنیم.

در Go به صورت پیش‌فرض پکیج testing وجود دارد و علاوه بر آن ابزارهای مختلفی مثل test suiteها، mocking و patching داریم که نوشتن تست را راحت‌تر می‌کنند. این ابزارها از نظر ایده شبیه چیزی هستند که در زبان‌های دیگر هم می‌بینیم؛ مثلا در Java ابزارهایی مثل JUnit و Mockito داریم و در Python هم PyTest و unittest.mock.

مدتی پیش وقتی روی یک پروژه کار می‌کردم و در حال یادگیری عمیق‌تر تست‌نویسی در Go و ابزارهایی مثل monkey patching بودم، با مشکلی برخورد کردم که باعث می‌شد تست‌ها بعضی وقت‌ها کار کنند و بعضی وقت‌ها نه. بعد از کمی بررسی فهمیدم که بهینه‌سازی‌های کامپایلر دلیل اصلی این رفتار هستند. در این مقاله می‌خواهم مشکل و راه‌حل آن را به زبان ساده توضیح بدهم تا شما وقت‌تان را برای پیدا کردن راه حل همین مشکل هدر ندهید.

Patching و Monkey Patching چیست؟

گاهی در تست‌ها لازم است یک تابع یا متد را موقتا با یک نسخه‌ی ساختگی (mock) جایگزین کنیم. این کار کمک می‌کند:

  • خطاها را شبیه‌سازی کنیم

  • مسیرهای خاصی از کد اجرا شوند

  • وابستگی‌هایی مثل دیتابیس یا شبکه را حذف کنیم

  • coverage را بالا ببریم

به این تکنیک monkey patching گفته می‌شود.

به زبان ساده:

monkey patching یعنی جایگزین کردن موقت یک تابع یا متد با نسخه‌ی mock فقط در زمان تست.

مثلا:

func GetTime() int64 { return time.Now().Unix() }

در تست می‌توانیم آن را این‌طور عوض کنیم:

patches := gomonkey.ApplyFunc(GetTime, func() int64 { return 1234567890 }) defer patches.Reset()

از این لحظه داخل تست، هر بار GetTime() صدا زده شود، مقدار ثابت برمی‌گرداند.

نکته مهم: فقط برای تست

Monkey patching اصلا مناسب production نیست.

این روش:

  • thread-safe نیست

  • رفتار قابل پیش‌بینی ندارد

  • وابسته به جزئیات داخلی runtime است

  • فقط برای تست طراحی شده

پس هرگز در کد اصلی برنامه از آن استفاده نکنید.

کتابخانه‌های Monkey Patching در Go

دو کتابخانه‌ی معروف وجود دارد:

  • bou.ke/monkey

  • agiledragon/gomonkey

هر دو با دستکاری pointer توابع در runtime کار می‌کنند.

در عمل، gomonkey گزینه‌ی بهتری است چون:

  • با نسخه‌های جدید Go سازگارتر است

  • پایدارتر است

  • API ساده‌تری دارد

نصب:

go get github.com/agiledragon/gomonkey/v2

استفاده‌ی ساده از gomonkey

Patch کردن یک تابع

patches := gomonkey.ApplyFunc(Add, func(a, b int) int { return 42 }) defer patches.Reset()

Patch کردن یک متد

patches := gomonkey.ApplyMethod( &UserService{}, "FetchUser", func(_ *UserService, id int) (*User, error) { return nil, errors.New("mock error") }, ) defer patches.Reset()

حتما Reset کنید

خیلی مهم است:

defer patches.Reset()

اگر reset نکنید، patch ممکن است روی تست‌های بعدی هم اثر بگذارد و خطاهای عجیب ایجاد کند.

مشکل اصلی: چرا گاهی Patch کار نمی‌کند؟

ممکن است با این حالت‌ها روبه‌رو شوید:

  • patch اعمال نمی‌شود

  • تابع اصلی اجرا می‌شود

  • تست‌ها بعضی وقت‌ها fail می‌شوند

  • روی یک سیستم کار می‌کند ولی روی سیستم دیگر نه

دلیل اصلی: بهینه‌سازی‌های کامپایلر Go

کامپایلر Go از روش های زیادی برای optimize می‌کند. مثلا:

  • inlining

  • حذف کدهای اضافی

  • reorder کردن دستورات

  • حذف سمبل‌ها

اگر یک تابع inline شود، دیگر به عنوان یک تابع مستقل وجود ندارد. یعنی چیزی برای patch کردن باقی نمی‌ماند.

مثلا:

func Add(a, b int) int { return a + b }

کامپایلر ممکن است این را مستقیم داخل caller کپی کند. در این حالت patch کردن Add هیچ اثری ندارد.

چرا در حالت Debug کار می‌کند؟

ممکن است دیده باشید:

وقتی با breakpoint یا debugger اجرا می‌کنید، patch کار می‌کند.

دلیلش این است که در حالت debug معمولا بهینه‌سازی‌ها غیرفعال می‌شوند. بنابراین تابع inline نمی‌شود و patch درست کار می‌کند.

ولی در اجرای عادی تست‌ها، optimize فعال است و patch خراب می‌شود.

راه‌حل: غیرفعال کردن بهینه‌سازی در زمان تست

راه‌حل ساده است:
موقع اجرای تست‌ها optimization و inlining را غیرفعال کنید.

با gcflags:

go test -gcflags="all=-N -l"

معنی فلگ‌ها

  • -N → غیرفعال کردن optimization

  • -l → غیرفعال کردن inlining

  • all= → برای همه‌ی پکیج‌ها اعمال شود (خیلی مهم)

اگر all= را نگذارید، بعضی پکیج‌ها هنوز optimize می‌شوند و patch ممکن است باز هم fail شود.

نکته کاربردی

می‌توانید alias بسازید:

alias gotest='go test -gcflags="all=-N -l" -count=1'

و بعد:

gotest ./...

-count=1 جلوی cache شدن تست‌ها را می‌گیرد.

چه زمانی از Monkey Patching استفاده کنیم؟

فقط وقتی:

  • dependency و legacy دارید

  • refactor کردن سخت است

  • interface کافی نیست

  • نیاز به کنترل دقیق اجرای کد دارید

در حالت عادی، بهتر است از interface و dependency injection استفاده کنید. monkey patching باید آخرین گزینه باشد.

نتیجه‌گیری

Monkey patching می‌تواند ابزار قدرتمندی برای تست در Go باشد. کتابخانه‌هایی مثل gomonkey این کار را ساده می‌کنند و کمک می‌کنند مسیرهای خاص را تست کنیم و coverage را بالا ببریم.

اما بهینه‌سازی‌های کامپایلر، مخصوصا inlining، می‌توانند باعث شوند patch اصلا اعمال نشود و تست‌ها رفتار عجیب داشته باشند.

راه‌حل خیلی ساده است:

go test -gcflags="all=-N -l"

بهینه‌سازی را در زمان تست غیرفعال کنید تا patch قابل‌اعتماد شود.

اگر از monkey patching استفاده می‌کنید، همیشه این سه نکته را یادتان باشد:
فقط برای تست، حتما reset کنید، و optimization را غیرفعال کنید.

golanggotesting
۲
۱
محمد سعید صدیقی
محمد سعید صدیقی
شاید از این پست‌ها خوشتان بیاید