Testing Guide
Comprehensive testing strategies for SixDegree integrations and molecules.
Overview
SixDegree supports multiple testing approaches:
- Unit Tests - Test individual functions and components
- Integration Tests - Test molecule discovery and API integration
- End-to-End Tests - Test complete workflows including chat
- Mock Testing - Use Doppelganger to simulate external APIs
Unit Testing
Testing Molecule Logic
package main
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.uber.org/zap/zaptest"
)
func TestEntityConversion(t *testing.T) {
mol := &MyMolecule{}
// Test data
resource := &Resource{
ID: "123",
Name: "test-resource",
URL: "https://example.com",
}
// Convert to entity
entity := mol.convertToEntity(resource)
// Assertions
assert.Equal(t, "resource_123", entity.UID)
assert.Equal(t, "Resource", entity.Kind)
assert.Equal(t, "test-resource", entity.Name)
assert.Contains(t, entity.Properties, "url")
}
func TestConfigurationValidation(t *testing.T) {
mol := &MyMolecule{}
logger := zaptest.NewLogger(t)
tests := []struct {
name string
config map[string]interface{}
wantErr bool
}{
{
name: "valid config",
config: map[string]interface{}{
"api_key": "valid-key",
"base_url": "https://api.example.com",
},
wantErr: false,
},
{
name: "missing api_key",
config: map[string]interface{}{
"base_url": "https://api.example.com",
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
config := &molecule.MoleculeConfig{
Discovery: &molecule.CapabilitySettings{
Settings: tt.config,
},
}
err := mol.Initialize(context.Background(), config, logger)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
Testing Relationship Logic
func TestRelationshipCreation(t *testing.T) {
mol := &MyMolecule{}
repo := &Repository{ID: "repo-1", Name: "api-service"}
service := &Service{ID: "svc-1", RepoID: "repo-1"}
entities := mol.createEntitiesWithRelations(repo, service)
// Find repository entity
var repoEntity *molecule.Entity
for i := range entities {
if entities[i].Kind == "Repository" {
repoEntity = &entities[i]
break
}
}
require.NotNil(t, repoEntity)
// Check relationships
assert.Len(t, repoEntity.Relations, 1)
assert.Equal(t, "DEPLOYS_TO", repoEntity.Relations[0].Type)
assert.Equal(t, "service_svc-1", repoEntity.Relations[0].Target)
}
Testing MCP Tools
func TestMCPToolInvocation(t *testing.T) {
mol := &MyMolecule{}
mol.client = NewMockClient()
input := json.RawMessage(`{
"query": "test search",
"limit": 10
}`)
result, err := mol.handleSearch(context.Background(), input)
require.NoError(t, err)
resultMap, ok := result.(map[string]interface{})
require.True(t, ok)
assert.Contains(t, resultMap, "results")
assert.Contains(t, resultMap, "count")
}
func TestMCPToolValidation(t *testing.T) {
mol := &MyMolecule{}
// Invalid input - missing required field
invalidInput := json.RawMessage(`{
"limit": 10
}`)
_, err := mol.handleSearch(context.Background(), invalidInput)
assert.Error(t, err)
assert.Contains(t, err.Error(), "query")
}
Integration Testing
Testing with Mock HTTP Server
import (
"net/http"
"net/http/httptest"
)
func TestDiscoveryWithMockServer(t *testing.T) {
// Create mock server
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify request
assert.Equal(t, "/api/resources", r.URL.Path)
assert.Equal(t, "Bearer test-token", r.Header.Get("Authorization"))
// Mock response
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode([]Resource{
{ID: "1", Name: "Resource 1"},
{ID: "2", Name: "Resource 2"},
})
}))
defer mockServer.Close()
// Configure molecule with mock server
mol := &MyMolecule{}
config := &molecule.MoleculeConfig{
Namespace: "test",
Discovery: &molecule.CapabilitySettings{
Settings: map[string]interface{}{
"api_key": "test-token",
"base_url": mockServer.URL,
},
},
}
logger := zaptest.NewLogger(t)
err := mol.Initialize(context.Background(), config, logger)
require.NoError(t, err)
// Run discovery
result, err := mol.Discover(context.Background())
require.NoError(t, err)
// Verify results
assert.Len(t, result.Entities, 2)
assert.Equal(t, "resource_1", result.Entities[0].UID)
assert.Equal(t, "Resource 1", result.Entities[0].Name)
}
Testing Pagination
func TestDiscoveryWithPagination(t *testing.T) {
pageCount := 0
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
page := r.URL.Query().Get("page")
pageNum, _ := strconv.Atoi(page)
if pageNum == 1 {
// First page
json.NewEncoder(w).Encode(PagedResponse{
Data: []Resource{{ID: "1"}, {ID: "2"}},
Page: 1,
HasMore: true,
})
} else {
// Second page
json.NewEncoder(w).Encode(PagedResponse{
Data: []Resource{{ID: "3"}},
Page: 2,
HasMore: false,
})
}
pageCount++
}))
defer mockServer.Close()
// Test pagination logic
mol := setupMolecule(mockServer.URL)
result, err := mol.Discover(context.Background())
require.NoError(t, err)
assert.Equal(t, 2, pageCount) // Verify two requests made
assert.Len(t, result.Entities, 3) // Total entities from both pages
}
Testing Error Handling
func TestDiscoveryErrorHandling(t *testing.T) {
tests := []struct {
name string
statusCode int
response string
wantErr string
}{
{
name: "unauthorized",
statusCode: 401,
response: `{"error": "unauthorized"}`,
wantErr: "unauthorized",
},
{
name: "rate limit",
statusCode: 429,
response: `{"error": "rate limit exceeded"}`,
wantErr: "rate limit",
},
{
name: "server error",
statusCode: 500,
response: `{"error": "internal server error"}`,
wantErr: "internal server error",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(tt.statusCode)
w.Write([]byte(tt.response))
}))
defer mockServer.Close()
mol := setupMolecule(mockServer.URL)
_, err := mol.Discover(context.Background())
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
})
}
}
Testing with Doppelganger
Doppelganger is a mock API server that simulates external service APIs for testing.
Starting Doppelganger
cd doppelganger
go run main.go
# Server starts on http://localhost:9090
Configuring Test Scenarios
Create test fixtures in doppelganger/fixtures/:
# fixtures/github.yaml
name: GitHub API Mock
base_path: /github
scenarios:
- name: List repositories
request:
method: GET
path: /orgs/test-org/repos
response:
status: 200
headers:
Content-Type: application/json
body: |
[
{
"id": 1,
"name": "repo-1",
"full_name": "test-org/repo-1",
"private": false
},
{
"id": 2,
"name": "repo-2",
"full_name": "test-org/repo-2",
"private": true
}
]
- name: Get repository
request:
method: GET
path: /repos/test-org/repo-1
response:
status: 200
body: |
{
"id": 1,
"name": "repo-1",
"full_name": "test-org/repo-1"
}
- name: Unauthorized
request:
method: GET
path: /orgs/unauthorized/repos
response:
status: 401
body: |
{
"message": "Bad credentials"
}
Using Doppelganger in Tests
func TestWithDoppelganger(t *testing.T) {
// Doppelganger should be running on localhost:9090
mol := &GitHubMolecule{}
config := &molecule.MoleculeConfig{
Discovery: &molecule.CapabilitySettings{
Settings: map[string]interface{}{
"token": "mock-token",
"api_url": "http://localhost:9090/github",
"selectors": []interface{}{
map[string]interface{}{
"organization": "test-org",
},
},
},
},
}
logger := zaptest.NewLogger(t)
err := mol.Initialize(context.Background(), config, logger)
require.NoError(t, err)
result, err := mol.Discover(context.Background())
require.NoError(t, err)
assert.Len(t, result.Entities, 2)
assert.Equal(t, "repo_1", result.Entities[0].UID)
}
End-to-End Testing
Testing Discovery Pipeline
func TestFullDiscoveryPipeline(t *testing.T) {
// Setup
db := setupTestDatabase(t)
defer cleanupDatabase(t, db)
platform := startTestPlatform(t, db)
defer platform.Stop()
mol := startTestMolecule(t)
defer mol.Stop()
// Create environment
env := createTestEnvironment(t, platform)
// Configure molecule
config := createMoleculeConfig(t, mol.URL())
platform.ConfigureMolecule(t, env.ID, config)
// Run discovery
runID := platform.RunDiscovery(t, env.ID, "test-molecule")
// Wait for completion
status := waitForDiscovery(t, platform, runID, 30*time.Second)
assert.Equal(t, "completed", status.Status)
// Verify entities were created
entities := platform.ListEntities(t, env.ID)
assert.NotEmpty(t, entities)
// Verify relationships
for _, entity := range entities {
if len(entity.Relations) > 0 {
relations := platform.GetEntityRelations(t, env.ID, entity.UID)
assert.NotEmpty(t, relations)
}
}
}
Testing Chat Integration
func TestChatWithMCP(t *testing.T) {
platform := startTestPlatform(t, setupTestDatabase(t))
defer platform.Stop()
// Create environment with entities
env := createTestEnvironment(t, platform)
seedTestEntities(t, platform, env.ID)
// Configure MCP molecule
mol := startTestMolecule(t)
platform.ConfigureMCP(t, env.ID, mol.URL())
// Create chat session
chat := platform.CreateChat(t, env.ID)
// Send message that triggers MCP tool
response := platform.SendChatMessage(t, chat.ID, "Search for resources matching 'api'")
// Verify tool was called
assert.Contains(t, response.ToolCalls, "search_resources")
// Verify response contains results
assert.Contains(t, response.Content, "api")
}
Performance Testing
Benchmarking Discovery
func BenchmarkDiscovery(b *testing.B) {
mol := setupMolecule(b)
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, err := mol.Discover(context.Background())
if err != nil {
b.Fatal(err)
}
}
}
func BenchmarkEntityConversion(b *testing.B) {
mol := &MyMolecule{}
resources := generateTestResources(1000)
b.ResetTimer()
for i := 0; i < b.N; i++ {
for _, r := range resources {
_ = mol.convertToEntity(r)
}
}
}
Load Testing MCP Tools
func TestMCPToolConcurrency(t *testing.T) {
mol := setupMolecule(t)
numGoroutines := 10
numRequests := 100
var wg sync.WaitGroup
errChan := make(chan error, numGoroutines*numRequests)
for g := 0; g < numGoroutines; g++ {
wg.Add(1)
go func() {
defer wg.Done()
for r := 0; r < numRequests; r++ {
input := json.RawMessage(`{"query": "test"}`)
_, err := mol.handleSearch(context.Background(), input)
if err != nil {
errChan <- err
}
}
}()
}
wg.Wait()
close(errChan)
// Check for errors
var errors []error
for err := range errChan {
errors = append(errors, err)
}
assert.Empty(t, errors, "Expected no errors during concurrent execution")
}
Testing Best Practices
1. Use Table-Driven Tests
func TestInputValidation(t *testing.T) {
tests := []struct {
name string
input string
wantErr bool
errMsg string
}{
{
name: "valid input",
input: `{"name": "test"}`,
wantErr: false,
},
{
name: "missing required field",
input: `{}`,
wantErr: true,
errMsg: "name is required",
},
{
name: "invalid json",
input: `{invalid}`,
wantErr: true,
errMsg: "invalid json",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateInput(tt.input)
if tt.wantErr {
require.Error(t, err)
assert.Contains(t, err.Error(), tt.errMsg)
} else {
assert.NoError(t, err)
}
})
}
}
2. Use Test Fixtures
// testdata/fixtures.go
package testdata
var ValidRepository = &Repository{
ID: "123",
Name: "test-repo",
URL: "https://github.com/org/test-repo",
}
var RepositoryWithRelations = &Repository{
ID: "123",
Name: "api-service",
Dependencies: []string{"lib-1", "lib-2"},
}
func LoadFixture(t *testing.T, filename string) []byte {
data, err := os.ReadFile(filepath.Join("testdata", filename))
require.NoError(t, err)
return data
}
3. Clean Up Resources
func TestWithCleanup(t *testing.T) {
server := httptest.NewServer(handler)
t.Cleanup(server.Close) // Automatic cleanup
tempDir := t.TempDir() // Automatically removed after test
// Test code...
}
4. Use Subtests for Organization
func TestMolecule(t *testing.T) {
mol := setupMolecule(t)
t.Run("Discovery", func(t *testing.T) {
t.Run("ValidConfig", func(t *testing.T) {
// Test valid configuration
})
t.Run("InvalidConfig", func(t *testing.T) {
// Test error handling
})
})
t.Run("MCPTools", func(t *testing.T) {
t.Run("SearchTool", func(t *testing.T) {
// Test search tool
})
t.Run("CreateTool", func(t *testing.T) {
// Test create tool
})
})
}
CI/CD Integration
GitHub Actions Workflow
name: Test
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
services:
# Database service configuration
# Contact maintainers for test database setup
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.23'
- name: Cache Go modules
uses: actions/cache@v4
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
- name: Install dependencies
run: go mod download
- name: Run tests
env:
DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}
run: go test -v -race -coverprofile=coverage.out ./...
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
file: ./coverage.out
Additional Resources
- Development Setup - Set up local environment
- Building Molecules - Molecule development guide
- Go SDK - SDK documentation