تابع TestMain توی گولنگ چه کاربردی داره؟

مختصر و مفید دربارۀ TestMain در گولنگ

موقع بررسی فایل‌های تستی که با گولنگ نوشته شدن، ممکنه با تابعی به‌نام TestMain مواجه شده باشید. اگر بخوایم این اسم رو بر اساس کانونشن نامگذاری توابع تست در گولنگ بررسی کنیم، قاعدتاً باید اسم تست تابع Main برنامه باشه. اما آیا واقعاً TestMain برای تست تابع Main نوشته می‌شه؟ اگر نه، کاربردش چیه و چه جاهایی به کار می‌آد؟

تابع TestMain دقیقاً چه کاری انجام میده؟

اول اجازه بدید به سوال قبل جواب بدیم: نه، TestMain تست تابع Main نیست! اصولاً نوشتن تست برای تابع Main چندان توصیه نمی‌شه، چون بهتره که این تابع مختصر و مفید باشه و برای گرفتن متغیرهای محیطی و راه‌اندازی اولیۀ برنامه و فراخوانی سایر پکیج‌ها و اجرای برنامۀ اصلی استفاده بشه.

حالا به سوال دوم می‌رسیم: پس TestMain دقیقاً چه کاری انجام می‌ده؟ این تابع در واقع برای آماده‌سازی‌های موردنیاز پیش از اجرای سایر تست‌ها و پاکسازی بعد از اتمام تمام تست‌ها طراحی شده. در واقع اگر فایل تست شما حاوی تابعی به این شکل باشه:

func TestMain(m *testing.M)

با اجرای دستور go test، به‌جای اینکه تست‌های شما به‌ترتیب اجرا بشن، این تابع اجرا می‌شه. استراکت M که ورودی این تابعه، فقط یک متد داره (Run) که با فراخوانیش، تمام تست‌های موجود در فایل شما اجرا می‌شن.

یک مثال خیلی ساده…

بذارین با یک مثال خیلی ساده، ببینیم در عمل چه اتفاقی می‌افته. کد سادۀ زیر رو در نظر بگیرید:

package myTestPackage

import (
	"log"
	"testing"
)

func TestMain(m *testing.M) {
	log.Println("Before running the tests...")
	m.Run()
	log.Println("After running the tests...")
}

func TestA(t *testing.T) {
	log.Println("TestA is running")
}

func TestB(t *testing.T) {
	log.Println("TestB is running")
}

خروجی این کد، به این صورته:

2024/03/17 20:43:55 Before running the tests...
2024/03/17 20:43:55 TestA is running
2024/03/17 20:43:55 TestB is running
PASS
2024/03/17 20:43:55 After running the tests…

گاهی لازمه که قبل از تست‌ها، یک‌سری عملیات انجام بشه و بعد از پایان همۀ تست‌ها هم، یک‌سری تمیزکاری‌هایی صورت بگیره. به‌عنوان نمونه تصور کنید برای انجام یک‌سری از تست‌ها، نیازه یک اتصال به دیتابیس برقرار کنید، یک‌ دیتابیس تستی بسازید و بهش جدول‌ها و رکوردهایی اضافه کنید. بعد از پایان کار هم لازمه تمام جدول‌ها و دیتابیس رو حذف و اتصال رو قطع کنید. خب، بخش اول کار، به‌نحوی با تابع ()init هم امکان‌پذیره، اما بخش دوم نه. TestMain کار رو راحت و مرتب می‌کنه.

یکی از نکته‌هایی که هنگام کار با TestMain باید بهش توجه داشت، اینه که در هر پکیج، فقط می‌شه یک بار این تابع رو تعریف کرد (چون اسم توابع در هر پکیج باید منحصربه‌فرد باشن)؛ در نتیجه اگر پکیج ما چند فایل تست داشته باشه، باید حواسمون باشه که این تابع رو در جای مناسبی تعریف کنیم. از اون طرف، لازمه بدونیم که برای هر پکیج، باید TestMain جداگانه‌ای تعریف کنیم.

نکته: تابع TestMain فقط یک بار اجرا می‌شه! در نتیجه اگر برای هر تست نیازمند پیش‌نیازها و پس‌نیازهای متفاوتی هستید، لازمه که خودتون در ابتدا/انتهای هر تست بهشون رسیدگی کنید.

توجه: اگر توی تست‌هاتون از فلگ استفاده می‌کنید، حتماً باید توی TestMain متد ()flag.Parse رو فراخوانی کنید.

نکتۀ کنکوری دربارۀ فراخوانی os.Exit در TestMain

متد ()m.Run یک Exit Code به ما برمی‌گردونه که ما می‌تونیم این عدد رو به ()os.Exit پاس بدیم. احتمالاً توی اکثر تست‌هایی که می‌بینید، این کار انجام شده. علتش هم اینه که در گذشته اگر ()os.Exit در انتهای TestMain فراخوانی نمی‌شد، فرض می‌شد که Exit Code صفره و در نتیجه حتی اگر تستی Fail شده بود، گو تصور می‌کرد همۀ تست‌ها پاس شدن.

این طراحی یک ایراد مهم داشت: ()os.Exit برنامه رو درجا متوقف می‌کنه و اهمیتی به کدهای defer شده نمی‌ده و این از پایه با اهداف استفاده از TestMain در تضاده. چون برای خیلی از پاکسازی‌ها (مثلاً بستن فایل‌ها یا اتصال‌ها)، از defer استفاده می‌شه. البته دور زدن این محدودیت هم روش‌هایی داره، اما خوشبختانه دیگه نیازی به دور زدنش نیست. چون از ورژن 1.15 به بعد، الزام به فراخوانی ()os.Exit در انتهای TestMain برداشته شده؛ در نتیجه اگر TestMain تموم بشه و این متد فراخوانی نشده باشه، خود گو به‌صورت خودکار با Exit Code دریافتی از ()m.Run، متد Exit رو فراخوانی می‌کنه. این‌طوری دیگه سر اجرا نشدن کدهای defer شده هم مشکلی پیش نمی‌آد.

در نوشتن این مطلب، از این منابع استفاده شده:


دیدگاه‌ها

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

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *