نماد سایت امپراطوری من

تست نویسی در گولنگ – بخش دوم

go-test-poster

در این آموزش میخواهیم در مورد تعریف تست واحد( Unit Test)، ویژگی های آن و بایدها و نبایدهایی که هنگام نوشتن تست واحد در نظر گرفت صحبت کنیم.

مفهوم تست واحد (Unit Test) در گولنگ

تست واحد یعنی بررسی درست‌کارکردن کوچک‌ترین بخش قابل تست کد (واحد = Unit) به‌صورت ایزوله، یعنی بدون وابستگی واقعی به پایگاه داده، شبکه، فایل‌سیستم یا بخش‌های دیگر برنامه. تعریف تست واحد در زبان برنامه نویسی گولنگ می‌تونه یک:

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

ویژگی‌های یک Unit Test

هر Unit Test باید همیشه این ۴ ویژگی را داشته باشد:

چه توابعی را می‌توان Unit Test کرد؟

بهترین کاندیدها:

توابع Pure (بدون side-effect)

مثل: محاسبه جمع، فرمت‌دهی رشته، پارس JSON

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

این نوع توابع عالی‌اند چون خروجی‌شان فقط به ورودی بستگی دارد.

توابع با منطق داخلی مهم ولی با وابستگی‌ها قابل شبیه‌سازی

فرض کن تابعی داریم که مجموع قیمت سفارش را محاسبه می‌کند، ولی برای گرفتن قیمت هر کالا به یک سرویس خارجی وابسته است. در Unit Test، این سرویس خارجی را mock می‌کنیم تا وابستگی واقعی حذف شود و فقط منطق داخلی تابع را تست کنیم. به کد زیر توجه کنید:

package order

type PriceService interface {
    GetPrice(productID int) float64
}

func CalculateOrderTotal(ps PriceService, productIDs []int) float64 {
    total := 0.0
    for _, id := range productIDs {
        price := ps.GetPrice(id) // وابستگی خارجی
        total += price
    }
    // منطق داخلی: اعمال تخفیف اگر جمع کل بالای 100 باشد
    if total > 100 {
        total *= 0.9
    }
    return total
}

در مثال بالا، تابع CalculateOrderTotal نیازمند یک struct است که توابع موجود در interface با نام PriceService را پیاده سازی کند. به بیان ساده، این تابع در ورودی یک struct میگیرد که باید حتما داخلش تابع GetPrice تعریف شده باشد. فرض کنید تابع GetPrice قیمت ها را از یک سرویس بیرونی می‌گیرد. برای اینکه بتوانیم برای تابع CalculateOrderTotal تست واحد بنویسیم، میتوانیم در داخل تست خودمان، نیازمندی تابع CalculateOrderTotal را با یک struct دیگر که همان تابع را دارد و رفتار struct اصلی را شبیه سازی می‌کند، جایگزین کنیم. به کد تست زیر دقت کنید:

package order

import "testing"

// Mock ساده برای PriceService
type mockPriceService struct{}

func (m mockPriceService) GetPrice(productID int) float64 {
    return 50 // قیمت ثابت برای سادگی
}

func TestCalculateOrderTotal_DiscountApplied(t *testing.T) {
    ps := mockPriceService{}
    products := []int{1, 2, 3} // سه محصول با قیمت 50

    got := CalculateOrderTotal(ps, products)
    want := 135.0 // جمع = 150 → با 10% تخفیف = 135

    if got != want {
        t.Fatalf("got %.2f, want %.2f", got, want)
    }
}

در این مثال:

متدهای struct که state داخلی را تغییر می‌دهند

مثل: Account.Deposit(amount) که موجودی را زیاد می‌کند. به کد زیر توجه کنید. یک struct داریم که کار حساب بانکی رو انجام میده و داخلش متغیری برای نگهداری موجودی داره. این موجودی همون state هست که میخواهیم تستش کنیم. دو تابع Deposit و Balance برای واریز به حساب و موجودی گرفتن استفاده میشن.

package bank

type Account struct {
    balance float64
}

func NewAccount(initialBalance float64) *Account {
    return &Account{balance: initialBalance}
}

func (a *Account) Deposit(amount float64) {
    if amount > 0 {
        a.balance += amount
    }
}

func (a *Account) Balance() float64 {
    return a.balance
}

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

package bank

import "testing"

func TestAccount_Deposit(t *testing.T) {
    // Arrange
    acc := NewAccount(100) // موجودی اولیه = 100

    // Act
    acc.Deposit(50)

    // Assert
    got := acc.Balance()
    want := 150.0
    if got != want {
        t.Fatalf("got %.2f, want %.2f", got, want)
    }
}

 

چه توابعی را بهتر است Unit Test نکنیم؟

توابعی که فقط wrapper یا عبوردهنده به لایه پایین‌تر هستند (بدون منطق واقعی)

ساده ترین و پرکاربردترین مثال برای این حالت وقتی است که شما از الگوی سرویس/ریپازیتوری استفاده می‌کنید. در این حالت ممکن است توابعی در سرویس وجود داشته باشند که عینا خروجی خود را از متدی در ریپازیتوری دریافت می‌کنند و خودشان صرفا نقش wrapper دارند. در این وضعیت تست اصلی باید روی متد repo نوشته شود (یا روی بخش بالادستی که این سرویس را استفاده می‌کند). حالا اگر repo تست شده باشد، تست مجدد این تابع تکراری و بدون ارزش افزوده است.

package user

type Repository interface {
    FindByID(id int) (string, error)
}

type Service struct {
    repo Repository
}

// GetUserName فقط متد Repository رو صدا می‌زنه و هیچ منطق اضافه‌ای نداره
func (s *Service) GetUserName(id int) (string, error) {
    return s.repo.FindByID(id) // عبور مستقیم بدون تغییر
}

توابعی که کاملاً به I/O خارجی متکی‌اند (مثل آپلود فایل روی S3)

اینها را بهتر است Integration Test کنیم، و برای Unit Test نسخه mock بنویسیم. توابعی که کاملاً به I/O خارجی وابسته‌اند، یعنی برای انجام کار حتماً باید به یک منبع خارجی وصل شوند، مثل:

در Unit Test، این توابع مستقیم تست نمی‌شوند چون:

به جایش:

توابع private که فقط از public function‌ ها صدا زده می‌شوند

در گولنگ، توابعی که با حرف کوچک شروع می‌شوند (مثل calculateTax) به صورت unexported یا اصطلاحاً private شناخته می‌شوند؛ یعنی فقط داخل همان پکیج قابل استفاده‌اند.

package tax

// تابع private — فقط داخل پکیج استفاده می‌شود
func calculateTax(amount float64) float64 {
    return amount * 0.1
}

// تابع public
func FinalPrice(amount float64) float64 {
    tax := calculateTax(amount)
    return amount + tax
}

وقتی یک تابع private فقط از طریق یک تابع public صدا زده می‌شود و هیچ استفاده‌ی دیگری ندارد،
نیازی نیست مستقیم برایش تست جداگانه بنویسیم.
بهتر است تابع public را تست کنیم، چون:

در نتیجه برای توابع private نیازی به نوشتن تست واحد نداریم.

package tax

import "testing"

func TestFinalPrice(t *testing.T) {
    got := FinalPrice(100)
    want := 110.0
    if got != want {
        t.Fatalf("got %.2f, want %.2f", got, want)
    }
}

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

خروج از نسخه موبایل