Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
135 changes: 135 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,8 @@ if err != nil {

## Testing

### Running Tests

Run the test suite:

```bash
Expand All @@ -292,8 +294,141 @@ go test ./pkg/database -v

# MongoDB tests
go test ./pkg/database -run TestMongo

# Mock tests
go test ./pkg/database -run TestMockDatabase
```

### Mocking for Tests

The package includes a complete mock implementation of the `DatabaseInterface` that allows you to control the behavior of database operations in your tests without needing a real database connection.

#### Basic Mock Usage

```go
import (
"context"
"testing"
"github.com/uug-ai/database/pkg/database"
)

func TestMyFunction(t *testing.T) {
// Create a new mock database
mock := database.NewMockDatabase()

// Set up expectations for what the mock should return
expectedUser := map[string]any{
"id": 1,
"name": "Alice",
"email": "alice@example.com",
}
mock.ExpectFindOne(expectedUser, nil)

// Inject the mock into your Database instance
opts := database.NewMongoOptions().
SetUri("mongodb://localhost").
SetTimeout(5000).
Build()

db, err := database.New(opts, mock)
if err != nil {
t.Fatalf("failed to create database: %v", err)
}

// Use the database - it will use your mock
result, err := db.Client.FindOne(context.Background(), "testdb", "users", map[string]any{"id": 1})
if err != nil {
t.Errorf("unexpected error: %v", err)
}

// Verify the call was tracked
if len(mock.FindOneCalls) != 1 {
t.Errorf("expected 1 FindOne call, got %d", len(mock.FindOneCalls))
}
}
```

#### Advanced Mock Features

**Expect Multiple Results:**
```go
mock := database.NewMockDatabase()

// Mock Find to return multiple documents
users := []map[string]any{
{"id": 1, "name": "Alice"},
{"id": 2, "name": "Bob"},
}
mock.ExpectFind(users, nil)

result, err := mock.Find(ctx, "testdb", "users", map[string]any{})
// result contains the mocked users
```

**Expect Errors:**
```go
mock := database.NewMockDatabase()

// Mock a connection error
mock.ExpectPing(errors.New("connection failed"))

err := mock.Ping(ctx)
// err will be "connection failed"
```

**Custom Behavior:**
```go
mock := database.NewMockDatabase()

// Define custom logic based on input
mock.FindFunc = func(ctx context.Context, db string, collection string, filter any, opts ...any) (any, error) {
filterMap := filter.(map[string]any)

if filterMap["status"] == "active" {
return []map[string]any{{"id": 1, "status": "active"}}, nil
}

return []map[string]any{}, nil
}
```

**Track Call History:**
```go
mock := database.NewMockDatabase()

// Make some calls
mock.Find(ctx, "testdb", "users", map[string]any{})
mock.FindOne(ctx, "testdb", "users", map[string]any{"id": 1})

// Verify the calls
if len(mock.FindCalls) != 1 {
t.Error("expected 1 Find call")
}

if mock.FindCalls[0].Collection != "users" {
t.Error("expected collection to be 'users'")
}

// Reset call history for the next test
mock.Reset()
```

#### Mock API

The `MockDatabase` type provides:

- **`NewMockDatabase()`**: Creates a new mock with sensible defaults
- **`ExpectPing(err error)`**: Set expected Ping behavior
- **`ExpectFind(result any, err error)`**: Set expected Find behavior
- **`ExpectFindOne(result any, err error)`**: Set expected FindOne behavior
- **`PingFunc`**: Custom function for Ping behavior
- **`FindFunc`**: Custom function for Find behavior
- **`FindOneFunc`**: Custom function for FindOne behavior
- **`PingCalls`**: Slice of all Ping calls made
- **`FindCalls`**: Slice of all Find calls made
- **`FindOneCalls`**: Slice of all FindOne calls made
- **`Reset()`**: Clear all call history

## OpenTelemetry Integration

This package includes built-in OpenTelemetry instrumentation for MongoDB operations:
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.24.10

require (
github.com/go-playground/validator/v10 v10.30.1
github.com/uug-ai/models v1.2.26
go.mongodb.org/mongo-driver v1.17.6
go.opentelemetry.io/contrib/instrumentation/go.mongodb.org/mongo-driver/mongo/otelmongo v0.64.0
)
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/uug-ai/models v1.2.26 h1:gHqq/+HT7D9EXEUpgLJVWbfjC+CwYmRHBJoRsMZwJfI=
github.com/uug-ai/models v1.2.26/go.mod h1:0EHI6EKF/f2J1iXmFuPFuZZ2yv9Q6kphqcS8wzHYGd8=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
Expand Down
10 changes: 8 additions & 2 deletions pkg/database/database.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
package database

import "github.com/go-playground/validator/v10"
import (
"context"

"github.com/go-playground/validator/v10"
)

type DatabaseInterface interface {
Ping() error
Ping(context.Context) error
Find(ctx context.Context, db string, collection string, filter any, opts ...any) (any, error)
FindOne(ctx context.Context, db string, collection string, filter any, opts ...any) (any, error)
}

// Database represents a database client instance
Expand Down
207 changes: 207 additions & 0 deletions pkg/database/mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
package database

import (
"context"
"fmt"
)

// MockDatabase is a mock implementation of DatabaseInterface for testing
type MockDatabase struct {
// PingFunc allows customizing Ping behavior
PingFunc func(ctx context.Context) error

// FindFunc allows customizing Find behavior
FindFunc func(ctx context.Context, db string, collection string, filter any, opts ...any) (any, error)

// FindOneFunc allows customizing FindOne behavior
FindOneFunc func(ctx context.Context, db string, collection string, filter any, opts ...any) (any, error)

// Sequential response queues for multiple calls
PingQueue []PingResponse
FindQueue []FindResponse
FindOneQueue []FindOneResponse

// Call tracking
PingCalls []PingCall
FindCalls []FindCall
FindOneCalls []FindOneCall
}

// PingResponse represents a queued response for Ping
type PingResponse struct {
Err error
}

// FindResponse represents a queued response for Find
type FindResponse struct {
Result any
Err error
}

// FindOneResponse represents a queued response for FindOne
type FindOneResponse struct {
Result any
Err error
}

// PingCall records a call to Ping
type PingCall struct {
Ctx context.Context
}

// FindCall records a call to Find
type FindCall struct {
Ctx context.Context
Db string
Collection string
Filter any
Opts []any
}

// FindOneCall records a call to FindOne
type FindOneCall struct {
Ctx context.Context
Db string
Collection string
Filter any
Opts []any
}

// NewMockDatabase creates a new MockDatabase with sensible defaults
func NewMockDatabase() *MockDatabase {
return &MockDatabase{
PingFunc: func(ctx context.Context) error {
return nil
},
FindFunc: func(ctx context.Context, db string, collection string, filter any, opts ...any) (any, error) {
return []any{}, nil
},
FindOneFunc: func(ctx context.Context, db string, collection string, filter any, opts ...any) (any, error) {
return nil, fmt.Errorf("no document found")
},
PingCalls: []PingCall{},
FindCalls: []FindCall{},
FindOneCalls: []FindOneCall{},
PingQueue: []PingResponse{},
FindQueue: []FindResponse{},
FindOneQueue: []FindOneResponse{},
}
}

// Ping implements DatabaseInterface
func (m *MockDatabase) Ping(ctx context.Context) error {
m.PingCalls = append(m.PingCalls, PingCall{Ctx: ctx})

// Check if there's a queued response
if len(m.PingQueue) > 0 {
response := m.PingQueue[0]
m.PingQueue = m.PingQueue[1:]
return response.Err
}

// Fall back to PingFunc
if m.PingFunc != nil {
return m.PingFunc(ctx)
}
return nil
}

// Find implements DatabaseInterface
func (m *MockDatabase) Find(ctx context.Context, db string, collection string, filter any, opts ...any) (any, error) {
m.FindCalls = append(m.FindCalls, FindCall{
Ctx: ctx,
Db: db,
Collection: collection,
Filter: filter,
Opts: opts,
})

// Check if there's a queued response
if len(m.FindQueue) > 0 {
response := m.FindQueue[0]
m.FindQueue = m.FindQueue[1:]
return response.Result, response.Err
}

// Fall back to FindFunc
if m.FindFunc != nil {
return m.FindFunc(ctx, db, collection, filter, opts...)
}
return []any{}, nil
}

// FindOne implements DatabaseInterface
func (m *MockDatabase) FindOne(ctx context.Context, db string, collection string, filter any, opts ...any) (any, error) {
m.FindOneCalls = append(m.FindOneCalls, FindOneCall{
Ctx: ctx,
Db: db,
Collection: collection,
Filter: filter,
Opts: opts,
})

// Check if there's a queued response
if len(m.FindOneQueue) > 0 {
response := m.FindOneQueue[0]
m.FindOneQueue = m.FindOneQueue[1:]
return response.Result, response.Err
}

// Fall back to FindOneFunc
if m.FindOneFunc != nil {
return m.FindOneFunc(ctx, db, collection, filter, opts...)
}
return nil, fmt.Errorf("no document found")
}

// Reset clears all recorded calls
func (m *MockDatabase) Reset() {
m.PingCalls = []PingCall{}
m.FindCalls = []FindCall{}
m.FindOneCalls = []FindOneCall{}
m.PingQueue = []PingResponse{}
m.FindQueue = []FindResponse{}
m.FindOneQueue = []FindOneResponse{}
}

// ExpectPing sets up an expectation for Ping
func (m *MockDatabase) ExpectPing(err error) *MockDatabase {
m.PingFunc = func(ctx context.Context) error {
return err
}
return m
}

// ExpectFind sets up an expectation for Find
func (m *MockDatabase) ExpectFind(result any, err error) *MockDatabase {
m.FindFunc = func(ctx context.Context, db string, collection string, filter any, opts ...any) (any, error) {
return result, err
}
return m
}

// ExpectFindOne sets up an expectation for FindOne
func (m *MockDatabase) ExpectFindOne(result any, err error) *MockDatabase {
m.FindOneFunc = func(ctx context.Context, db string, collection string, filter any, opts ...any) (any, error) {
return result, err
}
return m
}

// QueuePing adds a Ping response to the queue for sequential calls
func (m *MockDatabase) QueuePing(err error) *MockDatabase {
m.PingQueue = append(m.PingQueue, PingResponse{Err: err})
return m
}

// QueueFind adds a Find response to the queue for sequential calls
func (m *MockDatabase) QueueFind(result any, err error) *MockDatabase {
m.FindQueue = append(m.FindQueue, FindResponse{Result: result, Err: err})
return m
}

// QueueFindOne adds a FindOne response to the queue for sequential calls
func (m *MockDatabase) QueueFindOne(result any, err error) *MockDatabase {
m.FindOneQueue = append(m.FindOneQueue, FindOneResponse{Result: result, Err: err})
return m
}
Loading
Loading