Database mocking lets you test your Go applications without a live database. It speeds up testing, ensures consistent environments, and simplifies edge case simulation. Popular tools like go-sqlmock
and pgxmock
help simulate SQL queries, while Prisma Client Go ensures type safety.
go-sqlmock
, pgxmock
, or Prisma Client Go.
Mocking databases in Go helps you build reliable applications by testing in isolated and predictable environments. Ready to dive in? Let’s break it down step by step.
To get started with database mocking in Go, you'll need to install some dependencies and organize your code to make it easier to test.
First, install the key packages for database mocking in your Go project:
go get github.com/DATA-DOG/go-sqlmock github.com/prisma/prisma-client-go
The go-sqlmock
library is a powerful tool for simulating database interactions, while Prisma Client Go provides type-safe database access. Once installed, you're ready to set up your project.
Organize your project directory to keep things clear and maintainable:
myproject/
├── cmd/
│ └── main.go
├── internal/
│ ├── database/
│ │ ├── interfaces.go
│ │ └── mocks.go
│ └── models/
│ └── user.go
└── tests/
└── database_test.go
Initialize the project with the following commands:
go mod init myproject
go mod tidy
This setup ensures a clean structure, making it easier to manage your code and testing files.
To test database interactions, you'll need a schema. Here's an example schema you can use:
model User {
id Int @id @default(autoincrement())
email String @unique
name String
createdAt DateTime @default(now())
}
model Product {
id Int @id @default(autoincrement())
name String
price Float
inventory Int
lastUpdated DateTime @updatedAt
}
This schema includes two models: User
and Product
. It covers basic fields like IDs, timestamps, and unique constraints, which are useful for common testing scenarios. You'll use this schema to demonstrate various mocking techniques in your tests.
Let's dive into building mock interfaces for your database, taking advantage of Go's concurrency features and Prisma's type safety.
Start by defining an interface that represents your application's core database operations:
type Database interface {
CreateUser(ctx context.Context, user *models.User) error
GetUserByID(ctx context.Context, id int) (*models.User, error)
UpdateUser(ctx context.Context, user *models.User) error
DeleteUser(ctx context.Context, id int) error
// Product operations
CreateProduct(ctx context.Context, product *models.Product) error
GetProductByID(ctx context.Context, id int) (*models.Product, error)
UpdateProduct(ctx context.Context, product *models.Product) error
DeleteProduct(ctx context.Context, id int) error
}
This interface reflects the actual database methods, making it easy to switch between real and mock implementations during testing.
Here's how you can create a mock implementation:
type MockDatabase struct {
users map[int]*models.User
products map[int]*models.Product
mu sync.RWMutex // Ensures thread-safe operations
}
func NewMockDatabase() *MockDatabase {
return &MockDatabase{
users: make(map[int]*models.User),
products: make(map[int]*models.Product),
}
}
func (m *MockDatabase) CreateUser(ctx context.Context, user *models.User) error {
m.mu.Lock()
defer m.mu.Unlock()
// Simulate auto-increment ID
user.ID = len(m.users) + 1
m.users[user.ID] = user
return nil
}
This mock struct uses a sync.RWMutex
to ensure thread-safe operations, and it simulates database behaviors like auto-incrementing IDs.
For a type-safe mocking approach, you can use Prisma's Go client:
import (
"github.com/prisma/prisma-client-go/db"
"github.com/prisma/prisma-client-go/runtime/types"
)
type PrismaMockClient struct {
client *db.PrismaClient
mock *db.Mock
}
func NewPrismaMockClient() *PrismaMockClient {
client := db.NewClient()
mock := db.NewMock()
return &PrismaMockClient{
client: client,
mock: mock,
}
}
You can set up mocks for specific queries like this:
func (p *PrismaMockClient) SetupUserMock() {
p.mock.User.FindUnique(
db.User.ID.Equals(1),
).Returns(db.User{
ID: 1,
Email: "test@example.com",
Name: "Test User",
CreatedAt: types.DateTime(time.Now()),
})
}
This ensures your mock data stays consistent with your production schema, making it easier to test complex queries while maintaining accuracy.
Here's an example of structuring code using dependency injection in Go:
type UserService struct {
db Database
}
func NewUserService(db Database) *UserService {
return &UserService{db: db}
}
func (s *UserService) CreateUser(ctx context.Context, user *models.User) error {
if err := validateUser(user); err != nil {
return fmt.Errorf("invalid user data: %w", err)
}
return s.db.CreateUser(ctx, user)
}
This approach allows you to easily swap out the Database
implementation with a mock version when testing, making your code more flexible for different scenarios.
With a mock database in place, you can write focused tests for your service. Here's how that might look:
func TestUserService_CreateUser(t *testing.T) {
mockDB := NewMockDatabase()
service := NewUserService(mockDB)
testCases := []struct {
name string
user *models.User
wantErr bool
}{
{
name: "valid user",
user: &models.User{
Email: "test@example.com",
Name: "Test User",
},
wantErr: false,
},
{
name: "invalid user",
user: &models.User{
Email: "invalid-email",
},
wantErr: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctx := context.Background()
err := service.CreateUser(ctx, tc.user)
if tc.wantErr {
assert.Error(t, err)
return
}
assert.NoError(t, err)
storedUser, _ := mockDB.GetUserByID(ctx, tc.user.ID)
assert.Equal(t, tc.user.Email, storedUser.Email)
})
}
}
This test setup runs through multiple cases, checking both valid and invalid user scenarios. It ensures that errors are handled properly while confirming expected behavior for valid inputs.
You can also verify specific behaviors, such as validation errors, using targeted tests:
func TestUserService_CreateUser_ValidationError(t *testing.T) {
mockDB := NewMockDatabase()
service := NewUserService(mockDB)
invalidUser := &models.User{
Email: "invalid-email",
}
err := service.CreateUser(context.Background(), invalidUser)
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid user data")
}
This test ensures that invalid data triggers the correct error messages, reinforcing the reliability of your validation logic. By combining these patterns with tools like Prisma Client Go or custom mocks, you can thoroughly test your application's behavior in various scenarios.
When working with mock interfaces, it's crucial to ensure your test data stays consistent. Using Prisma Client Go's mocking features can help maintain type-safe, dependable data for your tests:
// In test setup
mock := db.NewMock()
mock.User.FindMany(db.User.Email.Contains("@example.com")).Returns([]db.User{
{ID: 1, Email: "test@example.com", Name: "Consistent User"},
})
To streamline this process, centralize your mock data setup by using schema-driven fixtures from Prisma. This ensures all tests share the same baseline data, making it easier to debug and ensuring reliable results.
Mocking more advanced database operations requires extra care. You can expand on the PrismaMockClient
example to handle more intricate query patterns. For instance:
func (p *PrismaMockClient) SetupComplexUserMock() {
p.mock.User.FindMany(
db.User.Email.Contains("@example.com"),
db.User.CreatedAt.Gt(time.Now().AddDate(0, -1, 0)),
).Returns([]db.User{
// Mock data matching complex query criteria
})
}
For complex queries, break them down into smaller, schema-aligned operations. This lets you mock specific behaviors more easily and ensures your tests align with expected outcomes.
Even with these methods, there are a few common mistakes to avoid:
Throughout this guide, we've explored how database mocking can speed up Go development by enabling isolated testing and ensuring type safety. Tools like Prisma Client Go and go-sqlmock have shown how to simplify this process.
Key advantages covered include:
Using a type-safe approach with Prisma Client Go, as highlighted in the examples, helps avoid common testing errors while maintaining thorough test coverage. Here are the key practices:
As shown in the examples, effective database mocking goes beyond simulating responses. It creates a solid foundation for building reliable and scalable applications while ensuring your testing environment remains maintainable.
Here are some common approaches for mocking databases in Go:
sync.RWMutex
to simulate basic CRUD operations.
If you're working with PostgreSQL, you can use pgxmock
while sticking to an interface-driven design. This keeps your code modular and test-friendly.
Prisma Client Go adds another layer of convenience with features like:
Tips for implementation: