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
24 changes: 18 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,31 @@ jobs:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: user
POSTGRES_PASSWORD: password
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpassword
POSTGRES_DB: testdb
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
- 5433:5432

mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: testdb
MYSQL_USER: testuser
MYSQL_PASSWORD: testpassword
options: >-
--health-cmd="mysqladmin ping -h localhost -u testuser -ptestpassword"
--health-interval=10s
--health-timeout=5s
--health-retries=5
ports:
- 3307:3306

steps:
- name: Checkout code
Expand Down Expand Up @@ -54,7 +69,4 @@ jobs:
version: latest

- name: Run tests
env:
DB_DRIVER: postgres
DB_CONNECTION_STRING: postgres://user:password@localhost:5432/testdb?sslmode=disable
run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
12 changes: 12 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,18 @@ test: ## Run tests
@go test -v -race -coverprofile=coverage.out ./...
@go tool cover -func=coverage.out

test-with-db: test-db-up test test-db-down ## Run tests with test databases

test-db-up: ## Start test databases
@echo "Starting test databases..."
@docker compose -f docker-compose.test.yml up -d
@echo "Waiting for databases to be ready..."
@sleep 10

test-db-down: ## Stop test databases
@echo "Stopping test databases..."
@docker compose -f docker-compose.test.yml down -v

test-coverage: test ## Run tests and show coverage in browser
@go tool cover -html=coverage.out

Expand Down
168 changes: 156 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ A production-ready Go project template following Clean Architecture and Domain-D
- **Input Validation** - Advanced validation with jellydator/validation library including password strength, email format, and custom rules
- **Password Hashing** - Secure password hashing with Argon2id via go-pwdhash
- **Docker Support** - Multi-stage Dockerfile for minimal container size
- **CI/CD** - GitHub Actions workflow for linting and testing
- **Integration Testing** - Real database tests using Docker Compose instead of mocks
- **CI/CD** - GitHub Actions workflow with PostgreSQL and MySQL for comprehensive testing
- **Comprehensive Makefile** - Easy development and deployment commands

## Project Structure
Expand Down Expand Up @@ -73,14 +74,17 @@ go-project-template/
│ ├── validation/ # Custom validation rules
│ │ ├── rules.go
│ │ └── rules_test.go
│ ├── testutil/ # Test utilities
│ │ └── database.go # Database test helpers
│ └── worker/ # Background workers
│ └── event_worker.go
├── migrations/
│ ├── mysql/ # MySQL migrations
│ └── postgresql/ # PostgreSQL migrations
├── .github/
│ └── workflows/
│ └── ci.yml
│ └── ci.yml # CI workflow with PostgreSQL & MySQL
├── docker-compose.test.yml # Test database configuration
├── Dockerfile
├── Makefile
├── go.mod
Expand All @@ -104,15 +108,16 @@ The project follows a modular domain architecture where each business domain is
- **`httputil/`** - Shared HTTP utility functions including error mapping and JSON responses
- **`config/`** - Application-wide configuration
- **`database/`** - Database connection and transaction management
- **`testutil/`** - Test helper utilities for database setup and cleanup
- **`worker/`** - Background processing infrastructure

This structure makes it easy to add new domains (e.g., `internal/product/`, `internal/order/`) without affecting existing modules.

## Prerequisites

- Go 1.25 or higher
- PostgreSQL 12+ or MySQL 8.0+
- Docker (optional)
- PostgreSQL 12+ or MySQL 8.0+ (for development)
- Docker and Docker Compose (for testing and optional development)
- Make (optional, for convenience commands)

