7.1 مقدمه‌ای بر تست در Go

7.1 مقدمه‌ای بر تست در Go

تست‌نویسی یکی از ارکان حیاتی توسعه نرم‌افزارهای قابل‌اعتماد و نگهدارپذیر است. در دنیای امروز که سرعت توسعه و تغییرات کد روزبه‌روز بیشتر می‌شود، وجود تست‌های دقیق و ساختاریافته تضمین می‌کند که عملکرد نرم‌افزار با تغییرات جدید دچار اختلال نشود. از کشف باگ‌ها گرفته تا مستندسازی رفتار مورد انتظار ماژول‌ها، تست‌ها نقشی فراتر از صرفاً اطمینان‌بخشی دارند. آن‌ها به تیم توسعه جرئت ریفکتور می‌دهند و مرزهای طراحی سیستم را شفاف می‌کنند.

زبان Go با در نظر گرفتن سادگی و مینیمالیسم به عنوان اصل بنیادین، تست‌نویسی را به عنوان بخشی جدانشدنی از چرخه توسعه درون خود جای داده است. بر خلاف بسیاری از زبان‌ها که برای تست‌نویسی نیاز به نصب ابزارهای اضافی دارند، Go یک ابزار تست‌نویسی داخلی، ساده و قدرتمند به نام testing در کنار دستور go test ارائه می‌دهد که فرآیند اجرا و گزارش‌گیری را بسیار روان و سریع می‌کند. این ابزارها با طراحی سطح پایین و بدون پیچیدگی، توسعه‌دهندگان را تشویق می‌کنند که تست را به بخشی از سبک برنامه‌نویسی خود تبدیل کنند.

در Go، تست‌ها همان توابع عادی هستند که با قواعد ساده‌ای مانند TestXxx(t *testing.T) نوشته می‌شوند. خبری از assertionهای پیچیده یا DSLهای سنگین نیست. همین سادگی، آزمون‌ها را خواناتر، نگهدار‌پذیرتر و از همه مهم‌تر، قابل‌درک برای همه اعضای تیم می‌کند. ابزارهای جانبی مانند testify یا ginkgo نیز به عنوان افزونه‌هایی قدرتمند برای پروژه‌های بزرگ‌تر و تست‌های ساختاریافته‌تر به کار گرفته می‌شوند، اما اصل فلسفه Go حفظ مینیمالیسم است.

در این فصل، با اصول، ابزارها و الگوهای تست‌نویسی در زبان Go آشنا خواهیم شد؛ از تست واحد، جدول‌محور، یکپارچه و انتها به انتها گرفته تا بنچمارک، تست فازی و تست‌های هم‌زمانی. همچنین به ابزارهایی چون testify, gomock, ginkgo, پوشش تست، ساخت Suite تستی و نحوه تست‌نویسی حرفه‌ای در پروژه‌های واقعی می‌پردازیم. هدف این فصل آن است که مهارت تست‌نویسی شما را در Go از سطح ابتدایی به سطحی قابل‌اعتماد و حرفه‌ای ارتقا دهد.

به نقل از ویکی پدیا آزمون نرم‌افزار یا تست نرم‌افزار (به انگلیسی: software testing) به فرایند ارزیابی نرم‌افزار به منظور اطمینان از عملکرد صحیح آن در رویدادهایی مختلفی که ممکن است در دوره استفاده از نرم‌افزار با آن مواجه شود می‌باشد و به عبارت دیگر پیدا کردن خطاهایی احتمالی یک نرم‌افزار برای عملکرد درست، صحیح و بهینه آن در طول استفاده از آن است. هر چقدر نرم‌افزار بتواند با رویدادها مختلف به صورت مطلوب تر و قابل پذیرش تری چه از نظر عملکرد و چه از راحتی کاربر داشته باشد می‌توان انتظار داشت نرم‌افزار دارای عملکرد بهتری می‌باشد.

7.1.1 جایگاه تست در چرخه توسعه نرم‌افزار #

