Testing
Overview
Testing ensures your application works correctly and remains stable as it evolves. The framework supports comprehensive testing strategies including unit tests, integration tests, and functional tests.
Unit Testing
Testing Maestros
func TestCreateGardenMae_Success(t *testing.T) {
// Setup
container := setupTestContainer()
mae := container.CreateGardenMae
input := &maes.CreateGardenMaeInput{
Form: &forms.CreateGardenForm{
Name: "Test Garden",
Description: "A test garden",
Location: "Test Location",
UserId: "user123",
},
}
// Execute
output, err := mae.Execute(context.Background(), input)
// Assert
require.NoError(t, err)
assert.NotEmpty(t, output.Garden.Id)
assert.Equal(t, "Test Garden", output.Garden.Name)
assert.Equal(t, "A test garden", output.Garden.Description)
assert.Equal(t, "user123", output.Garden.UserId)
assert.True(t, output.Garden.Active)
}
func TestCreateGardenMae_ValidationError(t *testing.T) {
container := setupTestContainer()
mae := container.CreateGardenMae
input := &maes.CreateGardenMaeInput{
Form: &forms.CreateGardenForm{
Name: "", // Invalid - empty name
},
}
output, err := mae.Execute(context.Background(), input)
assert.Error(t, err)
assert.Nil(t, output)
assert.Contains(t, err.Error(), "name")
}
Testing Services
func TestGardenFetcher_FindOneById(t *testing.T) {
db := setupTestDB()
defer db.Close()
fetcher := srv.NewGardenFetcher(db, nil)
// Create test data
testGarden := &mdl.Garden{
Id: "test-garden-1",
Name: "Test Garden",
Description: "Test Description",
Active: true,
}
insertTestGarden(db, testGarden)
// Test successful fetch
garden, exists, err := fetcher.FindOneById(context.Background(), "test-garden-1", nil)
require.NoError(t, err)
assert.True(t, exists)
assert.Equal(t, "test-garden-1", garden.Id)
assert.Equal(t, "Test Garden", garden.Name)
// Test non-existent garden
garden, exists, err = fetcher.FindOneById(context.Background(), "non-existent", nil)
require.NoError(t, err)
assert.False(t, exists)
assert.Nil(t, garden)
}
func TestGardenHandler_Create(t *testing.T) {
db := setupTestDB()
defer db.Close()
handler := srv.NewGardenHandler(db, nil)
garden := &mdl.Garden{
Name: "New Garden",
Description: "New garden description",
UserId: "user123",
Active: true,
}
createdGarden, err := handler.Create(context.Background(), garden, nil)
require.NoError(t, err)
assert.NotEmpty(t, createdGarden.Id)
assert.Equal(t, "New Garden", createdGarden.Name)
assert.False(t, createdGarden.CreatedAt.IsZero())
assert.False(t, createdGarden.UpdatedAt.IsZero())
}
Testing Controllers
func TestGardenController_Create(t *testing.T) {
container := setupTestContainer()
router := setupTestRouter(container)
gardenJSON := `{
"name": "Test Garden",
"description": "Test Description",
"location": "Test Location"
}`
w := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/gardens", strings.NewReader(gardenJSON))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+validTestToken)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
garden := response["garden"].(map[string]interface{})
assert.Equal(t, "Test Garden", garden["name"])
assert.NotEmpty(t, garden["id"])
}
func TestGardenController_Show(t *testing.T) {
container := setupTestContainer()
router := setupTestRouter(container)
// Create test garden
testGarden := createTestGarden(container)
w := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/gardens/"+testGarden.Id, nil)
req.Header.Set("Authorization", "Bearer "+validTestToken)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var response map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
garden := response["garden"].(map[string]interface{})
assert.Equal(t, testGarden.Id, garden["id"])
assert.Equal(t, testGarden.Name, garden["name"])
}
Integration Testing
Database Integration Tests
func TestGardenIntegration_CRUD(t *testing.T) {
container := setupIntegrationContainer()
defer container.Cleanup()
gardenHandler := container.GardenHandler
gardenFetcher := container.GardenFetcher
ctx := context.Background()
// Create
garden := &mdl.Garden{
Name: "Integration Test Garden",
Description: "Test garden for integration testing",
UserId: "test-user",
Active: true,
}
createdGarden, err := gardenHandler.Create(ctx, garden, nil)
require.NoError(t, err)
assert.NotEmpty(t, createdGarden.Id)
// Read
fetchedGarden, exists, err := gardenFetcher.FindOneById(ctx, createdGarden.Id, nil)
require.NoError(t, err)
assert.True(t, exists)
assert.Equal(t, createdGarden.Name, fetchedGarden.Name)
// Update
fetchedGarden.Name = "Updated Garden Name"
updatedGarden, err := gardenHandler.Update(ctx, fetchedGarden, nil)
require.NoError(t, err)
assert.Equal(t, "Updated Garden Name", updatedGarden.Name)
// Delete
err = gardenHandler.Delete(ctx, updatedGarden, nil)
require.NoError(t, err)
// Verify deletion
_, exists, err = gardenFetcher.FindOneById(ctx, updatedGarden.Id, nil)
require.NoError(t, err)
assert.False(t, exists)
}
API Integration Tests
func TestGardenAPI_Integration(t *testing.T) {
container := setupIntegrationContainer()
defer container.Cleanup()
router := setupTestRouter(container)
token := generateTestJWT("test-user")
// Test creating a garden
createData := map[string]interface{}{
"name": "API Test Garden",
"description": "Garden created via API test",
"location": "Test Location",
}
createJSON, _ := json.Marshal(createData)
w := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/api/v1/gardens", bytes.NewReader(createJSON))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusCreated, w.Code)
var createResponse map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &createResponse)
require.NoError(t, err)
garden := createResponse["garden"].(map[string]interface{})
gardenId := garden["id"].(string)
// Test fetching the created garden
w = httptest.NewRecorder()
req = httptest.NewRequest("GET", "/api/v1/gardens/"+gardenId, nil)
req.Header.Set("Authorization", "Bearer "+token)
router.ServeHTTP(w, req)
assert.Equal(t, http.StatusOK, w.Code)
var fetchResponse map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &fetchResponse)
require.NoError(t, err)
fetchedGarden := fetchResponse["garden"].(map[string]interface{})
assert.Equal(t, gardenId, fetchedGarden["id"])
assert.Equal(t, "API Test Garden", fetchedGarden["name"])
}
Functional Testing
End-to-End Workflow Tests
func TestCompleteGardenWorkflow(t *testing.T) {
container := setupIntegrationContainer()
defer container.Cleanup()
ctx := context.Background()
userId := "test-user-workflow"
// Step 1: Create a user
user := &mdl.User{
Email: "test@example.com",
Name: "Test User",
Password: "hashedpassword",
Active: true,
}
createdUser, err := container.UserHandler.Create(ctx, user, nil)
require.NoError(t, err)
// Step 2: Create a garden for the user
createGardenInput := &maes.CreateGardenMaeInput{
Form: &forms.CreateGardenForm{
Name: "Workflow Test Garden",
Description: "Testing complete workflow",
Location: "Test Location",
UserId: createdUser.Id,
},
}
gardenOutput, err := container.CreateGardenMae.Execute(ctx, createGardenInput)
require.NoError(t, err)
garden := gardenOutput.Garden
// Step 3: Add plants to the garden
plantForms := []*forms.CreatePlantForm{
{Name: "Tomato", Species: "Solanum lycopersicum", GardenId: garden.Id},
{Name: "Lettuce", Species: "Lactuca sativa", GardenId: garden.Id},
}
for _, plantForm := range plantForms {
createPlantInput := &maes.CreatePlantMaeInput{Form: plantForm}
_, err := container.CreatePlantMae.Execute(ctx, createPlantInput)
require.NoError(t, err)
}
// Step 4: Verify garden with plants
gardenWithPlants, exists, err := container.GardenFetcher.FindOneById(ctx, garden.Id, nil)
require.NoError(t, err)
assert.True(t, exists)
// Hydrate plants
hydratorMod := container.GardenHydrator.Mod()
hydratorMod.AddHydratingPath("plants")
err = container.GardenHydrator.One(ctx, gardenWithPlants, hydratorMod)
require.NoError(t, err)
assert.Len(t, gardenWithPlants.Plants, 2)
plantNames := make([]string, len(gardenWithPlants.Plants))
for i, plant := range gardenWithPlants.Plants {
plantNames[i] = plant.Name
}
assert.Contains(t, plantNames, "Tomato")
assert.Contains(t, plantNames, "Lettuce")
// Step 5: Update garden
updateGardenInput := &maes.UpdateGardenMaeInput{
GardenId: garden.Id,
Form: &forms.UpdateGardenForm{
Name: "Updated Workflow Garden",
Description: "Updated description",
},
}
updatedGardenOutput, err := container.UpdateGardenMae.Execute(ctx, updateGardenInput)
require.NoError(t, err)
assert.Equal(t, "Updated Workflow Garden", updatedGardenOutput.Garden.Name)
}
Test Utilities
Test Container Setup
func setupTestContainer() *container.Container {
// Use in-memory SQLite for tests
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
panic(err)
}
// Load test schema
schema, err := os.ReadFile("data/schema.sqlite.sql")
if err != nil {
panic(err)
}
_, err = db.Exec(string(schema))
if err != nil {
panic(err)
}
config := &config.Config{
Database: config.Database{
Driver: "sqlite3",
Path: ":memory:",
},
JWT: config.JWT{
Secret: "test-secret-key",
},
}
return container.NewContainer(db, config, log.New(io.Discard, "", 0))
}
func setupIntegrationContainer() *container.Container {
// Use temporary file database for integration tests
tempFile, err := os.CreateTemp("", "test_db_*.sqlite")
if err != nil {
panic(err)
}
tempFile.Close()
db, err := sql.Open("sqlite3", tempFile.Name())
if err != nil {
panic(err)
}
schema, err := os.ReadFile("data/schema.sqlite.sql")
if err != nil {
panic(err)
}
_, err = db.Exec(string(schema))
if err != nil {
panic(err)
}
config := &config.Config{
Database: config.Database{
Driver: "sqlite3",
Path: tempFile.Name(),
},
}
c := container.NewContainer(db, config, log.New(io.Discard, "", 0))
// Add cleanup function
c.Cleanup = func() {
db.Close()
os.Remove(tempFile.Name())
}
return c
}
Test Data Factories
type TestDataFactory struct {
container *container.Container
}
func NewTestDataFactory(container *container.Container) *TestDataFactory {
return &TestDataFactory{container: container}
}
func (tdf *TestDataFactory) CreateUser(overrides ...*mdl.User) *mdl.User {
user := &mdl.User{
Email: "test@example.com",
Name: "Test User",
PasswordHash: "$2a$10$hash",
Active: true,
Role: "user",
}
// Apply overrides
if len(overrides) > 0 && overrides[0] != nil {
if overrides[0].Email != "" {
user.Email = overrides[0].Email
}
if overrides[0].Name != "" {
user.Name = overrides[0].Name
}
if overrides[0].Role != "" {
user.Role = overrides[0].Role
}
}
createdUser, err := tdf.container.UserHandler.Create(context.Background(), user, nil)
if err != nil {
panic(err)
}
return createdUser
}
func (tdf *TestDataFactory) CreateGarden(userId string, overrides ...*mdl.Garden) *mdl.Garden {
garden := &mdl.Garden{
Name: "Test Garden",
Description: "Test garden description",
Location: "Test Location",
UserId: userId,
Active: true,
}
if len(overrides) > 0 && overrides[0] != nil {
if overrides[0].Name != "" {
garden.Name = overrides[0].Name
}
if overrides[0].Description != "" {
garden.Description = overrides[0].Description
}
}
createdGarden, err := tdf.container.GardenHandler.Create(context.Background(), garden, nil)
if err != nil {
panic(err)
}
return createdGarden
}
func (tdf *TestDataFactory) CreatePlant(gardenId string, overrides ...*mdl.Plant) *mdl.Plant {
plant := &mdl.Plant{
Name: "Test Plant",
Species: "Testicus planticus",
GardenId: gardenId,
Height: 10.5,
Edible: true,
}
if len(overrides) > 0 && overrides[0] != nil {
if overrides[0].Name != "" {
plant.Name = overrides[0].Name
}
if overrides[0].Species != "" {
plant.Species = overrides[0].Species
}
}
createdPlant, err := tdf.container.PlantHandler.Create(context.Background(), plant, nil)
if err != nil {
panic(err)
}
return createdPlant
}
Mocking and Stubs
Service Mocking
type MockGardenFetcher struct {
FindOneByIdFunc func(ctx context.Context, id string, mod *fetcher.FetcherMod) (*mdl.Garden, bool, error)
FindAllFunc func(ctx context.Context, mod *fetcher.FetcherMod) ([]*mdl.Garden, error)
}
func (m *MockGardenFetcher) FindOneById(ctx context.Context, id string, mod *fetcher.FetcherMod) (*mdl.Garden, bool, error) {
if m.FindOneByIdFunc != nil {
return m.FindOneByIdFunc(ctx, id, mod)
}
return nil, false, nil
}
func (m *MockGardenFetcher) FindAll(ctx context.Context, mod *fetcher.FetcherMod) ([]*mdl.Garden, error) {
if m.FindAllFunc != nil {
return m.FindAllFunc(ctx, mod)
}
return []*mdl.Garden{}, nil
}
// Usage in tests
func TestCreateGardenMae_WithMock(t *testing.T) {
mockFetcher := &MockGardenFetcher{
FindOneByIdFunc: func(ctx context.Context, id string, mod *fetcher.FetcherMod) (*mdl.Garden, bool, error) {
return &mdl.Garden{Id: id, Name: "Mocked Garden"}, true, nil
},
}
// Use mock in test...
}
Test Configuration
Test Environment Variables
# .env.test
ENV=test
DB_DRIVER=sqlite3
DB_PATH=:memory:
JWT_SECRET=test-secret-key
LOG_LEVEL=error
Running Tests
# Run all tests
go test ./...
# Run specific test package
go test ./tests/functional/maes
# Run with coverage
go test -cover ./...
# Run with race detection
go test -race ./...
# Run specific test
go test -run TestCreateGardenMae_Success ./tests/functional/maes
# Verbose output
go test -v ./...
# Generate coverage report
go test -coverprofile=coverage.out ./...
go tool cover -html=coverage.out -o coverage.html
Testing ensures your application works correctly and helps catch regressions early. A comprehensive test suite gives you confidence to refactor and extend your application.