## Quick Start
Expand Down Expand Up @@ -495,18 +500,35 @@ Then use `httputil.HandleError()` in your HTTP handlers for automatic mapping.
make build
```

### Run tests
### Testing

**Start test databases:**
```bash
make test-db-up
```

**Run tests:**
```bash
make test
```

### Run tests with coverage
**Run tests with automatic database management:**
```bash
make test-with-db # Starts databases, runs tests, stops databases
```

**Run tests with coverage:**
```bash
make test-coverage
```

**Stop test databases:**
```bash
make test-db-down
```

See the [Testing](#testing) section for more details.

### Run linter

```bash
Expand Down Expand Up @@ -643,6 +665,55 @@ func GetTx(ctx context.Context, db *sql.DB) Querier {

This pattern ensures repositories work seamlessly within transactions managed by the use case layer.

### Testing Approach

The project uses **integration testing with real databases** instead of mocks for repository layer tests. This approach provides:

- **Accuracy** - Tests verify actual SQL queries and database behavior
- **Real Integration** - Catches database-specific issues (constraints, types, unique violations, etc.)
- **Production Parity** - Tests reflect real production scenarios
- **Less Maintenance** - No mock expectations to maintain or update
- **Confidence** - Full database integration coverage

**Test Infrastructure:**

Tests use Docker Compose to spin up isolated test databases (PostgreSQL on port 5433, MySQL on port 3307) with dedicated test credentials. The `testutil` package provides helper functions that:

1. Connect to test databases
2. Run migrations automatically
3. Clean up data between tests
4. Provide isolated test environments

**Example test structure:**

```go
func TestPostgreSQLUserRepository_Create(t *testing.T) {
db := testutil.SetupPostgresDB(t) // Connect and run migrations
defer testutil.TeardownDB(t, db) // Clean up connection
defer testutil.CleanupPostgresDB(t, db) // Clean up test data

repo := NewPostgreSQLUserRepository(db)
ctx := context.Background()

user := &domain.User{
ID: uuid.Must(uuid.NewV7()),
Name: "John Doe",
Email: "john@example.com",
Password: "hashed_password",
}

err := repo.Create(ctx, user)
assert.NoError(t, err)

// Verify by querying the real database
createdUser, err := repo.GetByID(ctx, user.ID)
assert.NoError(t, err)
assert.Equal(t, user.Name, createdUser.Name)
}
```

This testing strategy ensures repository implementations work correctly with actual databases while maintaining fast test execution (~15 seconds for the full test suite).

### Dependency Injection Container

The project uses a custom dependency injection (DI) container located in `internal/app/` to manage all application components. This provides:
Expand Down Expand Up @@ -1132,21 +1203,94 @@ migrate -path migrations/postgresql -database "postgres://user:password@localhos

## Testing

The project includes a CI workflow that runs tests with PostgreSQL.
The project uses real databases (PostgreSQL and MySQL) for testing instead of mocks, ensuring tests accurately reflect production behavior.

### Test Infrastructure

Tests use Docker Compose to run test databases with dedicated test credentials and ports:

- **PostgreSQL**: `localhost:5433` (testuser/testpassword/testdb)
- **MySQL**: `localhost:3307` (testuser/testpassword/testdb)

The test helper utilities (`internal/testutil/database.go`) automatically:
1. Connect to test databases
2. Run migrations automatically before tests
3. Clean up data between tests to prevent pollution
4. Provide isolated test environments

### Running tests locally
**Important:** Both local development (via Docker Compose) and CI (via GitHub Actions) use identical database configurations, ensuring tests behave the same in all environments.

### Running Tests

**Start test databases:**
```bash
go test -v -race ./...
make test-db-up
```

### With coverage
**Run all tests:**
```bash
make test
```

**Run tests with coverage:**
```bash
go test -v -race -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
make test-coverage
```

**Run tests and manage databases automatically:**
```bash
make test-with-db # Starts databases, runs tests, stops databases
```

**Stop test databases:**
```bash
make test-db-down
```

### Running Tests Locally

```bash
# With test databases already running
go test -v -race ./...
```

### Test Structure

Tests use real database connections instead of mocks:

```go
func TestPostgreSQLUserRepository_Create(t *testing.T) {
db := testutil.SetupPostgresDB(t) // Connect and run migrations
defer testutil.TeardownDB(t, db) // Clean up connection
defer testutil.CleanupPostgresDB(t, db) // Clean up test data

repo := NewPostgreSQLUserRepository(db)
// ... test implementation
}
```

**Benefits of using real databases:**
- Tests verify actual SQL queries and database interactions
- Catches database-specific issues (constraints, types, etc.)
- Tests reflect production behavior more accurately
- No need to maintain mock expectations

### CI/CD Testing

The GitHub Actions workflow automatically:
1. Starts PostgreSQL (port 5433) and MySQL (port 3307) containers
2. Waits for both databases to be healthy
3. Runs all tests with race detection against both databases
4. Generates and uploads coverage reports to Codecov

**CI Configuration:**
- Uses the same database credentials as local tests (testuser/testpassword/testdb)
- Same port mappings as Docker Compose (5433 for Postgres, 3307 for MySQL)
- Runs on every push to `main` and all pull requests
- All tests must pass before merging

This ensures complete consistency between local development and CI environments.

## Dependencies

### Core Libraries
Expand Down
31 changes: 31 additions & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
services:
postgres-test:
image: postgres:16-alpine
container_name: go-project-template-postgres-test
environment:
POSTGRES_USER: testuser
POSTGRES_PASSWORD: testpassword
POSTGRES_DB: testdb
ports:
- "5433:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U testuser -d testdb"]
interval: 5s
timeout: 5s
retries: 5

mysql-test:
image: mysql:8.0
container_name: go-project-template-mysql-test
environment:
MYSQL_ROOT_PASSWORD: rootpassword
MYSQL_DATABASE: testdb
MYSQL_USER: testuser
MYSQL_PASSWORD: testpassword
ports:
- "3307:3306"
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "testuser", "-ptestpassword"]
interval: 5s
timeout: 5s
retries: 5
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ module github.com/allisson/go-project-template
go 1.25

require (
github.com/DATA-DOG/go-sqlmock v1.5.2
github.com/allisson/go-env v0.6.0
github.com/allisson/go-pwdhash v0.3.1
github.com/go-sql-driver/mysql v1.9.3
Expand Down
3 changes: 0 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/allisson/go-env v0.6.0 h1:YaWmnOjhF+0c7GjgJef4LC0XymV12EIoVxJHpHGnGnU=
Expand Down Expand Up @@ -49,7 +47,6 @@ github.com/jellydator/validation v1.2.0 h1:z3P3Hk5kdT9epXDraWAfMZtOIUM7UQ0PkNAnF
github.com/jellydator/validation v1.2.0/go.mod h1:AaCjfkQ4Ykdcb+YCwqCtaI3wDsf2UAGhJ06lJs0VgOw=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/kisielk/sqlstruct v0.0.0-20201105191214-5f3e10d3ab46/go.mod h1:yyMNCyc/Ib3bDTKd379tNMpB/7/H5TjM2Y9QJ5THLbE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
Expand Down
Loading