Go Programming

تسلط بر تست‌نویسی در Go: هنر و علم تست‌های مبتنی بر جدول

نوشتن تست‌های واحد مؤثر، یکی از حیاتی‌ترین مهارت‌ها برای هر توسعه‌دهنده Go است. اگرچه بسته testing کتابخانه استاندارد قدرتمند و ساده است، اما نوشتن تست‌هایی که هم جامع و هم قابل نگهداری باشند، نیازمند یک استراتژی محکم است. در میان الگوهای مختلف موجود در اکوسیستم Go، تست‌های مبتنی بر جدول (Table-Driven Tests) به عنوان استاندارد رایج برای نوشتن تست‌های واحد تمیز، خوانا و مقیاس‌پذیر برجسته هستند.

فلسفه تست‌نویسی در Go

Go سادگی و قراردادها را بر پیکربندی ترجیح می‌دهد. وقتی نوبت به تست‌نویسی می‌رسد، این زبان همه چیزهایی را که نیاز دارید، به صورت پیش‌فرض فراهم می‌کند. یک تابع تست معمولی از قرارداد نام‌گذاری TestXXX پیروی می‌کند و یک پارامتر *testing.T می‌پذیرد. جریان کاری اصلی شامل آماده‌سازی ورودی‌ها، اجرای تابع مورد آزمایش و بررسی نتیجه مورد انتظار است.

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

ساختاردهی تست‌های مبتنی بر جدول

یک تست مبتنی بر جدول به خوبی ساختاریافته، شامل سه بخش اصلی است: جدول تست (یک برش از ساختارها)، حلقه تکرار و منطق بررسی (assertion). هر ساختار در جدول، نماینده یک مورد تست واحد است که شامل مقادیر ورودی، خروجی‌های مورد انتظار و اغلب یک نام توصیفی برای اهداف اشکال‌زدایی می‌باشد.

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

package mathutils

import (
	"testing"
)

// Divide performs integer division.
func Divide(a, b int) (int, error) {
	if b == 0 {
		return 0, fmt.Errorf("division by zero")
	}
	return a / b, nil
}

// TestDivide tests the Divide function using table-driven testing.
func TestDivide(t *testing.T) {
	// Define the test cases
	tests := []struct {
		name      string
		numerator int
		denominator int
		expected  int
		wantErr   bool
	}{
		{
			name:      "positive numbers",
			numerator: 10,
			denominator: 2,
			expected:  5,
			wantErr:   false,
		},
		{
			name:      "negative result",
			numerator: -10,
			denominator: 2,
			expected:  -5,
			wantErr:   false,
		},
		{
			name:      "division by zero",
			numerator: 10,
			denominator: 0,
			expected:  0,
			wantErr:   true,
		},
	}

	// Iterate over the test cases
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got, err := Divide(tt.numerator, tt.denominator)
			
			// Check for expected error
			if (err != nil) != tt.wantErr {
				t.Errorf("Divide() error = %v, wantErr %v", err, tt.wantErr)
				return
			}
			
			// If no error, check the result
			if !tt.wantErr && got != tt.expected {
				t.Errorf("Divide() got = %v, want %v", got, tt.expected)
			}
		})
	}
}

در این مثال، هر مورد تست در یک لیترال ساختار (struct literal) بسته‌بندی شده است. حلقه از طریق این موارد تکرار می‌کند و t.Run را با نام مورد فراخوانی می‌کند. این کار برای خروجی go test حیاتی است، زیرا به شما امکان می‌دهد موارد شکست‌خورده فردی را در حین اجرای بقیه موارد رد کنید و بازخورد دقیق‌تری ارائه دهید.

بهترین شیوه‌ها برای قابلیت نگهداری

اگرچه تست‌های مبتنی بر جدول قدرتمند هستند، اما اگر به درستی مدیریت نشوند، می‌توانند پیچیده و نامرتب شوند. در اینجا چند نکته برای تمیز نگه داشتن تست‌های شما آورده شده است:

  • ساختارها را ساده نگه دارید: از ساختارهای تو درو پیچیده در موارد تست خودداری کنید، مگر اینکه ضروری باشد. اگر ورودی‌های شما بیش از حد پیچیده می‌شوند، قبل از افزودن آن‌ها به برش، در نظر بگیرید که آن‌ها را در متغیرها استخراج کنید.
  • از نام‌های توصیفی استفاده کنید: فیلد name در ساختار تست شما باید به وضوح سناریو را توصیف کند. این کار اشکال‌زدایی را هنگام شکست یک تست بسیار آسان‌تر می‌کند.
  • با گیرنده‌های اشاره‌گر (Pointer Receivers) با دقت رفتار کنید: اگر تابع شما وضعیت یک گیرنده را تغییر می‌دهد، مطمئن شوید که برای هر مورد تست، نمونه‌های تازه ایجاد می‌کنید تا از رقابت داده‌ها یا نشت وضعیت بین تکرارها جلوگیری شود.
  • آماده‌سازی را از منطق جدا کنید: اگر مورد تست شما نیاز به آماده‌سازی پیچیده دارد (مانند شبیه‌سازی پایگاه داده یا ورودی/خروجی فایل)، در نظر بگیرید که آن آماده‌سازی را به یک تابع کمکی استخراج کنید تا جدول تست خوانا باقی بماند.

نتیجه‌گیری

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

Share: