Skip to main content

Testing Guide

Station has comprehensive test coverage across unit, integration, and end-to-end tests.

Running Tests

All Tests

# Run all tests
make test

# Or directly with Go
go test ./...

# With verbose output
go test ./... -v

# With race detection
go test ./... -race

Specific Packages

# Test a specific package
go test ./internal/services/... -v

# Test a specific file
go test ./internal/services/agent_service_test.go -v

# Test a specific function
go test ./internal/services -run TestAgentExecution -v

Test Coverage

# Generate coverage report
make test-coverage

# Or manually
go test ./... -coverprofile=coverage.out
go tool cover -html=coverage.out -o coverage.html

# View coverage in terminal
go tool cover -func=coverage.out
Current test coverage: ~52.7%

Test Structure

station/
├── cmd/main/handlers/
│   └── agent/
│       ├── execution.go
│       └── execution_test.go      # Handler tests
├── internal/
│   ├── services/
│   │   ├── agent_service.go
│   │   └── agent_service_test.go  # Service tests
│   ├── api/v1/
│   │   ├── agents.go
│   │   └── agents_test.go         # API tests
│   └── db/repositories/
│       ├── agent_repository.go
│       └── agent_repository_test.go  # Repository tests
└── tests/
    └── integration/               # Integration tests

Writing Tests

Unit Test Pattern

package services

import (
    "context"
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestAgentService_Execute(t *testing.T) {
    // Arrange
    ctx := context.Background()
    svc := NewAgentService(mockRepo, mockEngine)

    tests := []struct {
        name    string
        agentID int64
        task    string
        want    string
        wantErr bool
    }{
        {
            name:    "successful execution",
            agentID: 1,
            task:    "Hello",
            want:    "Response",
            wantErr: false,
        },
        {
            name:    "agent not found",
            agentID: 999,
            task:    "Hello",
            want:    "",
            wantErr: true,
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            // Act
            got, err := svc.Execute(ctx, tt.agentID, tt.task)

            // Assert
            if tt.wantErr {
                require.Error(t, err)
                return
            }
            require.NoError(t, err)
            assert.Equal(t, tt.want, got)
        })
    }
}

Table-Driven Tests

Station uses table-driven tests extensively:
func TestValidateCronExpression(t *testing.T) {
    tests := []struct {
        name       string
        expression string
        valid      bool
    }{
        {"valid 6-field", "0 0 9 * * *", true},
        {"valid 5-field", "0 9 * * *", true},
        {"invalid format", "not-a-cron", false},
        {"empty", "", false},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := ValidateCronExpression(tt.expression)
            if tt.valid {
                assert.NoError(t, err)
            } else {
                assert.Error(t, err)
            }
        })
    }
}

Mocking

Station uses interfaces for testability. Generate mocks with:
make mocks
Example mock usage:
type MockAgentRepository struct {
    mock.Mock
}

func (m *MockAgentRepository) GetByID(ctx context.Context, id int64) (*Agent, error) {
    args := m.Called(ctx, id)
    if args.Get(0) == nil {
        return nil, args.Error(1)
    }
    return args.Get(0).(*Agent), args.Error(1)
}

func TestWithMock(t *testing.T) {
    mockRepo := new(MockAgentRepository)
    mockRepo.On("GetByID", mock.Anything, int64(1)).
        Return(&Agent{ID: 1, Name: "test"}, nil)

    svc := NewAgentService(mockRepo)
    agent, err := svc.Get(context.Background(), 1)

    require.NoError(t, err)
    assert.Equal(t, "test", agent.Name)
    mockRepo.AssertExpectations(t)
}

Integration Tests

Integration tests use real databases and services:
func TestIntegration_AgentExecution(t *testing.T) {
    if testing.Short() {
        t.Skip("Skipping integration test in short mode")
    }

    // Setup real database
    db, cleanup := setupTestDB(t)
    defer cleanup()

    // Create real services
    repos := repositories.New(db)
    svc := services.NewAgentService(repos)

    // Run integration test
    // ...
}
Run integration tests:
# Run all tests including integration
go test ./... -v

# Skip integration tests
go test ./... -short

API Tests

func TestAPI_CreateAgent(t *testing.T) {
    // Setup test server
    router := setupTestRouter()

    // Create request
    body := `{"name": "test-agent", "prompt": "You are helpful"}`
    req := httptest.NewRequest("POST", "/api/v1/agents", strings.NewReader(body))
    req.Header.Set("Content-Type", "application/json")
    req.Header.Set("Authorization", "Bearer test-token")

    // Execute
    w := httptest.NewRecorder()
    router.ServeHTTP(w, req)

    // Assert
    assert.Equal(t, http.StatusCreated, w.Code)

    var response map[string]interface{}
    err := json.Unmarshal(w.Body.Bytes(), &response)
    require.NoError(t, err)
    assert.Equal(t, "test-agent", response["name"])
}

Database Tests

func TestRepository_AgentCRUD(t *testing.T) {
    // Use in-memory SQLite for tests
    db, err := sql.Open("sqlite3", ":memory:")
    require.NoError(t, err)
    defer db.Close()

    // Run migrations
    _, err = db.Exec(schema)
    require.NoError(t, err)

    repo := NewAgentRepository(db)

    // Test Create
    agent := &Agent{Name: "test", Prompt: "Hello"}
    id, err := repo.Create(context.Background(), agent)
    require.NoError(t, err)
    assert.Greater(t, id, int64(0))

    // Test Read
    found, err := repo.GetByID(context.Background(), id)
    require.NoError(t, err)
    assert.Equal(t, "test", found.Name)

    // Test Update
    found.Name = "updated"
    err = repo.Update(context.Background(), found)
    require.NoError(t, err)

    // Test Delete
    err = repo.Delete(context.Background(), id)
    require.NoError(t, err)
}

Test Helpers

Test Database Setup

func setupTestDB(t *testing.T) (*sql.DB, func()) {
    t.Helper()

    // Create temp file
    f, err := os.CreateTemp("", "station-test-*.db")
    require.NoError(t, err)
    f.Close()

    // Open database
    db, err := sql.Open("sqlite3", f.Name())
    require.NoError(t, err)

    // Run migrations
    _, err = db.Exec(schema.SQL)
    require.NoError(t, err)

    cleanup := func() {
        db.Close()
        os.Remove(f.Name())
    }

    return db, cleanup
}

Test Fixtures

func createTestAgent(t *testing.T, repo AgentRepository) *Agent {
    t.Helper()

    agent := &Agent{
        Name:        "test-agent-" + randomString(8),
        Prompt:      "You are a test agent",
        MaxSteps:    5,
        Environment: "default",
    }

    id, err := repo.Create(context.Background(), agent)
    require.NoError(t, err)
    agent.ID = id

    return agent
}

Continuous Integration

Tests run automatically on:
  • Every push to main
  • Every pull request
  • Release tag creation
CI configuration runs:
# .github/workflows/test.yml
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: '1.21'
      - run: make test
      - run: make lint

Testing Best Practices

  1. Test behavior, not implementation - Focus on what the code does, not how
  2. Use table-driven tests - Makes adding test cases easy
  3. Mock at boundaries - Mock external services, not internal code
  4. Keep tests fast - Use in-memory databases, skip slow tests with -short
  5. Test error cases - Ensure errors are handled correctly
  6. Clean up resources - Use defer for cleanup
  7. Use meaningful names - Test names should describe the scenario

Next Steps