Unit Testing
Test individual components in isolation.
Import
import (
"testing"
goosetest "github.com/awesome-goose/goose/testing"
)
What is Unit Testing?
Unit tests verify that individual functions and methods work correctly in isolation from the rest of the system.
Testing Services
Basic Service Test
// users.service_test.go
package users
import (
"testing"
goosetest "github.com/awesome-goose/goose/testing"
)
func TestUserService_ValidateEmail(t *testing.T) {
test := goosetest.New(t)
service := &UserService{}
tests := []struct {
email string
valid bool
}{
{"user@example.com", true},
{"invalid", false},
{"", false},
{"user@", false},
}
for _, tc := range tests {
t.Run(tc.email, func(t *testing.T) {
result := service.ValidateEmail(tc.email)
test.Expect(result).ToEqual(tc.valid)
})
}
}
Service with Dependencies Using TestContainer
import (
"testing"
goosetest "github.com/awesome-goose/goose/testing"
)
// Mock database interface
type MockDB struct {
users map[string]*User
}
func (m *MockDB) FindByID(id string) (*User, error) {
user, ok := m.users[id]
if !ok {
return nil, errors.New("not found")
}
return user, nil
}
func (m *MockDB) Create(user *User) error {
m.users[user.ID] = user
return nil
}
func TestUserService_GetByID(t *testing.T) {
test := goosetest.New(t)
container := goosetest.NewTestContainer()
mockDB := &MockDB{
users: map[string]*User{
"123": {ID: "123", Email: "test@example.com"},
},
}
container.Register(mockDB)
service := &UserService{}
container.Inject(service)
t.Run("existing user", func(t *testing.T) {
user, err := service.GetByID("123")
test.Expect(err).ToBeNil()
test.Expect(user.Email).ToEqual("test@example.com")
})
t.Run("non-existing user", func(t *testing.T) {
_, err := service.GetByID("999")
test.Expect(err).Not().ToBeNil()
})
}
Using ServiceTest Helper
import (
"testing"
goosetest "github.com/awesome-goose/goose/testing"
)
func TestUserServiceWithHelper(t *testing.T) {
gt := goosetest.NewGooseTest(t)
gt.WithContainer(func(c *goosetest.TestContainer) {
c.Register(&MockDB{users: make(map[string]*User)})
})
service := &UserService{}
st := gt.Service(service)
// Call method and get results
results := st.Run("Create", CreateUserDTO{Email: "test@example.com"})
gt.Expect(results[0]).Not().ToBeNil() // user
gt.Expect(results[1]).ToBeNil() // error
}
Testing Business Logic
Pure Functions
import (
"testing"
goosetest "github.com/awesome-goose/goose/testing"
)
func TestCalculateDiscount(t *testing.T) {
test := goosetest.New(t)
tests := []struct {
name string
price float64
percent float64
expected float64
}{
{"10% off $100", 100.00, 10, 90.00},
{"25% off $80", 80.00, 25, 60.00},
{"no discount", 50.00, 0, 50.00},
{"100% off", 100.00, 100, 0.00},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := calculateDiscount(tc.price, tc.percent)
test.Expect(result).ToEqual(tc.expected)
})
}
}
Complex Logic
import (
"testing"
goosetest "github.com/awesome-goose/goose/testing"
)
func TestOrderService_CalculateTotal(t *testing.T) {
test := goosetest.New(t)
service := &OrderService{}
order := &Order{
Items: []OrderItem{
{ProductID: "1", Price: 10.00, Quantity: 2},
{ProductID: "2", Price: 25.00, Quantity: 1},
},
Discount: 5.00,
ShippingCost: 10.00,
}
total := service.CalculateTotal(order)
// Items: (10*2) + (25*1) = 45
// Discount: -5
// Shipping: +10
// Total: 50
test.Expect(total).ToEqual(50.00)
}
Testing Error Handling
import (
"strings"
"testing"
goosetest "github.com/awesome-goose/goose/testing"
)
func TestUserService_Create_ValidationErrors(t *testing.T) {
test := goosetest.New(t)
service := &UserService{}
tests := []struct {
name string
dto CreateUserDTO
expectError bool
errorMsg string
}{
{
name: "empty email",
dto: CreateUserDTO{Email: "", Name: "Test"},
expectError: true,
errorMsg: "email is required",
},
{
name: "invalid email",
dto: CreateUserDTO{Email: "invalid", Name: "Test"},
expectError: true,
errorMsg: "invalid email format",
},
{
name: "empty name",
dto: CreateUserDTO{Email: "test@example.com", Name: ""},
expectError: true,
errorMsg: "name is required",
},
{
name: "valid input",
dto: CreateUserDTO{Email: "test@example.com", Name: "Test"},
expectError: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
_, err := service.Create(tc.dto)
if tc.expectError {
test.Expect(err).Not().ToBeNil()
test.Expect(strings.Contains(err.Error(), tc.errorMsg)).ToBeTrue()
} else {
test.Expect(err).ToBeNil()
}
})
}
}
Testing Edge Cases
import (
"math"
"testing"
goosetest "github.com/awesome-goose/goose/testing"
)
func TestUserService_EdgeCases(t *testing.T) {
test := goosetest.New(t)
service := &UserService{}
t.Run("nil input", func(t *testing.T) {
_, err := service.Process(nil)
test.Expect(err).Not().ToBeNil()
})
t.Run("empty slice", func(t *testing.T) {
result := service.ProcessMany([]User{})
test.Expect(len(result)).ToEqual(0)
})
t.Run("boundary values", func(t *testing.T) {
// Test max int
result := service.Calculate(math.MaxInt64)
test.Expect(result).ToBeGreaterThan(0)
})
}
Testing Controllers
Use ControllerTest to test controllers with dependency injection:
import (
"testing"
goosetest "github.com/awesome-goose/goose/testing"
)
func TestUserController(t *testing.T) {
gt := goosetest.NewGooseTest(t)
// Set up mock dependencies
gt.WithContainer(func(c *goosetest.TestContainer) {
c.Register(&MockUserService{})
})
controller := &UserController{}
ct := gt.Controller(controller)
// Create mock context
ctx := gt.NewContext()
ctx.MockRequest().WithMethod("GET").WithPaths("users", "123")
// Run controller method
ct.Run("GetUser", ctx)
// Assert response
response := ctx.MockResponse()
gt.Expect(response.StatusCode()).ToEqual(200)
}
Testing Modules
Use ModuleTest to verify module structure:
import (
"testing"
goosetest "github.com/awesome-goose/goose/testing"
"myapp/app/users"
)
func TestUsersModule(t *testing.T) {
gt := goosetest.NewGooseTest(t)
mt := gt.Module(&users.UsersModule{})
t.Run("should have valid imports", func(t *testing.T) {
imports := mt.TestImports()
gt.Expect(len(imports)).ToBeGreaterThan(0)
})
t.Run("should have valid exports", func(t *testing.T) {
exports := mt.TestExports()
gt.Expect(exports).Not().ToBeNil()
})
t.Run("should have valid declarations", func(t *testing.T) {
declarations := mt.TestDeclarations()
gt.Expect(len(declarations)).ToBeGreaterThan(0)
})
}
Testing Routes
Use RouteTest to verify route configuration:
import (
"testing"
goosetest "github.com/awesome-goose/goose/testing"
"myapp/app/users"
)
func TestUsersRoutes(t *testing.T) {
gt := goosetest.NewGooseTest(t)
rt := gt.Route(users.ROUTES)
t.Run("should have GET /users route", func(t *testing.T) {
rt.TestRouteExists("GET", []string{"users"})
})
t.Run("should have GET /users/:id route", func(t *testing.T) {
rt.TestRouteExists("GET", []string{"users", "123"})
})
t.Run("should have POST /users route", func(t *testing.T) {
rt.TestRouteExists("POST", []string{"users"})
})
t.Run("should extract route params", func(t *testing.T) {
rt.TestRouteParams("GET", []string{"users", "456"}, map[string]string{"id": "456"})
})
t.Run("should return not found for invalid route", func(t *testing.T) {
rt.TestRouteNotFound("DELETE", []string{"invalid"})
})
}
Testing Structs
Constructor Tests
import (
"testing"
goosetest "github.com/awesome-goose/goose/testing"
)
func TestNewUser(t *testing.T) {
test := goosetest.New(t)
user := NewUser("test@example.com", "Test User")
test.Expect(user.ID).Not().ToEqual("")
test.Expect(user.Email).ToEqual("test@example.com")
test.Expect(user.CreatedAt.IsZero()).ToBeFalse()
}
Method Tests
import (
"testing"
"time"
goosetest "github.com/awesome-goose/goose/testing"
)
func TestUser_IsActive(t *testing.T) {
test := goosetest.New(t)
now := time.Now()
tests := []struct {
name string
user User
expected bool
}{
{
name: "active user",
user: User{Active: true, DeletedAt: nil},
expected: true,
},
{
name: "inactive user",
user: User{Active: false, DeletedAt: nil},
expected: false,
},
{
name: "deleted user",
user: User{Active: true, DeletedAt: &now},
expected: false,
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := tc.user.IsActive()
test.Expect(result).ToEqual(tc.expected)
})
}
}
Using Test Fixtures
Generate random test data with Fixture:
import (
"testing"
goosetest "github.com/awesome-goose/goose/testing"
)
func TestWithRandomData(t *testing.T) {
test := goosetest.New(t)
fixture := goosetest.NewFixture()
// Generate random user data
user := &User{
Email: fixture.Email(),
Name: fixture.String(10),
Age: fixture.Int(18, 65),
Active: fixture.Bool(),
}
// Test with random data
err := service.Create(user)
test.Expect(err).ToBeNil()
}
Next Steps
- Integration Testing - Test module interactions
- HTTP Testing - Test API endpoints
- E2E Testing - Test full user flows
Testing Concurrency
func TestCounter_ThreadSafe(t *testing.T) {
counter := NewCounter()
var wg sync.WaitGroup
iterations := 1000
// Increment concurrently
for i := 0; i < iterations; i++ {
wg.Add(1)
go func() {
defer wg.Done()
counter.Increment()
}()
}
wg.Wait()
if counter.Value() != iterations {
t.Errorf("expected %d, got %d", iterations, counter.Value())
}
}
Benchmarks
func BenchmarkUserService_ValidateEmail(b *testing.B) {
service := &UserService{}
email := "test@example.com"
b.ResetTimer()
for i := 0; i < b.N; i++ {
service.ValidateEmail(email)
}
}
func BenchmarkCalculateDiscount(b *testing.B) {
for i := 0; i < b.N; i++ {
calculateDiscount(100.00, 15)
}
}
// Run benchmarks: go test -bench=.
Best Practices
- Test one thing at a time - Each test should verify one behavior
- Use descriptive names -
TestUserService_Create_WithInvalidEmail_ReturnsError - Keep tests fast - Unit tests should run in milliseconds
- Don't test private methods - Test through public API
- Use test helpers - Reduce duplication with helper functions
- Test error paths - Not just happy paths
- Avoid test interdependence - Each test should be independent
Next Steps
- Integration Testing - Test module interactions
- Mocking - Create test doubles
- HTTP Testing - Test API endpoints