Skip to main content
Unlisted page
This page is unlisted. Search engines will not index it, and only users having a direct link can access it.

Testing Guide

Comprehensive testing strategies for SixDegree integrations and molecules.

Overview

SixDegree supports multiple testing approaches:

  1. Unit Tests - Test individual functions and components
  2. Integration Tests - Test molecule discovery and API integration
  3. End-to-End Tests - Test complete workflows including chat
  4. 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