885 lines
22 KiB
Markdown
885 lines
22 KiB
Markdown
# Testing Patterns Guide
|
|
|
|
Complete guide to testing applications that use cool-mysql, including mocking strategies and test patterns.
|
|
|
|
## Table of Contents
|
|
|
|
1. [Testing Strategies](#testing-strategies)
|
|
2. [Using sqlmock](#using-sqlmock)
|
|
3. [Test Database Setup](#test-database-setup)
|
|
4. [Testing Patterns](#testing-patterns)
|
|
5. [Context-Based Testing](#context-based-testing)
|
|
6. [Testing Caching](#testing-caching)
|
|
7. [Integration Testing](#integration-testing)
|
|
8. [Best Practices](#best-practices)
|
|
|
|
## Testing Strategies
|
|
|
|
### Three Approaches
|
|
|
|
| Approach | Pros | Cons | Best For |
|
|
|----------|------|------|----------|
|
|
| **sqlmock** | Fast, no DB needed, precise control | Manual setup, brittle | Unit tests |
|
|
| **Test Database** | Real MySQL behavior | Slower, requires DB | Integration tests |
|
|
| **In-Memory DB** | Fast, real SQL | Limited MySQL features | Quick tests |
|
|
|
|
### When to Use Each
|
|
|
|
**sqlmock:**
|
|
- Unit testing business logic
|
|
- Testing error handling
|
|
- CI/CD pipelines without database
|
|
- Rapid iteration
|
|
|
|
**Test Database:**
|
|
- Integration testing
|
|
- Testing complex queries
|
|
- Verifying MySQL-specific behavior
|
|
- End-to-end tests
|
|
|
|
**In-Memory (SQLite):**
|
|
- Quick local tests
|
|
- Testing SQL logic (not MySQL-specific)
|
|
- Prototyping
|
|
|
|
## Using sqlmock
|
|
|
|
### Setup
|
|
|
|
```go
|
|
import (
|
|
"testing"
|
|
"github.com/DATA-DOG/go-sqlmock"
|
|
mysqlDriver "github.com/go-sql-driver/mysql"
|
|
"github.com/StirlingMarketingGroup/cool-mysql"
|
|
)
|
|
|
|
func setupMockDB(t *testing.T) (*mysql.Database, sqlmock.Sqlmock) {
|
|
// Create mock SQL connection
|
|
mockDB, mock, err := sqlmock.New()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create mock: %v", err)
|
|
}
|
|
|
|
// Create cool-mysql Database from mock connection
|
|
db, err := mysql.NewFromConn(mockDB, mockDB)
|
|
if err != nil {
|
|
t.Fatalf("Failed to create db: %v", err)
|
|
}
|
|
|
|
return db, mock
|
|
}
|
|
```
|
|
|
|
### Basic Select Test
|
|
|
|
```go
|
|
func TestSelectUsers(t *testing.T) {
|
|
db, mock := setupMockDB(t)
|
|
defer mock.ExpectClose()
|
|
|
|
// Define expected query and result
|
|
rows := sqlmock.NewRows([]string{"id", "name", "email"}).
|
|
AddRow(1, "Alice", "alice@example.com").
|
|
AddRow(2, "Bob", "bob@example.com")
|
|
|
|
mock.ExpectQuery("SELECT (.+) FROM `users` WHERE age > ?").
|
|
WithArgs(18).
|
|
WillReturnRows(rows)
|
|
|
|
// Execute query
|
|
var users []User
|
|
err := db.Select(&users,
|
|
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge",
|
|
0,
|
|
18)
|
|
|
|
// Verify
|
|
if err != nil {
|
|
t.Errorf("Select failed: %v", err)
|
|
}
|
|
|
|
if len(users) != 2 {
|
|
t.Errorf("Expected 2 users, got %d", len(users))
|
|
}
|
|
|
|
if users[0].Name != "Alice" {
|
|
t.Errorf("Expected Alice, got %s", users[0].Name)
|
|
}
|
|
|
|
// Verify all expectations met
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("Unfulfilled expectations: %v", err)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Insert Test
|
|
|
|
```go
|
|
func TestInsertUser(t *testing.T) {
|
|
db, mock := setupMockDB(t)
|
|
defer mock.ExpectClose()
|
|
|
|
user := User{
|
|
ID: 1,
|
|
Name: "Alice",
|
|
Email: "alice@example.com",
|
|
}
|
|
|
|
// Expect INSERT statement
|
|
mock.ExpectExec("INSERT INTO `users`").
|
|
WithArgs(1, "Alice", "alice@example.com").
|
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
|
|
|
// Execute insert
|
|
err := db.Insert("users", user)
|
|
|
|
// Verify
|
|
if err != nil {
|
|
t.Errorf("Insert failed: %v", err)
|
|
}
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("Unfulfilled expectations: %v", err)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Update Test
|
|
|
|
```go
|
|
func TestUpdateUser(t *testing.T) {
|
|
db, mock := setupMockDB(t)
|
|
defer mock.ExpectClose()
|
|
|
|
// Expect UPDATE statement
|
|
mock.ExpectExec("UPDATE `users` SET `name` = \\? WHERE `id` = \\?").
|
|
WithArgs("Alice Updated", 1).
|
|
WillReturnResult(sqlmock.NewResult(0, 1))
|
|
|
|
// Execute update
|
|
err := db.Exec("UPDATE `users` SET `name` = @@name WHERE `id` = @@id",
|
|
mysql.Params{"name": "Alice Updated", "id": 1})
|
|
|
|
// Verify
|
|
if err != nil {
|
|
t.Errorf("Update failed: %v", err)
|
|
}
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("Unfulfilled expectations: %v", err)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Error Handling Test
|
|
|
|
```go
|
|
func TestSelectError(t *testing.T) {
|
|
db, mock := setupMockDB(t)
|
|
defer mock.ExpectClose()
|
|
|
|
// Expect query to return error
|
|
mock.ExpectQuery("SELECT (.+) FROM `users`").
|
|
WillReturnError(sql.ErrNoRows)
|
|
|
|
// Execute query
|
|
var user User
|
|
err := db.Select(&user, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `id` = @@id", 0,
|
|
999)
|
|
|
|
// Verify error returned
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
t.Errorf("Expected sql.ErrNoRows, got %v", err)
|
|
}
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("Unfulfilled expectations: %v", err)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Transaction Test
|
|
|
|
```go
|
|
func TestTransaction(t *testing.T) {
|
|
db, mock := setupMockDB(t)
|
|
defer mock.ExpectClose()
|
|
|
|
// Expect transaction
|
|
mock.ExpectBegin()
|
|
mock.ExpectExec("INSERT INTO `users`").
|
|
WithArgs(1, "Alice", "alice@example.com").
|
|
WillReturnResult(sqlmock.NewResult(1, 1))
|
|
mock.ExpectCommit()
|
|
|
|
// Execute transaction
|
|
ctx := context.Background()
|
|
tx, commit, cancel, err := mysql.GetOrCreateTxFromContext(ctx)
|
|
defer cancel()
|
|
if err != nil {
|
|
t.Fatalf("Failed to create tx: %v", err)
|
|
}
|
|
|
|
// Store transaction in context so operations use it
|
|
ctx = mysql.NewContextWithTx(ctx, tx)
|
|
|
|
user := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
|
|
err = db.Insert("users", user)
|
|
if err != nil {
|
|
t.Errorf("Insert failed: %v", err)
|
|
}
|
|
|
|
if err := commit(); err != nil {
|
|
t.Errorf("Commit failed: %v", err)
|
|
}
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("Unfulfilled expectations: %v", err)
|
|
}
|
|
}
|
|
```
|
|
|
|
## Test Database Setup
|
|
|
|
### Docker Compose Setup
|
|
|
|
```yaml
|
|
# docker-compose.test.yml
|
|
version: '3.8'
|
|
services:
|
|
mysql-test:
|
|
image: mysql:8.0
|
|
environment:
|
|
MYSQL_ROOT_PASSWORD: testpass
|
|
MYSQL_DATABASE: testdb
|
|
ports:
|
|
- "3307:3306"
|
|
tmpfs:
|
|
- /var/lib/mysql # In-memory for speed
|
|
```
|
|
|
|
### Test Helper
|
|
|
|
```go
|
|
// testutil/db.go
|
|
package testutil
|
|
|
|
import (
|
|
"database/sql"
|
|
"testing"
|
|
"github.com/StirlingMarketingGroup/cool-mysql"
|
|
)
|
|
|
|
func SetupTestDB(t *testing.T) *mysql.Database {
|
|
db, err := mysql.New(
|
|
"root", "testpass", "testdb", "localhost", 3307,
|
|
"root", "testpass", "testdb", "localhost", 3307,
|
|
"utf8mb4_unicode_ci",
|
|
"UTC",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("Failed to connect to test DB: %v", err)
|
|
}
|
|
|
|
// Clean database before test
|
|
cleanDB(t, db)
|
|
|
|
// Setup schema
|
|
setupSchema(t, db)
|
|
|
|
return db
|
|
}
|
|
|
|
func cleanDB(t *testing.T, db *mysql.Database) {
|
|
tables := []string{"users", "orders", "products"}
|
|
for _, table := range tables {
|
|
db.Exec("DROP TABLE IF EXISTS " + table)
|
|
}
|
|
}
|
|
|
|
func setupSchema(t *testing.T, db *mysql.Database) {
|
|
schema := `
|
|
CREATE TABLE users (
|
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
|
name VARCHAR(255) NOT NULL,
|
|
email VARCHAR(255) UNIQUE NOT NULL,
|
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`
|
|
if err := db.Exec(schema); err != nil {
|
|
t.Fatalf("Failed to create schema: %v", err)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Using Test Database
|
|
|
|
```go
|
|
func TestInsertUserIntegration(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
|
|
user := User{
|
|
Name: "Alice",
|
|
Email: "alice@example.com",
|
|
}
|
|
|
|
// Insert user
|
|
err := db.Insert("users", user)
|
|
if err != nil {
|
|
t.Fatalf("Insert failed: %v", err)
|
|
}
|
|
|
|
// Verify insertion
|
|
var retrieved User
|
|
err = db.Select(&retrieved,
|
|
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `email` = @@email",
|
|
0,
|
|
"alice@example.com")
|
|
|
|
if err != nil {
|
|
t.Fatalf("Select failed: %v", err)
|
|
}
|
|
|
|
if retrieved.Name != "Alice" {
|
|
t.Errorf("Expected Alice, got %s", retrieved.Name)
|
|
}
|
|
}
|
|
```
|
|
|
|
## Testing Patterns
|
|
|
|
### Table-Driven Tests
|
|
|
|
```go
|
|
func TestSelectUsers(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
minAge int
|
|
expected []User
|
|
expectError bool
|
|
}{
|
|
{
|
|
name: "adults only",
|
|
minAge: 18,
|
|
expected: []User{
|
|
{ID: 1, Name: "Alice", Age: 25},
|
|
{ID: 2, Name: "Bob", Age: 30},
|
|
},
|
|
expectError: false,
|
|
},
|
|
{
|
|
name: "no results",
|
|
minAge: 100,
|
|
expected: []User{},
|
|
expectError: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
db, mock := setupMockDB(t)
|
|
defer mock.ExpectClose()
|
|
|
|
rows := sqlmock.NewRows([]string{"id", "name", "age"})
|
|
for _, u := range tt.expected {
|
|
rows.AddRow(u.ID, u.Name, u.Age)
|
|
}
|
|
|
|
mock.ExpectQuery("SELECT (.+) FROM `users`").
|
|
WithArgs(tt.minAge).
|
|
WillReturnRows(rows)
|
|
|
|
var users []User
|
|
err := db.Select(&users,
|
|
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge",
|
|
0,
|
|
tt.minAge)
|
|
|
|
if tt.expectError && err == nil {
|
|
t.Error("Expected error, got nil")
|
|
}
|
|
|
|
if !tt.expectError && err != nil {
|
|
t.Errorf("Unexpected error: %v", err)
|
|
}
|
|
|
|
if len(users) != len(tt.expected) {
|
|
t.Errorf("Expected %d users, got %d",
|
|
len(tt.expected), len(users))
|
|
}
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("Unfulfilled expectations: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
```
|
|
|
|
### Testing Named Parameters
|
|
|
|
```go
|
|
func TestNamedParameters(t *testing.T) {
|
|
db, mock := setupMockDB(t)
|
|
defer mock.ExpectClose()
|
|
|
|
// cool-mysql converts @@param to ? internally
|
|
mock.ExpectQuery("SELECT (.+) FROM `users` WHERE age > \\? AND `status` = \\?").
|
|
WithArgs(18, "active").
|
|
WillReturnRows(sqlmock.NewRows([]string{"id", "name"}))
|
|
|
|
var users []User
|
|
err := db.Select(&users,
|
|
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge AND `status` = @@status",
|
|
0,
|
|
mysql.Params{"minAge": 18, "status": "active"})
|
|
|
|
if err != nil {
|
|
t.Errorf("Query failed: %v", err)
|
|
}
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("Unfulfilled expectations: %v", err)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Testing Struct Tags
|
|
|
|
```go
|
|
func TestStructTagMapping(t *testing.T) {
|
|
db, mock := setupMockDB(t)
|
|
defer mock.ExpectClose()
|
|
|
|
type CustomUser struct {
|
|
UserID int `mysql:"id"`
|
|
Name string `mysql:"user_name"`
|
|
}
|
|
|
|
// Expect query with actual column names
|
|
rows := sqlmock.NewRows([]string{"id", "user_name"}).
|
|
AddRow(1, "Alice")
|
|
|
|
mock.ExpectQuery("SELECT `id`, user_name FROM `users`").
|
|
WillReturnRows(rows)
|
|
|
|
var users []CustomUser
|
|
err := db.Select(&users, "SELECT `id`, user_name FROM `users`", 0)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Query failed: %v", err)
|
|
}
|
|
|
|
if users[0].UserID != 1 {
|
|
t.Errorf("Expected UserID=1, got %d", users[0].UserID)
|
|
}
|
|
|
|
if users[0].Name != "Alice" {
|
|
t.Errorf("Expected Name=Alice, got %s", users[0].Name)
|
|
}
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("Unfulfilled expectations: %v", err)
|
|
}
|
|
}
|
|
```
|
|
|
|
## Context-Based Testing
|
|
|
|
### Testing with Context
|
|
|
|
```go
|
|
func TestSelectWithContext(t *testing.T) {
|
|
db, mock := setupMockDB(t)
|
|
defer mock.ExpectClose()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
|
|
rows := sqlmock.NewRows([]string{"id", "name"}).
|
|
AddRow(1, "Alice")
|
|
|
|
mock.ExpectQuery("SELECT (.+) FROM `users`").
|
|
WillReturnRows(rows)
|
|
|
|
var users []User
|
|
err := db.SelectContext(ctx, &users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
|
|
|
|
if err != nil {
|
|
t.Errorf("Query failed: %v", err)
|
|
}
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("Unfulfilled expectations: %v", err)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Testing Context Cancellation
|
|
|
|
```go
|
|
func TestContextCancellation(t *testing.T) {
|
|
db, mock := setupMockDB(t)
|
|
defer mock.ExpectClose()
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel() // Cancel immediately
|
|
|
|
mock.ExpectQuery("SELECT (.+) FROM `users`").
|
|
WillDelayFor(100 * time.Millisecond)
|
|
|
|
var users []User
|
|
err := db.SelectContext(ctx, &users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
|
|
|
|
if err == nil {
|
|
t.Error("Expected context cancellation error")
|
|
}
|
|
}
|
|
```
|
|
|
|
### Testing Database in Context
|
|
|
|
```go
|
|
func TestDatabaseInContext(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
|
|
// Store DB in context
|
|
ctx := mysql.NewContext(context.Background(), db)
|
|
|
|
// Retrieve DB from context
|
|
retrievedDB := mysql.FromContext(ctx)
|
|
|
|
if retrievedDB == nil {
|
|
t.Error("Expected database in context")
|
|
}
|
|
|
|
// Use DB from context
|
|
var users []User
|
|
err := retrievedDB.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
|
|
|
|
if err != nil {
|
|
t.Errorf("Query failed: %v", err)
|
|
}
|
|
}
|
|
```
|
|
|
|
## Testing Caching
|
|
|
|
### Testing Cache Hits
|
|
|
|
```go
|
|
func TestCacheHit(t *testing.T) {
|
|
db, mock := setupMockDB(t)
|
|
defer mock.ExpectClose()
|
|
|
|
// Enable weak cache for testing
|
|
db.UseCache(mysql.NewWeakCache())
|
|
|
|
rows := sqlmock.NewRows([]string{"id", "name"}).
|
|
AddRow(1, "Alice")
|
|
|
|
// First query - cache miss
|
|
mock.ExpectQuery("SELECT (.+) FROM `users`").
|
|
WillReturnRows(rows)
|
|
|
|
var users1 []User
|
|
err := db.Select(&users1, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 5*time.Minute)
|
|
if err != nil {
|
|
t.Fatalf("First query failed: %v", err)
|
|
}
|
|
|
|
// Second query - cache hit (no DB query expected)
|
|
var users2 []User
|
|
err = db.Select(&users2, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 5*time.Minute)
|
|
if err != nil {
|
|
t.Fatalf("Second query failed: %v", err)
|
|
}
|
|
|
|
// Verify same results
|
|
if len(users1) != len(users2) {
|
|
t.Error("Cache returned different results")
|
|
}
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("Unfulfilled expectations: %v", err)
|
|
}
|
|
}
|
|
```
|
|
|
|
### Testing Cache Bypass
|
|
|
|
```go
|
|
func TestCacheBypass(t *testing.T) {
|
|
db, mock := setupMockDB(t)
|
|
defer mock.ExpectClose()
|
|
|
|
db.UseCache(mysql.NewWeakCache())
|
|
|
|
rows := sqlmock.NewRows([]string{"id", "name"}).
|
|
AddRow(1, "Alice")
|
|
|
|
// Each query with TTL=0 should hit database
|
|
mock.ExpectQuery("SELECT (.+) FROM `users`").
|
|
WillReturnRows(rows)
|
|
mock.ExpectQuery("SELECT (.+) FROM `users`").
|
|
WillReturnRows(rows)
|
|
|
|
var users []User
|
|
db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0) // TTL=0, no cache
|
|
db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0) // TTL=0, no cache
|
|
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("Expected 2 queries, got different: %v", err)
|
|
}
|
|
}
|
|
```
|
|
|
|
## Integration Testing
|
|
|
|
### End-to-End Test
|
|
|
|
```go
|
|
func TestUserWorkflow(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
|
|
// Create user
|
|
user := User{
|
|
Name: "Alice",
|
|
Email: "alice@example.com",
|
|
}
|
|
|
|
err := db.Insert("users", user)
|
|
if err != nil {
|
|
t.Fatalf("Insert failed: %v", err)
|
|
}
|
|
|
|
// Query user
|
|
var retrieved User
|
|
err = db.Select(&retrieved,
|
|
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `email` = @@email",
|
|
0,
|
|
"alice@example.com")
|
|
if err != nil {
|
|
t.Fatalf("Select failed: %v", err)
|
|
}
|
|
|
|
// Update user
|
|
err = db.Exec("UPDATE `users` SET `name` = @@name WHERE `email` = @@email",
|
|
mysql.Params{"name": "Alice Updated", "email": "alice@example.com"})
|
|
if err != nil {
|
|
t.Fatalf("Update failed: %v", err)
|
|
}
|
|
|
|
// Verify update
|
|
err = db.Select(&retrieved,
|
|
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `email` = @@email",
|
|
0,
|
|
"alice@example.com")
|
|
if err != nil {
|
|
t.Fatalf("Select after update failed: %v", err)
|
|
}
|
|
|
|
if retrieved.Name != "Alice Updated" {
|
|
t.Errorf("Expected 'Alice Updated', got '%s'", retrieved.Name)
|
|
}
|
|
|
|
// Delete user
|
|
err = db.Exec("DELETE FROM `users` WHERE `email` = @@email",
|
|
"alice@example.com")
|
|
if err != nil {
|
|
t.Fatalf("Delete failed: %v", err)
|
|
}
|
|
|
|
// Verify deletion
|
|
err = db.Select(&retrieved,
|
|
"SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE `email` = @@email",
|
|
0,
|
|
"alice@example.com")
|
|
if !errors.Is(err, sql.ErrNoRows) {
|
|
t.Error("Expected user to be deleted")
|
|
}
|
|
}
|
|
```
|
|
|
|
## Best Practices
|
|
|
|
### 1. Use Helper Functions
|
|
|
|
```go
|
|
func expectUserQuery(mock sqlmock.Sqlmock, users []User) {
|
|
rows := sqlmock.NewRows([]string{"id", "name", "email"})
|
|
for _, u := range users {
|
|
rows.AddRow(u.ID, u.Name, u.Email)
|
|
}
|
|
mock.ExpectQuery("SELECT (.+) FROM `users`").WillReturnRows(rows)
|
|
}
|
|
|
|
func TestWithHelper(t *testing.T) {
|
|
db, mock := setupMockDB(t)
|
|
defer mock.ExpectClose()
|
|
|
|
expectedUsers := []User{{ID: 1, Name: "Alice", Email: "alice@example.com"}}
|
|
expectUserQuery(mock, expectedUsers)
|
|
|
|
var users []User
|
|
db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
|
|
|
|
// Assertions...
|
|
}
|
|
```
|
|
|
|
### 2. Test Error Paths
|
|
|
|
```go
|
|
func TestInsertDuplicateEmail(t *testing.T) {
|
|
db, mock := setupMockDB(t)
|
|
defer mock.ExpectClose()
|
|
|
|
mock.ExpectExec("INSERT INTO `users`").
|
|
WillReturnError(&mysqlDriver.MySQLError{Number: 1062}) // Duplicate entry
|
|
|
|
user := User{Name: "Alice", Email: "alice@example.com"}
|
|
err := db.Insert("users", user)
|
|
|
|
if err == nil {
|
|
t.Error("Expected duplicate key error")
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Clean Up Resources
|
|
|
|
```go
|
|
func TestWithCleanup(t *testing.T) {
|
|
db, mock := setupMockDB(t)
|
|
t.Cleanup(func() {
|
|
mock.ExpectClose()
|
|
// Any other cleanup
|
|
})
|
|
|
|
// Test code...
|
|
}
|
|
```
|
|
|
|
### 4. Test Concurrent Access
|
|
|
|
```go
|
|
func TestConcurrentAccess(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
|
|
var wg sync.WaitGroup
|
|
errors := make(chan error, 10)
|
|
|
|
for i := 0; i < 10; i++ {
|
|
wg.Add(1)
|
|
go func(id int) {
|
|
defer wg.Done()
|
|
|
|
user := User{
|
|
Name: fmt.Sprintf("User%d", id),
|
|
Email: fmt.Sprintf("user%d@example.com", id),
|
|
}
|
|
|
|
if err := db.Insert("users", user); err != nil {
|
|
errors <- err
|
|
}
|
|
}(i)
|
|
}
|
|
|
|
wg.Wait()
|
|
close(errors)
|
|
|
|
for err := range errors {
|
|
t.Errorf("Concurrent insert failed: %v", err)
|
|
}
|
|
|
|
// Verify all users inserted
|
|
var count int64
|
|
count, err := db.Count("SELECT COUNT(*) FROM `users`", 0)
|
|
if err != nil {
|
|
t.Fatalf("Count failed: %v", err)
|
|
}
|
|
|
|
if count != 10 {
|
|
t.Errorf("Expected 10 users, got %d", count)
|
|
}
|
|
}
|
|
```
|
|
|
|
### 5. Use Subtests
|
|
|
|
```go
|
|
func TestUserOperations(t *testing.T) {
|
|
db := testutil.SetupTestDB(t)
|
|
|
|
t.Run("Insert", func(t *testing.T) {
|
|
user := User{Name: "Alice", Email: "alice@example.com"}
|
|
err := db.Insert("users", user)
|
|
if err != nil {
|
|
t.Fatalf("Insert failed: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("Select", func(t *testing.T) {
|
|
var users []User
|
|
err := db.Select(&users, "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users`", 0)
|
|
if err != nil {
|
|
t.Fatalf("Select failed: %v", err)
|
|
}
|
|
if len(users) == 0 {
|
|
t.Error("Expected at least one user")
|
|
}
|
|
})
|
|
|
|
t.Run("Update", func(t *testing.T) {
|
|
err := db.Exec("UPDATE `users` SET `name` = @@name WHERE `email` = @@email",
|
|
mysql.Params{"name": "Alice Updated", "email": "alice@example.com"})
|
|
if err != nil {
|
|
t.Fatalf("Update failed: %v", err)
|
|
}
|
|
})
|
|
}
|
|
```
|
|
|
|
### 6. Verify Expectations
|
|
|
|
```go
|
|
func TestAlwaysVerify(t *testing.T) {
|
|
db, mock := setupMockDB(t)
|
|
defer func() {
|
|
if err := mock.ExpectationsWereMet(); err != nil {
|
|
t.Errorf("Unfulfilled expectations: %v", err)
|
|
}
|
|
}()
|
|
|
|
// Test code...
|
|
}
|
|
```
|
|
|
|
### 7. Test Parameter Interpolation
|
|
|
|
```go
|
|
func TestParameterInterpolation(t *testing.T) {
|
|
db, _ := setupMockDB(t)
|
|
|
|
query := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > @@minAge AND `status` = @@status"
|
|
params := mysql.Params{"minAge": 18, "status": "active"}
|
|
|
|
replacedQuery, normalizedParams, err := db.InterpolateParams(query, params)
|
|
if err != nil {
|
|
t.Fatalf("InterpolateParams failed: %v", err)
|
|
}
|
|
|
|
expectedQuery := "SELECT `id`, `name`, `email`, `age`, `active`, `created_at`, `updated_at` FROM `users` WHERE age > ? AND `status` = ?"
|
|
if replacedQuery != expectedQuery {
|
|
t.Errorf("Expected query '%s', got '%s'", expectedQuery, replacedQuery)
|
|
}
|
|
|
|
if len(normalizedParams) != 2 {
|
|
t.Errorf("Expected 2 params, got %d", len(normalizedParams))
|
|
}
|
|
}
|
|
```
|