Documentation

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.