در توسعه نرم‌افزار، نوشتن تست فقط یک فعالیت جانبی برای اطمینان از صحت عملکرد نیست، بلکه بخش جدایی‌ناپذیری از طراحی، مستندسازی و حفظ کیفیت سیستم محسوب می‌شود. زمانی که نرم‌افزار رشد می‌کند، تغییر می‌کند و افراد جدید به تیم توسعه می‌پیوندند، تست‌ها نقش ستون‌های قابل اعتماد برای حفظ رفتار صحیح سیستم را ایفا می‌کنند. در این میان، زبان برنامه‌نویسی Go با ساده‌سازی ابزارها و یکپارچه‌سازی امکانات تست‌نویسی، فرآیند توسعه را به‌گونه‌ای طراحی کرده که تست بخشی طبیعی از چرخه توسعه باشد.

7.1.1.1 تست به عنوان بخشی از طراحی سیستم #

در بسیاری از روش‌های توسعه نرم‌افزار، نوشتن تست حتی پیش از پیاده‌سازی کد توصیه می‌شود. در روش‌هایی مانند توسعه مبتنی بر تست یا توسعه مبتنی بر رفتار ، تست‌ها بخش اولیه فرآیند طراحی هستند. در زبان Go، نوشتن تست با استفاده از ساختارهای ساده‌ای مانند TestXxx(t *testing.T) یا استفاده از الگوی تست جدول‌محور باعث می‌شود که طراحی APIها و منطق برنامه به‌صورت طبیعی بر پایه رفتار قابل تست شکل بگیرد. این نوع طراحی نه‌تنها کیفیت را افزایش می‌دهد، بلکه کد را تغییرپذیرتر و خواناتر می‌کند.

7.1.1.2 تست به عنوان مستند رفتاری سیستم #

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

تستی که می‌نویسی، قراردادی زنده با عملکرد سیستم است.

7.1.1.3 تست به عنوان سپر تغییرات #

در پروژه‌هایی که توسعه مداوم دارند، تغییر کد امری اجتناب‌ناپذیر است. اما تغییر بدون پشتوانه تست، خطر ایجاد بازگشت خطا را به همراه دارد. تست‌ها در اینجا به‌عنوان یک سپر دفاعی عمل می‌کنند. پیش از اعمال تغییر، می‌توان با اجرای سریع تست‌ها مطمئن شد که تغییرات، سایر بخش‌های سیستم را تحت تأثیر قرار نداده‌اند. سرعت بالای اجرای تست‌ها در Go باعث می‌شود که این فرآیند به‌صورت مستمر در چرخه توسعه انجام شود. با وجود این تست‌ها، می‌توان با اطمینان بالا اقدام به بازسازی ساختار کد یا اضافه‌کردن ویژگی‌های جدید کرد.

نمی‌توانی چیزی را بهینه کنی که راستی‌آزمایی‌اش نکرده‌ای.

7.1.1.4 تست در Go: اولویت یا گزینه؟ #

در زبان Go، تست‌نویسی نه‌تنها یک امکان در دسترس، بلکه بخشی از ابزار رسمی توسعه است. برخلاف بسیاری از زبان‌ها که نیازمند نصب ابزارها یا کتابخانه‌های شخص ثالث برای نوشتن تست هستند، Go با فراهم کردن ابزارهایی مانند testing، go test، -cover، -bench و -fuzz از ابتدا زیرساخت تست را فراهم کرده است. این ابزارها سبک توسعه‌ای را ترویج می‌دهند که در آن تست‌نویسی نه بعد از پیاده‌سازی، بلکه در حین توسعه و حتی پیش از آن انجام می‌شود. به همین دلیل، در بسیاری از پروژه‌های حرفه‌ای با زبان Go، تست بخش عادی و ضروری توسعه محسوب می‌شود، نه صرفاً یک انتخاب اختیاری.

7.1.2 مزایای تست‌نویسی در Go #

