Skip to main content
Glama
orneryd

M.I.M.I.R - Multi-agent Intelligent Memory & Insight Repository

by orneryd
testing-patterns.md21.9 kB
# Testing Patterns & Best Practices **Purpose**: Comprehensive guide to testing in NornicDB **Audience**: AI coding agents **Coverage Target**: 90%+ for all code --- ## Table of Contents 1. [Testing Philosophy](#testing-philosophy) 2. [Test Structure](#test-structure) 3. [Table-Driven Tests](#table-driven-tests) 4. [Bug Regression Tests](#bug-regression-tests) 5. [Integration Tests](#integration-tests) 6. [Benchmark Tests](#benchmark-tests) 7. [Race Condition Tests](#race-condition-tests) 8. [Mock & Stub Patterns](#mock--stub-patterns) 9. [Coverage Requirements](#coverage-requirements) 10. [Common Patterns from Codebase](#common-patterns-from-codebase) --- ## Testing Philosophy ### Test Pyramid ``` ┌──────────────┐ │ E2E Tests │ 5% - Full system integration │ (Slow) │ ├──────────────┤ │ Integration │ 15% - Multi-component │ Tests │ ├──────────────┤ │ Unit Tests │ 80% - Single function/method │ (Fast) │ └──────────────┘ ``` ### Key Principles 1. **Fast Feedback** - Unit tests run in milliseconds 2. **Isolated** - Tests don't depend on each other 3. **Repeatable** - Same input = same output 4. **Self-Validating** - Pass/fail is automatic 5. **Timely** - Written before or with code (TDD) --- ## Test Structure ### Standard Test Function ```go func TestFeatureName(t *testing.T) { // 1. ARRANGE - Setup test data and dependencies store := storage.NewMemoryEngine() exec := NewStorageExecutor(store) ctx := context.Background() // Create test data setupTestData(t, store) // 2. ACT - Execute the operation being tested result, err := exec.Execute(ctx, "MATCH (n:Person) RETURN n", nil) // 3. ASSERT - Verify expected outcomes require.NoError(t, err, "Query should execute without error") assert.Len(t, result.Rows, 3, "Should return 3 people") assert.Equal(t, "Alice", result.Rows[0][0].(string)) } ``` ### Subtests for Variations ```go func TestNodeValidation(t *testing.T) { t.Run("valid node", func(t *testing.T) { node := &Node{ID: "123", Labels: []string{"Person"}} err := ValidateNode(node) assert.NoError(t, err) }) t.Run("empty ID", func(t *testing.T) { node := &Node{ID: "", Labels: []string{"Person"}} err := ValidateNode(node) assert.Error(t, err) assert.Contains(t, err.Error(), "empty ID") }) t.Run("nil labels", func(t *testing.T) { node := &Node{ID: "123", Labels: nil} err := ValidateNode(node) assert.NoError(t, err, "Labels are optional") }) } ``` --- ## Table-Driven Tests **Use for testing multiple inputs/outputs:** ```go func TestTypeConversion(t *testing.T) { tests := []struct { name string input interface{} expected int64 wantErr bool }{ { name: "integer", input: 42, expected: 42, wantErr: false, }, { name: "float64", input: 42.0, expected: 42, wantErr: false, }, { name: "string number", input: "42", expected: 42, wantErr: false, }, { name: "invalid string", input: "not a number", expected: 0, wantErr: true, }, { name: "nil", input: nil, expected: 0, wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := toInt64(tt.input) if tt.wantErr { assert.Error(t, err) } else { assert.NoError(t, err) assert.Equal(t, tt.expected, result) } }) } } ``` ### Real Example from Codebase ```go // pkg/cypher/functions_test.go func TestStringFunctions(t *testing.T) { tests := []struct { name string query string expected interface{} }{ { name: "toLower", query: "RETURN toLower('HELLO') as result", expected: "hello", }, { name: "toUpper", query: "RETURN toUpper('hello') as result", expected: "HELLO", }, { name: "trim", query: "RETURN trim(' hello ') as result", expected: "hello", }, } store := storage.NewMemoryEngine() exec := NewStorageExecutor(store) ctx := context.Background() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result, err := exec.Execute(ctx, tt.query, nil) require.NoError(t, err) require.Len(t, result.Rows, 1) assert.Equal(t, tt.expected, result.Rows[0][0]) }) } } ``` --- ## Bug Regression Tests **MANDATORY for all bugs** - Prevent regressions forever. ### Pattern from Codebase ```go // pkg/cypher/aggregation_bugs_test.go // ==================================================================================== // BUG #1: WHERE ... IS NOT NULL combined with WITH aggregation returns empty results // ==================================================================================== func TestBug_WhereIsNotNullWithAggregation(t *testing.T) { store := storage.NewMemoryEngine() exec := NewStorageExecutor(store) ctx := context.Background() // Setup: Create test data that triggers the bug setupAggregationTestData(t, store) t.Run("WHERE IS NOT NULL before WITH aggregation", func(t *testing.T) { // This is the exact query that fails in production // Expected: returns 2 rows (.ts=2, .md=3) // Actual bug: returns 0 rows result, err := exec.Execute(ctx, ` MATCH (f:File) WHERE f.extension IS NOT NULL WITH f.extension as ext, COUNT(f) as count RETURN ext, count ORDER BY count DESC `, nil) require.NoError(t, err, "Query should execute without error") require.GreaterOrEqual(t, len(result.Rows), 2, "Should return at least 2 rows (one per extension)") // Verify exact counts extCounts := make(map[string]int64) for _, row := range result.Rows { if row[0] != nil { ext := row[0].(string) count := row[1].(int64) extCounts[ext] = count } } assert.Equal(t, int64(2), extCounts[".ts"], ".ts should have 2 files") assert.Equal(t, int64(3), extCounts[".md"], ".md should have 3 files") }) // Add variations to catch similar bugs t.Run("WHERE IS NOT NULL with ORDER BY", func(t *testing.T) { result, err := exec.Execute(ctx, ` MATCH (f:File) WHERE f.extension IS NOT NULL RETURN f.extension ORDER BY f.extension `, nil) require.NoError(t, err) assert.GreaterOrEqual(t, len(result.Rows), 5) }) t.Run("WHERE IS NOT NULL with LIMIT", func(t *testing.T) { result, err := exec.Execute(ctx, ` MATCH (f:File) WHERE f.extension IS NOT NULL RETURN f.extension LIMIT 3 `, nil) require.NoError(t, err) assert.Len(t, result.Rows, 3) }) } // Helper function to setup test data func setupAggregationTestData(t *testing.T, store *storage.MemoryEngine) { ctx := context.Background() exec := NewStorageExecutor(store) queries := []string{ `CREATE (f:File {id: 'file1', extension: '.ts'})`, `CREATE (f:File {id: 'file2', extension: '.ts'})`, `CREATE (f:File {id: 'file3', extension: '.md'})`, `CREATE (f:File {id: 'file4', extension: '.md'})`, `CREATE (f:File {id: 'file5', extension: '.md'})`, // Files without extension (to test IS NOT NULL filtering) `CREATE (f:File {id: 'file6', path: '/test/noext'})`, `CREATE (f:File {id: 'file7', path: '/test/noext2'})`, } for _, q := range queries { _, err := exec.Execute(ctx, q, nil) require.NoError(t, err) } } ``` ### Bug Test Template ```go // ==================================================================================== // BUG #N: [Brief description of the bug] // ==================================================================================== // Discovered: [Date] // Reporter: [Who found it] // Impact: [What breaks] // Root Cause: [Why it happened] // ==================================================================================== func TestBug_DescriptiveName(t *testing.T) { // Setup: Create minimal conditions that trigger bug store := storage.NewMemoryEngine() setupBugConditions(t, store) t.Run("exact reproduction", func(t *testing.T) { // Execute the exact operation that failed result, err := performBuggyOperation() // Assert expected behavior (this failed before fix) require.NoError(t, err) assert.Equal(t, expectedValue, result) }) t.Run("variation 1", func(t *testing.T) { // Test related scenarios }) t.Run("variation 2", func(t *testing.T) { // Test edge cases }) } ``` --- ## Integration Tests **Test multiple components working together:** ```go // pkg/bolt/integration_test.go func TestBoltServerIntegration(t *testing.T) { // Setup: Start real server with real storage store := storage.NewMemoryEngine() server := bolt.NewServer(store, ":7687") go server.Start() defer server.Stop() // Wait for server to be ready time.Sleep(100 * time.Millisecond) t.Run("connect and execute query", func(t *testing.T) { // Use real Neo4j driver driver, err := neo4j.NewDriver("bolt://localhost:7687", neo4j.NoAuth()) require.NoError(t, err) defer driver.Close() session := driver.NewSession(neo4j.SessionConfig{}) defer session.Close() // Execute real Cypher query result, err := session.Run( "CREATE (n:Person {name: $name}) RETURN n", map[string]interface{}{"name": "Alice"}, ) require.NoError(t, err) // Verify result record, err := result.Single() require.NoError(t, err) node := record.Values[0].(neo4j.Node) assert.Equal(t, "Alice", node.Props["name"]) }) t.Run("multiple concurrent connections", func(t *testing.T) { // Test connection pooling var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func(id int) { defer wg.Done() driver, _ := neo4j.NewDriver("bolt://localhost:7687", neo4j.NoAuth()) defer driver.Close() session := driver.NewSession(neo4j.SessionConfig{}) defer session.Close() _, err := session.Run( fmt.Sprintf("CREATE (n:Test {id: %d})", id), nil, ) assert.NoError(t, err) }(i) } wg.Wait() // Verify all nodes created session := driver.NewSession(neo4j.SessionConfig{}) result, _ := session.Run("MATCH (n:Test) RETURN count(n)", nil) record, _ := result.Single() assert.Equal(t, int64(10), record.Values[0]) }) } ``` --- ## Benchmark Tests **Measure performance and prevent regressions:** ```go // pkg/cypher/executor_test.go func BenchmarkQueryExecution(b *testing.B) { store := setupBenchmarkData() exec := NewStorageExecutor(store) ctx := context.Background() b.Run("simple MATCH", func(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { _, err := exec.Execute(ctx, "MATCH (n:Person) RETURN n LIMIT 100", nil) if err != nil { b.Fatal(err) } } }) b.Run("MATCH with WHERE", func(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { _, err := exec.Execute(ctx, "MATCH (n:Person) WHERE n.age > 25 RETURN n", nil) if err != nil { b.Fatal(err) } } }) b.Run("relationship traversal", func(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { _, err := exec.Execute(ctx, "MATCH (a:Person)-[r:KNOWS]->(b:Person) RETURN a, r, b", nil) if err != nil { b.Fatal(err) } } }) } // Setup benchmark data once func setupBenchmarkData() *storage.MemoryEngine { store := storage.NewMemoryEngine() exec := NewStorageExecutor(store) ctx := context.Background() // Create 1000 nodes for i := 0; i < 1000; i++ { exec.Execute(ctx, fmt.Sprintf( "CREATE (n:Person {id: %d, age: %d})", i, 20+i%50), nil) } // Create 5000 relationships for i := 0; i < 5000; i++ { exec.Execute(ctx, fmt.Sprintf( "MATCH (a:Person {id: %d}), (b:Person {id: %d}) "+ "CREATE (a)-[:KNOWS]->(b)", i%1000, (i+1)%1000), nil) } return store } ``` ### Benchmark Output Analysis ```bash $ go test -bench=. -benchmem BenchmarkQueryExecution/simple_MATCH-8 4252 ops/sec 2.3 MB/op BenchmarkQueryExecution/MATCH_with_WHERE-8 3891 ops/sec 2.1 MB/op BenchmarkQueryExecution/relationship_traversal-8 2769 ops/sec 3.4 MB/op ``` **What to look for:** - **ops/sec**: Higher is better - **MB/op**: Lower is better - **Regressions**: >5% slower = investigate --- ## Race Condition Tests **Detect concurrency bugs:** ```go func TestConcurrentAccess(t *testing.T) { store := storage.NewMemoryEngine() exec := NewStorageExecutor(store) ctx := context.Background() // Create initial data exec.Execute(ctx, "CREATE (n:Counter {value: 0})", nil) // Concurrent writes var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(1) go func() { defer wg.Done() // Increment counter _, err := exec.Execute(ctx, ` MATCH (n:Counter) SET n.value = n.value + 1 `, nil) assert.NoError(t, err) }() } wg.Wait() // Verify final value result, err := exec.Execute(ctx, "MATCH (n:Counter) RETURN n.value", nil) require.NoError(t, err) // Should be 100 if properly synchronized value := result.Rows[0][0].(int64) assert.Equal(t, int64(100), value, "Race condition detected: concurrent updates lost") } ``` **Run with race detector:** ```bash go test ./... -race ``` --- ## Mock & Stub Patterns ### Interface-Based Mocking ```go // Define interface for dependency type StorageEngine interface { GetNode(id NodeID) (*Node, error) CreateNode(node *Node) error } // Mock implementation for testing type MockStorage struct { nodes map[NodeID]*Node calls []string // Track method calls } func (m *MockStorage) GetNode(id NodeID) (*Node, error) { m.calls = append(m.calls, fmt.Sprintf("GetNode(%s)", id)) node, ok := m.nodes[id] if !ok { return nil, ErrNotFound } return node, nil } func (m *MockStorage) CreateNode(node *Node) error { m.calls = append(m.calls, fmt.Sprintf("CreateNode(%s)", node.ID)) if _, exists := m.nodes[node.ID]; exists { return ErrAlreadyExists } m.nodes[node.ID] = node return nil } // Use in tests func TestWithMock(t *testing.T) { mock := &MockStorage{ nodes: make(map[NodeID]*Node), } service := NewService(mock) // Test behavior err := service.DoSomething() assert.NoError(t, err) // Verify interactions assert.Contains(t, mock.calls, "GetNode(123)") assert.Len(t, mock.calls, 2, "Should call storage twice") } ``` ### Function Mocking ```go // Use function types for easy mocking type EmbeddingFunc func(text string) ([]float32, error) type SearchEngine struct { embedder EmbeddingFunc } // Test with mock function func TestSearchWithMockEmbeddings(t *testing.T) { mockEmbedder := func(text string) ([]float32, error) { // Return predictable embeddings for testing return []float32{0.1, 0.2, 0.3}, nil } engine := &SearchEngine{embedder: mockEmbedder} results, err := engine.Search("test query") assert.NoError(t, err) assert.Len(t, results, 3) } ``` --- ## Coverage Requirements ### Measuring Coverage ```bash # Generate coverage report go test ./... -coverprofile=coverage.out # View summary go tool cover -func=coverage.out # View HTML report go tool cover -html=coverage.out ``` ### Coverage Targets - **Core logic** (cypher, storage): **95%+** - **Utilities** (cache, pool): **90%+** - **Integration** (bolt, server): **85%+** - **UI/CLI**: **70%+** (focus on business logic) ### What to Test ✅ **Always test:** - Happy path (normal usage) - Error conditions (invalid input, not found) - Edge cases (empty, nil, boundary values) - Concurrency (race conditions) - All error returns - All branches (if/else, switch cases) ❌ **Don't waste time:** - Third-party library internals - Generated code (unless custom logic) - Trivial getters/setters (unless they have logic) - Private functions (test through public API) ### Example Coverage Report ``` github.com/orneryd/nornicdb/pkg/cypher/executor.go:45: Execute 95.2% github.com/orneryd/nornicdb/pkg/cypher/parser.go:23: Parse 92.8% github.com/orneryd/nornicdb/pkg/storage/memory.go:67: CreateNode 98.1% github.com/orneryd/nornicdb/pkg/storage/badger.go:89: GetNode 94.5% --------------------------------------------------------------- TOTAL 94.7% ``` --- ## Common Patterns from Codebase ### Pattern 1: Setup Helper Functions ```go // Reusable setup for multiple tests func setupTestExecutor(t *testing.T) (*StorageExecutor, *storage.MemoryEngine) { store := storage.NewMemoryEngine() exec := NewStorageExecutor(store) return exec, store } func setupTestData(t *testing.T, store *storage.MemoryEngine) { exec := NewStorageExecutor(store) ctx := context.Background() queries := []string{ `CREATE (a:Person {name: 'Alice', age: 30})`, `CREATE (b:Person {name: 'Bob', age: 25})`, `CREATE (a)-[:KNOWS]->(b)`, } for _, q := range queries { _, err := exec.Execute(ctx, q, nil) require.NoError(t, err) } } ``` ### Pattern 2: Assertion Helpers ```go // Helper to assert query results func assertQueryResult(t *testing.T, exec *Executor, query string, expectedRows int, expectedCols int) { result, err := exec.Execute(context.Background(), query, nil) require.NoError(t, err, "Query should execute without error") assert.Len(t, result.Rows, expectedRows, "Should return %d rows", expectedRows) if len(result.Rows) > 0 { assert.Len(t, result.Rows[0], expectedCols, "Should return %d columns", expectedCols) } } ``` ### Pattern 3: Error Testing ```go func TestErrorHandling(t *testing.T) { store := storage.NewMemoryEngine() exec := NewStorageExecutor(store) ctx := context.Background() t.Run("syntax error", func(t *testing.T) { _, err := exec.Execute(ctx, "INVALID QUERY", nil) assert.Error(t, err) assert.Contains(t, err.Error(), "syntax error") }) t.Run("node not found", func(t *testing.T) { _, err := exec.Execute(ctx, "MATCH (n:Person {id: 'nonexistent'}) RETURN n", nil) assert.NoError(t, err, "Query executes but returns empty") // Empty result is not an error in Cypher }) t.Run("type mismatch", func(t *testing.T) { exec.Execute(ctx, "CREATE (n:Test {value: 'string'})", nil) _, err := exec.Execute(ctx, "MATCH (n:Test) WHERE n.value > 10 RETURN n", nil) assert.Error(t, err) assert.Contains(t, err.Error(), "type mismatch") }) } ``` --- ## Quick Reference ### Test Checklist For every new feature or bug fix: - [ ] Unit tests for core logic - [ ] Table-driven tests for multiple inputs - [ ] Error path tests - [ ] Edge case tests (nil, empty, boundary) - [ ] Integration tests (if multi-component) - [ ] Benchmark tests (if performance-critical) - [ ] Race condition tests (`go test -race`) - [ ] Coverage ≥90% (`go tool cover`) - [ ] All tests pass (`go test ./... -v`) ### Common Test Commands ```bash # Run all tests go test ./... -v # Run specific package go test ./pkg/cypher -v # Run specific test go test ./pkg/cypher -run TestBug_WhereIsNotNull -v # Run with coverage go test ./... -coverprofile=coverage.out go tool cover -html=coverage.out # Run benchmarks go test ./pkg/cypher -bench=. -benchmem # Check for race conditions go test ./... -race # Run tests in parallel go test ./... -parallel 8 ``` --- **Remember**: Tests are documentation. Write them clearly so future developers (including AI agents) understand what the code does and why.

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/orneryd/Mimir'

If you have feedback or need assistance with the MCP directory API, please join our Discord server