1. مقدمهای در مورد آزمون واحد
آزمون واحد به بررسی و اعتبارسنجی کوچکترین واحد آزمون پذیر در یک برنامه اشاره دارد، مانند یک تابع یا متد در زبان Go. آزمون واحد اطمینان میدهد که کد به عنوان انتظار کار میکند و به توسعهدهندگان اجازه میدهد که تغییراتی در کد ایجاد کنند بدون اینکه به طور ناخواسته از کارایی موجود آن خراب شود.
در یک پروژه Golang، اهمیت آزمون واحد بدون اشارهی مستقیم خودگونه است. اولاً، میتواند کیفیت کد را بهبود بخشد و به توسعهدهندگان اطمینان بیشتری بدهد تا تغییراتی در کد ایجاد کنند. ثانیاً، آزمون واحد میتواند به عنوان مستند برای کد عمل کند و رفتار انتظاری آن را توضیح دهد. علاوه بر این اجرای آزمونهای واحد به شکل خودکار در محیط یکپارچهسازی پیوسته، میتواند به سرعت بازیابی خطاهای به تازگی معرفی شده را فراهم کند و اینگونه پایداری نرمافزار را بهبود بخشد.
2. انجام آزمونهای پایه با استفاده از بسته testing
کتابخانه استاندارد زبان Go، شامل بسته testing
است که ابزارها و قابلیتهایی را برای نوشتن و اجرای آزمونها فراهم میکند.
2.1 ایجاد یک مورد تست اولیه
برای نوشتن یک تابع آزمون، شما باید یک فایل با پسوند _test.go
ایجاد کنید. به عنوان مثال، اگر فایل کد منبع شما نام calculator.go
داشته باشد، فایل آزمون شما باید نام calculator_test.go
داشته باشد.
حالا وقت آن است که به ایجاد تابع آزمون بپردازید. یک تابع آزمون باید بسته testing
را وارد کرده و الگوی خاصی را دنبال کند. در ادامه یک مثال ساده آمده است:
// calculator_test.go
package calculator
import (
"testing"
"fmt"
)
// تست تابع جمع
func TestAdd(t *testing.T) {
result := Add(1, 2)
expected := 3
if result != expected {
t.Errorf("از %v انتظار داشتم، اما %v را دریافت کردم", expected, result)
}
}
در این مثال، TestAdd
یک تابع آزمون است که یک تابع Add
واقعی را آزمایش میکند. اگر نتیجه تابع Add
با نتیجه انتظاری مطابقت داشته باشد، تست موفق خواهد بود؛ در غیر این صورت، t.Errorf
فراخوانی میشود تا اطلاعات مربوط به شکست تست را ثبت کند.
2.2 درک قوانین نامگذاری و امضای توابع تست
توابع تست باید با Test
شروع شده و دنبال شده توسط هر رشته غیر قلمکوچکی باشند و تنها پارامتر آنها باید یک اشاره به testing.T
باشد. همانطور که در مثال نشان داده شده است، TestAdd
قوانین نامگذاری صحیح و امضای مناسب را دنبال میکند.
2.3 اجرای موردهای تست
میتوانید موردهای تست خود را با استفاده از ابزار خط فرمان اجرا کنید. برای یک مورد تست خاص، دستور زیر را اجرا کنید:
go test -v // اجرای تستها در دایرکتور فعلی و نمایش خروجی تفصیلی
اگر میخواهید یک مورد تست خاص را اجرا کنید، میتوانید از پرچم -run
دنبال شده توسط یک عبارت منظم استفاده کنید:
go test -v -run TestAdd // فقط اجرای تابع تست TestAdd
دستور go test
به طور خودکار تمام فایلهای _test.go
را پیدا میکند و هر تابع تستی را که با معیارها مطابقت داشته باشد، اجرا میکند. اگر همهی تستها با موفقیت اجرا شوند، پیامی مشابه PASS
را در خط فرمان مشاهده میکنید؛ اگر هر تستی شکست خورد، پیامی مشابه FAIL
همراه با پیام خطای مربوطه مشاهده خواهید کرد.
3. نوشتن موردهای تست
3.1 گزارش دادن خطاها با استفاده از t.Errorf
و t.Fatalf
در زبان Go، چارچوب آزمون ابزارهای مختلفی برای گزارش خطاها فراهم میکند. دو تابع متداول استفاده شده، Errorf
و Fatalf
هستند که هر دوی آنها متدی از شی testing.T
هستند. Errorf
برای گزارش خطاها در تست استفاده میشود اما اجازه هم نمیدهد که مورد تست فعلی متوقف شود، در حالی که Fatalf
همانند این کار، تست را فوراً پس از گزارش خطا متوقف میکند. مهم است که بسته به الزامات آزمون، روش مناسب را انتخاب کنید.
مثال استفاده از Errorf
:
func TestAdd(t *testing.T) {
got := Add(1, 2)
want := 3
if got != want {
t.Errorf("Add(1, 2) = %d; میخواستم %d", got, want)
}
}
اگر میخواهید تست فوراً پس از شناسایی خطا متوقف شود، میتوانید از Fatalf
استفاده کنید:
func TestSubtract(t *testing.T) {
got := Subtract(5, 3)
if got != 2 {
t.Fatalf("Subtract(5, 3) = %d; میخواستم 2", got)
}
}
در کل، اگر خطا باعث اجرای صحیح کد بعدی یا شکست تست قابل تأیید در پیشروی است، توصیه میشود که از Fatalf
استفاده کنید. در غیر این صورت، توصیه میشود که از Errorf
برای گرفتن یک نتیجه آزمون جامعتر استفاده کنید.
3.2 سازماندهی زیرآزمونها و اجرای زیرآزمونها
در Go، ما میتوانیم از t.Run
برای سازماندهی زیرآزمونها استفاده کنیم که به ما کمک میکند که کدهای تست را به صورت ساختارمندتری بنویسیم. زیرآزمونها میتوانند دارای Setup
و Teardown
خود باشند و میتوانند به صورت جداگانه اجرا شوند و این امکان را فراهم میکنند. این ویژگی به ویژه برای انجام تستهای پیچیده یا تستهای پارامتریک بسیار مفید است.
مثال استفاده از t.Run
:
func TestMultiply(t *testing.T) {
testcases := []struct {
name string
a, b, expected int
}{
{"2x3", 2, 3, 6},
{"-1x-1", -1, -1, 1},
{"0x4", 0, 4, 0},
}
for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) {
if got := Multiply(tc.a, tc.b); got != tc.expected {
t.Errorf("ضرب(%d, %d) = %d; مقدار مورد انتظار %d", tc.a, tc.b, got, tc.expected)
}
})
}
}
اگر بخواهیم زیرآزمون موسوم به "2x3" را به صورت جداگانه اجرا کنیم، میتوانیم دستور زیر را در خط فرمان اجرا کنیم:
go test -run TestMultiply/2x3
لطفاً توجه داشته باشید که نامهای زیرآزمونها حساس به حروف بزرگ و کوچک هستند.
4. آمادهسازی قبل و بعد از تست
4.1 تنظیم و پاکسازی
وقتی تستها را انجام میدهیم، اغلب نیاز داریم که برای تستها یک وضعیت اولیه را آماده کنیم (مانند اتصال به پایگاه داده، ایجاد فایل و غیره) و به همین ترتیب، نیاز داریم کارهای پاکسازی را بعد از اتمام تستها انجام دهیم. در Go، ما معمولاً Setup
و Teardown
را مستقیماً در توابع تست انجام میدهیم، و تابع t.Cleanup
امکان ثبت توابع بازگردانی را برای ما فراهم میکند.
در ادامه یک مثال ساده را مشاهده میکنید:
func TestDatabase(t *testing.T) {
db, err := SetupDatabase()
if err != nil {
t.Fatalf("آمادهسازی ناموفق بود: %v", err)
}
// ثبت یک تابع بازگردانی برای اطمینان از بسته شدن اتصال به پایگاه داده پس از اتمام تست
t.Cleanup(func() {
if err := db.Close(); err != nil {
t.Errorf("بستن پایگاه داده ناموفق بود: %v", err)
}
})
// انجام تست...
}
در تابع TestDatabase
، ابتدا تابع SetupDatabase
را برای راهاندازی محیط تست صدا میزنیم. سپس از t.Cleanup()
برای ثبت یک تابعی استفاده میکنیم که پس از اتمام تست برای انجام کارهای پاکسازی، در این مثال بستن اتصال به پایگاه داده، فراخوانی میشود. این روش به ما امکان میدهد تا از اینکه منابع به درستی آزاد شوند، با وجود موفقیت یا عدم موفقیت تست مطمئن شویم.
5. بهبود کارایی تست
بهبود کارایی تست میتواند به ما کمک کند که به سرعت توسعه را تکرار کنیم، به سرعت مشکلات را کشف کنیم و از کیفیت کد اطمینان حاصل کنیم. در زیر، ما در مورد پوشش تست، تستهای جدولی مبتنی بر داده و استفاده از موک برای بهبود کارایی تست خواهیم گفت.
5.1 پوشش تست و ابزارهای مربوطه
ابزار go test
قابلیت بسیار مفیدی به نام پوشش تست را فراهم میکند که به ما کمک میکند که بفهمیم کدام بخشهای کد توسط تستها پوشش داده شدهاند و از طریق آن نواحی کدی که توسط تستها پوشش داده نشدهاند را کشف کنیم.
با استفاده از دستور go test -cover
، میتوانید درصد پوشش تست فعلی را ببینید:
go test -cover
اگر میخواهید درک دقیقتری از اینکه کدام خطوط کد اجرا شده و کدام نشدهاند داشته باشید، میتوانید از پارامتر -coverprofile
استفاده کنید که یک فایل دادهی پوشش را تولید میکند. سپس میتوانید از دستور go tool cover
برای تولید گزارش جزئیات پوشش تست استفاده کنید.
go test -coverprofile=coverage.out
go tool cover -html=coverage.out
دستور فوق یک گزارش وب را باز میکند و به صورت تصویری نشان میدهد که کدام خطوط از کد توسط تستها پوشش داده شدهاند و کدام نشدهاند. رنگ سبز نشاندهنده کد پوششدادهشده تست است، در حالی که رنگ قرمز خطوط کدی نشان دهنده صحت تست نشده هستند.
5.2 استفاده از موک (Mocks)
در زمان تستگیری، اغلب با سناریوهایی روبهرو میشویم که نیاز به شبیهسازی وابستگیهای خارجی داریم. موکها میتوانند به ما کمک کنند که این وابستگیها را شبیهسازی کرده و نیاز به وابستگی به خدمات یا منابع خارجی خاص در محیط تست را از بین ببریم.
در جامعه Go، ابزارهای زیادی برای موک وجود دارند مانند testify/mock
و gomock
. این ابزارها معمولاً مجموعهای از APIها را برای ایجاد و استفاده از شی موک فراهم میکنند.
در زیر مثال ابتدایی استفاده از testify/mock
آمده است. اولین کار این است که یک رابط و نسخه موک آن را تعریف کنیم:
type DataService interface {
FetchData() (int, error)
}
type MockDataService struct {
mock.Mock
}
func (m *MockDataService) FetchData() (int, error) {
args := m.Called()
return args.Int(0), args.Error(1)
}
در زمان تستگیری، میتوانیم از MockDataService
برای جایگزینی خدمات داده واقعی استفاده کنیم:
func TestSomething(t *testing.T) {
mockDataSvc := new(MockDataService)
mockDataSvc.On("FetchData").Return(42, nil) // پیکربندی رفتار مورد انتظار
result, err := mockDataSvc.FetchData() // استفاده از شی موک
assert.NoError(t, err)
assert.Equal(t, 42, result)
mockDataSvc.AssertExpectations(t) // تایید اینکه رفتار مورد انتظار رخ داده است
}
از طریق روی این نهج، میتوانیم در تستگیری از وابستگی به خدمات خارجی، تماسهای دیتابیس و غیره خودداری کنیم چرا که این کار میتواند اجرای تستها را تسریع کرده و تستهای ما را پایدارتر و قابل اطمینانتر کند.
6. تکنیکهای پیشرفته تست
بعد از مسلط شدن بر مبانی تست واحد Go، میتوانیم تکنیکهای پیشرفتهتری را بررسی کنیم که در ساخت نرمافزارهای محکمتر و بهبود کارایی تستها مفید هستند.
6.1 تست کردن توابع خصوصی
در گولنگ، توابع خصوصی معمولاً به توابعی اشاره دارند که قابل دسترسی نیستند، یعنی توابعی که نام آنها با حرف کوچک شروع میشود. معمولاً ما ترجیح میدهیم که رابطهای عمومی را تست کنیم زیرا آنها نشاندهنده قابلیت استفاده از کد هستند. با این حال، مواردی وجود دارند که تست مستقیم توابع خصوصی هم معنی پیدا میکند، مانند زمانی که تابع خصوصی منطق پیچیدهای دارد و توسط چند تابع عمومی فراخوانی میشود.
تست کردن توابع خصوصی از تست کردن توابع عمومی بخصوصیتهای آنها بخوبی متفاوت است چرا که از خارج از بسته قابل دسترسی نیستند. یک تکنیک معمول این است که کد تست را در همان بسته بنویسیم تا بتوانیم به صورت مستقیم به تابع خصوصی دسترسی داشته باشیم.
در زیر یک مثال ساده آمده است:
// calculator.go
package calculator
func add(a, b int) int {
return a + b
}
فایل تست مربوطه به این صورت است:
// calculator_test.go
package calculator
import "testing"
func TestAdd(t *testing.T) {
expected := 4
actual := add(2, 2)
if actual != expected {
t.Errorf("انتظار داشتیم %d، دریافت شد %d", expected, actual)
}
}
با قرار دادن فایل تست در همان بسته، میتوانیم به صورت مستقیم تابع add
را تست کنیم.
6.2 الگوهای تست مشترک و بهترین اصول
تست واحد در گولنگ الگوهای مشترکی دارد که به تسهیل تست کار کمک کرده و به حفظ وضوح کد و قابلیت نگهداری کمک میکند.
-
تستهای جدولی
تستگیری مبتنی بر جدول یک روش برای سازماندهی ورودیها و خروجیهای مورد تست است. با تعریف یک مجموعه از موارد تست و سپس حلقه زدن از آنها برای تست، این روش افزودن موارد تست جدید را بسیار آسان میکند و همچنین کد را خوانا و قابل نگهداری میکند.
// calculator_test.go
package calculator
import "testing"
func TestAddTableDriven(t *testing.T) {
var tests = []struct {
a, b int
want int
}{
{1, 2, 3},
{2, 2, 4},
{5, -1, 4},
}
for _, tt := range tests {
testname := fmt.Sprintf("%d,%d", tt.a, tt.b)
t.Run(testname, func(t *testing.T) {
ans := add(tt.a, tt.b)
if tt.want != ans {
t.Errorf("دریافت شد %d، انتظار داشتیم %d", ans, tt.want)
}
})
}
}
-
استفاده از موکها برای تست
موک کردن یک تکنیک تست است که در آن وابستگیها را با هدف تست اجزا مختلفی جایگزین میکند. در گولنگ، رابطها راه اصلی برای پیادهسازی موکها هستند. با استفاده از رابطها، میتوان یک اجرای موک ایجاد کرده و سپس آن را در تست استفاده کرد.