یکی از نقاط قوت زبان Go، سادگی و یکپارچگی ابزارهای تست‌نویسی است. در حالی که در بسیاری از زبان‌ها، برای شروع تست‌نویسی باید چارچوب‌های خارجی نصب و پیکربندی شوند، در Go هر پروژه به‌صورت پیش‌فرض می‌تواند شامل فایل‌های تست باشد و با استفاده از ابزار رسمی go test به‌سادگی تست‌ها را اجرا کند. این طراحی باعث شده تست‌نویسی در Go نه یک کار جانبی یا پیچیده، بلکه بخشی طبیعی از توسعه روزمره باشد.

7.1.2.1 ساده‌سازی فرآیند تست‌نویسی #

تست‌ها در Go صرفاً توابعی هستند که با الگوی TestXxx(t *testing.T) تعریف می‌شوند. نیازی به ارث‌بری، annotation یا ساختارهای پیچیده نیست. تنها چیزی که لازم است یک تابع ساده و چند شرط منطقی برای بررسی نتایج است. همین سادگی باعث می‌شود افراد تازه‌کار نیز بتوانند به‌راحتی نوشتن تست را آغاز کنند و به مرور، تست‌های پیچیده‌تر بنویسند.

7.1.2.2 سرعت بالا در اجرا #

تست‌های Go به دلیل ساختار ساده و اجرای مستقل، بسیار سریع اجرا می‌شوند. این موضوع در پروژه‌های بزرگ که شامل صدها یا هزاران تست هستند، اهمیت ویژه‌ای دارد. با استفاده از امکانات داخلی مانند -short، -run و -parallel می‌توان تست‌ها را به‌صورت انتخابی، سریع و هم‌زمان اجرا کرد. این ویژگی، بازخورد سریع برای توسعه‌دهنده فراهم می‌کند و مانع از کند شدن چرخه توسعه می‌شود.

7.1.2.3 پوشش طیف متنوعی از تست‌ها #

Go به‌صورت پیش‌فرض از انواع مختلف تست پشتیبانی می‌کند:

  • تست واحد unit test
  • تست یکپارچه integration test
  • تست انتها به انتها end-to-end test
  • تست عملکرد benchmark
  • تست فازی fuzz testing

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

7.1.2.4 پشتیبانی از ابزارهای جانبی و شخص ثالث #

در کنار ابزارهای داخلی، اکوسیستم Go شامل کتابخانه‌ها و فریم‌ورک‌های محبوبی برای تست‌نویسی پیشرفته است. کتابخانه‌هایی مانند:

  • testify: برای assertions و mock
  • ginkgo: برای تست‌های ساختاریافته به سبک BDD
  • gomock, moq: برای تولید mock خودکار
  • go-cmp: برای مقایسه دقیق ساختارهای پیچیده

این ابزارها امکانات اضافی و قدرتمندی به تست‌نویسی اضافه می‌کنند و در عین حال با ساختار زبان سازگار باقی می‌مانند.

7.1.2.5 ادغام آسان با CI/CD #

از آنجا که اجرای تست در Go از طریق یک دستور ساده انجام می‌شود (go test ./...)، ادغام آن در خط تولید خودکار بسیار ساده است. بیشتر پلتفرم‌های CI مثل GitHub Actions، GitLab CI، CircleCI و TravisCI، بدون نیاز به پیکربندی اضافی، می‌توانند تست‌های Go را اجرا و گزارش کنند. این ویژگی باعث می‌شود که فرهنگ تست‌نویسی در تیم به‌راحتی به یک عادت سازنده تبدیل شود.

7.1.2.6 پایداری و نگهداری آسان تست‌ها #

ساختار تست در Go به‌گونه‌ای است که نگهداری آن در طول زمان ساده و کم‌هزینه است. چون تست‌ها از توابع معمولی تشکیل شده‌اند، به‌راحتی می‌توان آن‌ها را بازنویسی، جدا یا ترکیب کرد. همچنین از آنجا که ابزارهای تست بخشی از هسته زبان هستند، احتمال ناسازگاری نسخه‌ای یا شکستن تست‌ها به دلیل تغییر ابزار بسیار پایین است.

حتماً، در ادامه نگارش کامل بخش ۷.۱.۳ رویکرد زبان Go به تست‌نویسی با رعایت ساختار مورد نظر، استفاده از کلمات تخصصی به‌صورت tooltip، بدون ارجاع مستقیم به کتاب‌ها و با زبان حرفه‌ای فارسی آورده شده است:

7.1.3 رویکرد زبان Go به تست‌نویسی #

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

7.1.3.1 استفاده از ابزارهای رسمی و داخلی #

در Go، نیازی به نصب هیچ ابزار اضافی برای تست‌نویسی وجود ندارد. با استفاده از پکیج تستینگ و ابزار گو تست می‌توان تست‌ها را نوشت، اجرا کرد و گزارش گرفت. این ابزارها بخشی از هسته زبان هستند و در کنار دستورات اصلی مانند go build یا go run مورد استفاده قرار می‌گیرند. این یکپارچگی باعث می‌شود تست‌نویسی بخشی کاملاً طبیعی از فرآیند توسعه باشد، نه عملی اختیاری یا پیچیده.

7.1.3.2 سادگی در تعریف تست‌ها #

برخلاف زبان‌هایی که برای نوشتن تست نیاز به استفاده از annotation، ارث‌بری از کلاس‌های خاص یا تعریف ساختارهای پیچیده دارند، در Go یک تست تنها یک تابع معمولی با امضای func TestXxx(t *testing.T) است. این توابع می‌توانند هر منطق دلخواهی را پیاده‌سازی کنند و در صورت مشاهده خطا با فراخوانی t.Errorf() یا t.Fatal() گزارش شکست را صادر نمایند. این رویکرد باعث می‌شود تست‌ها خوانا، مینیمال و قابل فهم برای تمام اعضای تیم باشند.

7.1.3.3 سبک مینیمال و بدون چارچوب #

Go از عمد از اضافه کردن چارچوب‌های تست پیچیده و سیستم‌های assertion سنگین اجتناب کرده است. این تصمیم با هدف حفظ سادگی، شفافیت و کنترل بیشتر توسعه‌دهنده اتخاذ شده است. اگرچه کتابخانه‌های شخص ثالث مانند testify یا ginkgo برای پروژه‌های بزرگ یا تست‌های ساختاریافته‌تر وجود دارند، اما فلسفه Go این است که تست ساده باشد و تا جای ممکن از ابزارهای استاندارد استفاده شود.

7.1.3.4 پشتیبانی طبیعی از انواع تست #

رویکرد Go به تست‌نویسی صرفاً محدود به بررسی صحت نیست. ابزار رسمی go test با فلگ‌هایی مانند -bench, -cover و -fuzz از تست‌های متنوعی پشتیبانی می‌کند، از جمله:

  • تست عملکرد benchmark
  • سنجش پوشش کد code coverage
  • تست فازی fuzz testing

همه این قابلیت‌ها در ابزارهای داخلی Go گنجانده شده‌اند و برای استفاده از آن‌ها نیازی به نصب افزونه یا وابستگی جدید نیست. این ویژگی Go را از بسیاری از زبان‌ها متمایز می‌کند.

7.1.3.5 طراحی برای توسعه‌دهنده، نه ابزار #

فلسفه طراحی Go این است که توسعه‌دهنده باید به‌جای جنگیدن با ابزارها، مستقیماً روی حل مسئله تمرکز کند. این اصل در تست‌نویسی نیز دیده می‌شود. Go توسعه‌دهنده را تشویق می‌کند به‌جای وابستگی به جادوی فریم‌ورک‌ها، با ابزارهای ساده و قابل درک، منطق تست را به‌صورت شفاف پیاده‌سازی کند. این رویکرد در بلندمدت باعث تسهیل نگهداری پروژه، کاهش پیچیدگی و افزایش اطمینان از صحت سیستم می‌شود.

7.1.4 اصول و قراردادهای تست در Go #

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

7.1.4.1 ساختار فایل‌های تست #

تمام تست‌ها در Go باید در فایل‌هایی با پسوند _test.go قرار بگیرند. این قانون توسط ابزار go test شناسایی می‌شود و فقط این فایل‌ها برای اجرای تست در نظر گرفته می‌شوند. این فایل‌ها معمولاً در کنار فایل‌های اصلی قرار می‌گیرند، مثلاً:

math.go
math_test.go

این ساختار باعث می‌شود تست‌ها به راحتی با کد اصلی مقایسه و توسعه داده شوند، و وابستگی آن‌ها کاملاً واضح باشد.

7.1.4.2 امضای تابع تست #

هر تابع تست باید به شکل زیر تعریف شود:

func TestXxx(t *testing.T)

در اینجا Xxx می‌تواند نامی اختیاری و دلخواه باشد، اما باید با حرف بزرگ آغاز شود تا توسط ابزار تست شناسایی شود. آرگومان t از نوع تی تستینگ است و امکاناتی مانند ثبت خطا، توقف تست، یا گزارش وضعیت را فراهم می‌کند.

برای مثال:

func TestAdd(t *testing.T) {
  got := Add(2, 3)
  if got != 5 {
    t.Errorf("expected 5, got %d", got)
  }
}

7.1.4.3 گروه‌بندی تست‌ها با Subtest #

در Go می‌توان تست‌های مرتبط را با استفاده از متد t.Run به‌صورت زیر تست اجرا کرد. این کار باعث دسته‌بندی منطقی تست‌ها و جداسازی گزارش آن‌ها در خروجی می‌شود:

func TestMathOperations(t *testing.T) {
  t.Run("Addition", func(t *testing.T) {
    // ...
  })

  t.Run("Multiplication", func(t *testing.T) {
    // ...
  })
}

7.1.4.4 موازی‌سازی تست‌ها #

Go به‌صورت بومی از اجرای هم‌زمان تست‌ها پشتیبانی می‌کند. با استفاده از t.Parallel() می‌توان تست‌ها را به‌صورت مستقل و موازی اجرا کرد، به‌ویژه مفید برای تست‌هایی که داده‌ها یا منابع مشترک ندارند:

func TestFastOperation(t *testing.T) {
  t.Parallel()
  // اجرای تست
}

در صورتی که تست‌هایی با منابع مشترک دارید، باید از سینک مانند Mutex یا channel برای کنترل استفاده کنید.

7.1.4.5 قرارداد نام‌گذاری تست‌ها #

  • تمام تست‌ها باید با Test شروع شوند.
  • تست‌های بنچمارک باید با Benchmark شروع شوند و از *testing.B استفاده کنند.
  • تست‌های فازی باید با Fuzz شروع شوند و از *testing.F استفاده کنند.
  • استفاده از اسم‌های معنادار برای تست‌ها باعث خوانایی بهتر کد و گزارش‌ها می‌شود.

مثال‌های معتبر:

func BenchmarkSort(b *testing.B) { ... }
func FuzzParseDate(f *testing.F) { ... }

7.1.4.6 تست‌های بدون تست واقعی #

گاهی ممکن است نیاز باشد یک تست خالی نوشته شود فقط برای اطمینان از اینکه برنامه کامپایل می‌شود یا فقط هدف خاصی را بررسی کند. در این موارد، می‌توان از t.Skip() استفاده کرد:

func TestStub(t *testing.T) {
  t.Skip("implementation pending")
}

7.1.5 مقایسه با زبان‌های دیگر #

زبان Go رویکردی ساده، سریع و بی‌واسطه به تست‌نویسی دارد که آن را از بسیاری از زبان‌های رایج توسعه نرم‌افزار متمایز می‌کند. در حالی که برخی زبان‌ها با استفاده از چارچوب‌های سنگین یا ابزارهای جانبی به سراغ تست می‌روند، Go تست را به‌عنوان بخشی از طراحی زبان در نظر گرفته است. در این بخش، نگاهی تطبیقی به روش تست‌نویسی در Go و چند زبان دیگر می‌اندازیم.

7.1.5.1 مقایسه با Python #

در زبان Python، تست‌نویسی اغلب با استفاده از کتابخانه‌هایی مانند unittest, pytest, یا nose انجام می‌شود. اگرچه این ابزارها قدرتمند هستند و ویژگی‌هایی مانند تشخیص خودکار، fixture injection و assert پیشرفته دارند، اما معمولاً نیاز به نصب و پیکربندی اولیه دارند. در مقابل، Go از ابزار داخلی testing استفاده می‌کند که بدون وابستگی و با دستور go test قابل استفاده است.

ویژگیGoPython + pytest
ابزار داخلیبلهخیر
assert پیشرفتهخیر (با if)بله
سرعت اجرابسیار بالامعمولی
منحنی یادگیریبسیار سادهنسبتاً متوسط

7.1.5.2 مقایسه با Java #

در Java، تست‌نویسی معمولاً با استفاده از چارچوب‌هایی مانند JUnit یا TestNG انجام می‌شود. این چارچوب‌ها مبتنی بر annotation و reflection هستند، که در Go وجود ندارد. تعریف یک تست ساده در Java نیازمند کلاس، annotation و ساختار نسبتاً سنگینی است، در حالی که در Go یک تابع ساده کافی است. از طرفی ابزارهای Java برای پروژه‌های پیچیده‌تر امکانات بیشتری دارند، اما این مزیت همراه با پیچیدگی نیز هست.

ویژگیGoJava + JUnit
نیاز به annotationخیربله
نوشتار سادهبلهخیر
یکپارچگی با زبانکاملاز طریق چارچوب جداگانه
زمان اجراسریعکندتر

7.1.5.3 مقایسه با JavaScript / TypeScript #

در JavaScript یا TypeScript، ابزارهای متنوعی برای تست وجود دارد مانند Jest, Mocha, Chai, Vitest. اگرچه این ابزارها تجربه تست بسیار مدرنی فراهم می‌کنند (مانند snapshot testing و mocking خودکار)، اما پیکربندی آن‌ها در پروژه‌های بزرگ می‌تواند زمان‌بر باشد. از طرف دیگر، وجود اکوسیستم‌های پیچیده باعث می‌شود گاهی فهمیدن منطق تست دشوار شود. Go از این پیچیدگی اجتناب کرده و تجربه‌ای ساده، قابل پیش‌بینی و سریع ارائه می‌دهد.

ویژگیGoJavaScript + Jest
نصب ابزارلازم نیستضروری
snapshot testingنداردبله
mocking داخلینداردبله
خطایابی آسان تست‌هابلهگاهی دشوار

7.1.5.4 مقایسه با Rust #

در Rust، تست‌ها معمولاً درون ماژول‌هایی با annotation #[cfg(test)] و ماکروهای #[test] نوشته می‌شوند. مانند Go، تست بخشی از زبان است و اجرای آن با دستور cargo test انجام می‌شود. از این نظر، رویکرد Rust بسیار به Go نزدیک است. با این حال، تعریف ماژول‌ها و ویژگی‌های زبانی Rust ممکن است منحنی یادگیری بالاتری داشته باشد، در حالی که Go از سادگی بیشتری برخوردار است.

ویژگیGoRust
ابزار تست داخلیبلهبله
تست داخل ماژولاختیاریبله
macro و annotationندارددارد
پیچیدگی یادگیریپایینمتوسط به بالا

7.1.5.5 نتیجه‌گیری مقایسه #

رویکرد Go به تست‌نویسی کاملاً عمل‌گرا، ساده و بدون وابستگی به ابزارهای جانبی است. این سادگی باعث می‌شود تیم‌های توسعه بتوانند سریع‌تر شروع به نوشتن تست کنند و فرآیند اجرای تست‌ها نیز سبک، سریع و قابل‌اتکا باقی بماند. در حالی که بسیاری از زبان‌ها بر غنای ابزارهای تست تکیه می‌کنند، Go با ارائه یک راهکار ساده اما کارآمد، توسعه‌دهنده را به نوشتن تست تشویق می‌کند بدون اینکه او را از مسیر تولید منحرف کند.

7.1.6 آزمونگر ایده‌آل گولنگ‌نویس: چرا باید تست بنویسیم؟ #

در اکوسیستم Go، تست‌نویسی نه یک مهارت اضافه بلکه بخشی از هویت یک توسعه‌دهنده حرفه‌ای محسوب می‌شود. اگرچه ابزارها و کتابخانه‌ها نوشتن تست را بسیار آسان کرده‌اند، اما نوشتن تست صرفاً به خاطر وجود ابزار انجام نمی‌شود. در واقع، نوشتن تست در Go به دلایل مهم‌تری ضرورت دارد.

7.1.6.1 تضمین رفتار سیستم #

هر خط تست، تضمینی است برای آنکه یک ویژگی مشخص از سیستم در شرایط خاص به درستی عمل می‌کند. وقتی تستی می‌نویسیم، در واقع داریم تعریف می‌کنیم که چه چیزی قابل قبول است و چه چیزی نیست. بدون تست، تغییر دادن یا توسعه کد به یک فعالیت پرریسک تبدیل می‌شود.

7.1.6.2 مستندسازی زنده #

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

7.1.6.3 کاهش هزینه‌های نگهداری #

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

7.1.6.4 ایجاد فرهنگ توسعه حرفه‌ای #

نوشتن تست فقط مهارت نیست، بلکه نشانه‌ی یک طرز فکر است. توسعه‌دهنده‌ای که تست می‌نویسد، به کیفیت، آینده‌پذیری و قابل‌اعتماد بودن سیستم اهمیت می‌دهد. در دنیای Go، این رویکرد از یک توصیه فراتر رفته و به یک عرف تبدیل شده است.

7.1.7 ابزار go test و نحوه اجرای تست‌ها از خط فرمان #

در زبان Go، اجرای تست‌ها از طریق ابزار رسمی go test انجام می‌شود. این ابزار بخشی از زنجیره ابزارهای CLI زبان است و بدون نیاز به نصب هیچ ابزار اضافه‌ای، امکان نوشتن، اجرا و گزارش‌گیری از تست‌ها را فراهم می‌کند. در این بخش به بررسی کامل نحوه استفاده از go test، همراه با فلگ‌ها، گزینه‌ها و حالت‌های مختلف اجرای آن می‌پردازیم.

7.1.7.1 اجرای ساده تست‌ها #

برای اجرای تست‌های موجود در یک بسته (پوشه فعلی)، تنها کافی‌ست در همان مسیر دستور زیر را وارد کنید:

go test

این دستور تمام فایل‌های _test.go را کامپایل کرده، به صورت خودکار go vet را اجرا می‌کند و سپس تست‌ها را اجرا می‌کند.

خروجی به‌صورت خلاصه نمایش داده می‌شود:

ok  	mymodule/mypkg	0.011s

اگر تست‌ها شکست بخورند، خروجی با FAIL نمایش داده می‌شود.


7.1.7.2 اجرای انتخابی تست‌ها با -run #

می‌توان یک یا چند تابع تست خاص را با استفاده از الگوی منظم (regex) و فلگ -run اجرا کرد:

go test -run=TestAdd
go test -run='Add|Sub'

نکته: تابع تست باید دقیقاً با الگوی TestXxx تعریف شده باشد.

7.1.7.3 تست‌های موازی و مدیریت زمان #

  • برای تنظیم حداکثر تعداد تست‌هایی که می‌توانند هم‌زمان اجرا شوند:
go test -parallel=4
  • برای محدود کردن زمان اجرای هر تست یا کل تست‌ها:
go test -timeout=5s

در صورت عبور از این زمان، پروسه تست با خطا قطع می‌شود.


7.1.7.4 سنجش پوشش کد با -cover #

برای مشاهده درصد پوشش کد توسط تست‌ها:

go test -cover

برای تولید فایل پوشش:

go test -coverprofile=coverage.out

و برای مشاهده پوشش به صورت HTML:

go tool cover -html=coverage.out

7.1.7.5 اجرای بنچمارک‌ها با -bench #

برای اجرای بنچمارک‌ها از فلگ -bench استفاده می‌شود. برای اجرای همه بنچمارک‌ها:

go test -bench=.

برای اجرای بنچمارک خاص:

go test -bench=BenchmarkSort

با فلگ -benchmem می‌توان اطلاعات حافظه مصرفی را نیز دریافت کرد:

go test -bench=. -benchmem

7.1.7.6 تست‌های فازی با -fuzz #

برای اجرای تست فازی:

go test -fuzz=FuzzParse

برای محدود کردن زمان اجرای فاز فازی:

go test -fuzz=FuzzParse -fuzztime=20s

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


7.1.7.7 ترکیب‌های رایج در پروژه‌های واقعی #

در بسیاری از پروژه‌ها از ترکیب چند فلگ استفاده می‌شود. مثال‌هایی از ترکیب‌های رایج:

go test -v ./...                             # اجرای کامل تست‌ها به‌صورت verbose
go test -run=TestHandler -cover ./handlers   # تست handler خاص با پوشش کد
go test -bench=BenchmarkEncode -benchmem     # اجرای بنچمارک به همراه مصرف حافظه
go test -fuzz=FuzzMyFunc -fuzztime=1m        # اجرای تست فازی به مدت یک دقیقه
go test -count=1                             # غیرفعال‌سازی cache

7.1.7.8 حالت‌های اجرای go test #

go test در دو حالت مختلف اجرا می‌شود:

  • Local Directory Mode: بدون آرگومان بسته اجرا می‌شود و فقط پوشه فعلی را تست می‌کند:
go test
  • Package List Mode: با مشخص‌کردن مسیر یا پکیج اجرا می‌شود:
go test ./...
go test mymodule/utils

در حالت دوم، سیستم کَش تست فعال می‌شود و تست‌های قبلاً موفق اجرا نشده و نتیجه cache نمایش داده می‌شود (با برچسب (cached)).

7.1.7.9 بررسی race condition با فلگ -race #

در برنامه‌های همزمان، احتمال بروز شرایط مسابقه وجود دارد؛ یعنی دو یا چند goroutine به‌صورت ناهمزمان به یک متغیر مشترک دسترسی پیدا کنند و حداقل یکی از آن‌ها آن را تغییر دهد. این نوع باگ‌ها بسیار خطرناک هستند، زیرا در زمان اجرا قابل پیش‌بینی نیستند و معمولاً فقط در برخی شرایط خاص خود را نشان می‌دهند.

برای شناسایی چنین مشکلاتی در Go، می‌توان از فلگ -race هنگام اجرای تست‌ها استفاده کرد. این فلگ باعث می‌شود کامپایلر، برنامه را به‌گونه‌ای کامپایل کند که دسترسی به حافظه در زمان اجرا بررسی شود.

نحوه استفاده: #

go test -race

یا برای اجرای تست‌های یک پکیج خاص:

go test -race ./pkg/concurrent

در صورت وجود race، خروجی‌ای مشابه زیر نمایش داده می‌شود:

==================
WARNING: DATA RACE
Read at 0x00c0000b2000 by goroutine 6:
  main.main.func1()

Previous write at 0x00c0000b2000 by goroutine 5:
  main.main.func2()
==================

نکات مهم: #

  • استفاده از -race باعث کند شدن اجرای تست‌ها (معمولاً 2 تا 5 برابر) و مصرف بیشتر حافظه می‌شود.
  • توصیه می‌شود در فرآیند CI، حداقل یک‌بار در روز یا قبل از انتشار نسخه جدید با -race تست کامل انجام شود.
  • تشخیص race توسط این ابزار معمولاً دقیق است، اما به صورت صددرصد تضمینی نیست.