813 lines
25 KiB
Markdown
813 lines
25 KiB
Markdown
|
|
---
|
||
|
|
name: go-backend-dev
|
||
|
|
description: "Backend Agent uses this skill to implement Golang backend. Based on implementation plan and API spec, use Domain-Driven + go-zero style architecture and TDD process to produce production-ready code. Trigger: After Task Breakdown complete (Stage 9)."
|
||
|
|
---
|
||
|
|
|
||
|
|
# /go-backend-dev — Golang Backend Implementation
|
||
|
|
|
||
|
|
Backend Agent uses this skill to implement Golang backend.
|
||
|
|
|
||
|
|
## Responsibilities
|
||
|
|
|
||
|
|
1. Establish project structure based on implementation plan (Domain-Driven + go-zero style)
|
||
|
|
2. Implement features using TDD process (Red-Green-Refactor)
|
||
|
|
3. Deliver incrementally by vertical slices (end-to-end, not layer-by-layer)
|
||
|
|
4. Implement Domain / Usecase / Logic / Repository layers
|
||
|
|
5. Write unit tests and integration tests
|
||
|
|
|
||
|
|
## Input
|
||
|
|
|
||
|
|
- Implementation plan (`./plans/{feature}.md`)
|
||
|
|
- API spec (`docs/api/{date}-{feature}.yaml`)
|
||
|
|
- DB Schema (`docs/db/{date}-{feature}.sql`)
|
||
|
|
|
||
|
|
## Output
|
||
|
|
|
||
|
|
- Golang code structure
|
||
|
|
- Test code (unit tests >= 80%, business logic >= 90%)
|
||
|
|
- Protobuf definitions (if gRPC needed)
|
||
|
|
|
||
|
|
## Flow
|
||
|
|
|
||
|
|
```
|
||
|
|
Read implementation plan + API spec + DB Schema
|
||
|
|
↓
|
||
|
|
Identify vertical slices (each slice = end-to-end feature)
|
||
|
|
↓
|
||
|
|
Execute TDD loop for each slice:
|
||
|
|
├── RED: Write test → Test fails
|
||
|
|
├── GREEN: Write minimal code → Test passes
|
||
|
|
└── REFACTOR: Refactor → Test still passes
|
||
|
|
↓
|
||
|
|
Build order within slice:
|
||
|
|
domain (entity/value object/interface)
|
||
|
|
→ pkg/domain/usecase (interface)
|
||
|
|
→ pkg/domain/repository (interface)
|
||
|
|
→ pkg/usecase (implementation)
|
||
|
|
→ pkg/mock (mock)
|
||
|
|
→ internal/logic (handler logic)
|
||
|
|
→ pkg/repository (infrastructure implementation)
|
||
|
|
↓
|
||
|
|
All slices complete → Run integration tests
|
||
|
|
↓
|
||
|
|
Confirm deliverables checklist
|
||
|
|
```
|
||
|
|
|
||
|
|
### Step Details
|
||
|
|
|
||
|
|
**1. Read Input**
|
||
|
|
|
||
|
|
Read three documents simultaneously:
|
||
|
|
- Implementation plan: Understand vertical slice breakdown and priority
|
||
|
|
- API spec: Understand endpoints, request/response structures
|
||
|
|
- DB Schema: Understand table structures and relationships
|
||
|
|
|
||
|
|
**2. Identify Vertical Slices**
|
||
|
|
|
||
|
|
Not horizontal slicing (layer by layer), but vertical slicing (end-to-end):
|
||
|
|
|
||
|
|
```
|
||
|
|
✅ Correct way (vertical):
|
||
|
|
Slice 1: User registration (domain.entity + domain.usecase interface + usecase implementation + logic + repository)
|
||
|
|
Slice 2: User login (same as above)
|
||
|
|
Slice 3: User list (same as above)
|
||
|
|
|
||
|
|
❌ Wrong way (horizontal):
|
||
|
|
Stage 1: All domain entities
|
||
|
|
Stage 2: All usecases
|
||
|
|
Stage 3: All logic handlers
|
||
|
|
```
|
||
|
|
|
||
|
|
**3. TDD Loop (Each Slice)**
|
||
|
|
|
||
|
|
For each slice, follow Red-Green-Refactor:
|
||
|
|
|
||
|
|
```
|
||
|
|
RED: Write a test → Test fails
|
||
|
|
GREEN: Write minimal code to make test pass → Test passes
|
||
|
|
REFACTOR: Refactor code → Test still passes
|
||
|
|
```
|
||
|
|
|
||
|
|
Build order within slice (inside-out):
|
||
|
|
1. `pkg/domain/entity/` — Define Entity and Value Object
|
||
|
|
2. `pkg/domain/member/` — Define value objects and enums (with tests)
|
||
|
|
3. `pkg/domain/usecase/` — Define Use Case interface
|
||
|
|
4. `pkg/domain/repository/` — Define Repository interface
|
||
|
|
5. `pkg/usecase/` — Implement business logic (write tests first)
|
||
|
|
6. `pkg/mock/` — Generate mocks
|
||
|
|
7. `internal/logic/` — Handler logic
|
||
|
|
8. `pkg/repository/` — Infrastructure implementation (with DB tests)
|
||
|
|
|
||
|
|
**4. Testing**
|
||
|
|
|
||
|
|
After each slice completes, ensure:
|
||
|
|
- Unit tests pass (`pkg/usecase/*_test.go`)
|
||
|
|
- Value object tests pass (`pkg/domain/member/*_test.go`)
|
||
|
|
- Repository tests pass (`pkg/repository/*_test.go`)
|
||
|
|
- Integration tests pass (critical paths)
|
||
|
|
- Test coverage meets requirements
|
||
|
|
|
||
|
|
**5. Completion Verification**
|
||
|
|
|
||
|
|
Finally confirm all deliverables are complete.
|
||
|
|
|
||
|
|
## Project Structure
|
||
|
|
|
||
|
|
```
|
||
|
|
project-root/
|
||
|
|
├── build/
|
||
|
|
│ └── Dockerfile # Build image
|
||
|
|
│
|
||
|
|
├── etc/
|
||
|
|
│ └── {service}.example.yaml # Example config file
|
||
|
|
│
|
||
|
|
├── generate/
|
||
|
|
│ └── protobuf/
|
||
|
|
│ └── {service}.proto # Protobuf definitions (if gRPC)
|
||
|
|
│
|
||
|
|
├── internal/ # Application layer (not exposed externally)
|
||
|
|
│ ├── config/
|
||
|
|
│ │ └── config.go # Application config
|
||
|
|
│ ├── logic/
|
||
|
|
│ │ └── {module}/
|
||
|
|
│ │ ├── create_{entity}_logic.go # One logic file per use case
|
||
|
|
│ │ ├── get_{entity}_logic.go
|
||
|
|
│ │ ├── update_{entity}_logic.go
|
||
|
|
│ │ └── ...
|
||
|
|
│ ├── server/
|
||
|
|
│ │ └── {module}/
|
||
|
|
│ │ └── {module}_server.go # Server definition (HTTP/gRPC)
|
||
|
|
│ └── svc/
|
||
|
|
│ └── service_context.go # Dependency injection container
|
||
|
|
│
|
||
|
|
├── pkg/ # Domain layer (can be exposed externally)
|
||
|
|
│ ├── domain/
|
||
|
|
│ │ ├── config/
|
||
|
|
│ │ │ └── config.go # Domain config
|
||
|
|
│ │ ├── entity/
|
||
|
|
│ │ │ ├── {entity}.go # Entity definition
|
||
|
|
│ │ │ ├── {entity}_uid_table.go # UID mapping table
|
||
|
|
│ │ │ └── auto_id.go # Auto ID generation
|
||
|
|
│ │ ├── {module}/
|
||
|
|
│ │ │ ├── {value_object}.go # Value objects and enums
|
||
|
|
│ │ │ └── {value_object}_test.go # Value object tests
|
||
|
|
│ │ ├── repository/
|
||
|
|
│ │ │ ├── {entity}.go # Repository interface
|
||
|
|
│ │ │ └── ...
|
||
|
|
│ │ ├── usecase/
|
||
|
|
│ │ │ ├── {module}.go # Use Case interface
|
||
|
|
│ │ │ └── ...
|
||
|
|
│ │ ├── errors.go # Domain sentinel errors
|
||
|
|
│ │ ├── const.go # Domain constants
|
||
|
|
│ │ └── redis.go # Redis domain definitions
|
||
|
|
│ ├── mock/
|
||
|
|
│ │ ├── repository/
|
||
|
|
│ │ │ ├── {entity}.go # Repository mock
|
||
|
|
│ │ │ └── ...
|
||
|
|
│ │ └── usecase/
|
||
|
|
│ │ └── {module}.go # Use Case mock
|
||
|
|
│ ├── repository/
|
||
|
|
│ │ ├── {entity}.go # Repository implementation
|
||
|
|
│ │ ├── {entity}_test.go # Repository tests
|
||
|
|
│ │ ├── {entity}_uid.go # UID Repository implementation
|
||
|
|
│ │ ├── {entity}_uid_test.go
|
||
|
|
│ │ ├── error.go # Repository error definitions
|
||
|
|
│ │ └── start_{db}_container_test.go # testcontainers startup
|
||
|
|
│ └── usecase/
|
||
|
|
│ ├── {module}.go # Use Case implementation
|
||
|
|
│ ├── {operation}.go # Specific operation
|
||
|
|
│ ├── {operation}_test.go # Use Case tests
|
||
|
|
│ └── {utils}.go # Utility functions
|
||
|
|
│
|
||
|
|
├── {service}.go # Application entry point
|
||
|
|
├── Makefile
|
||
|
|
├── go.mod
|
||
|
|
├── go.sum
|
||
|
|
├── docker-compose.yml
|
||
|
|
└── readme.md
|
||
|
|
```
|
||
|
|
|
||
|
|
## Dependency Direction Rules
|
||
|
|
|
||
|
|
```
|
||
|
|
pkg/domain/ ← No external dependencies (innermost, pure definitions)
|
||
|
|
↑
|
||
|
|
pkg/domain/usecase/ ← Use Case interface (only interface definitions)
|
||
|
|
pkg/domain/repository/ ← Repository interface (only interface definitions)
|
||
|
|
↑
|
||
|
|
pkg/usecase/ ← Depends on domain interfaces (business logic implementation)
|
||
|
|
pkg/mock/ ← Depends on domain interfaces (test mocks)
|
||
|
|
↑
|
||
|
|
internal/logic/ ← Depends on usecase implementations (handler logic)
|
||
|
|
internal/server/ ← Depends on logic (HTTP/gRPC server)
|
||
|
|
internal/svc/ ← Depends on all (DI container, assemble dependencies)
|
||
|
|
↑
|
||
|
|
pkg/repository/ ← Depends on domain interfaces (infrastructure implementation)
|
||
|
|
```
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────┐
|
||
|
|
│ pkg/domain/ │ ← Pure definitions, no dependencies
|
||
|
|
│ ├── entity/ │
|
||
|
|
│ ├── {module}/ (value objects)│
|
||
|
|
│ ├── repository/ (interfaces) │
|
||
|
|
│ ├── usecase/ (interfaces) │
|
||
|
|
│ ├── errors.go │
|
||
|
|
│ └── const.go │
|
||
|
|
├─────────────────────────────────┤
|
||
|
|
│ pkg/usecase/ │ ← Depends on domain interfaces
|
||
|
|
│ pkg/mock/ │ ← Depends on domain interfaces
|
||
|
|
├─────────────────────────────────┤
|
||
|
|
│ internal/logic/ │ ← Depends on usecase
|
||
|
|
│ internal/server/ │
|
||
|
|
│ internal/config/ │
|
||
|
|
│ internal/svc/ │ ← DI container
|
||
|
|
├─────────────────────────────────┤
|
||
|
|
│ pkg/repository/ │ ← Depends on domain interfaces
|
||
|
|
└─────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
## Architecture Principles
|
||
|
|
|
||
|
|
### `pkg/domain/` — Pure Domain Definitions
|
||
|
|
|
||
|
|
`pkg/domain/` is the core, containing only **interfaces and definitions**, no implementations:
|
||
|
|
|
||
|
|
```go
|
||
|
|
// pkg/domain/entity/user.go — Entity definition
|
||
|
|
package entity
|
||
|
|
|
||
|
|
type User struct {
|
||
|
|
ID string `json:"id"`
|
||
|
|
Email string `json:"email"`
|
||
|
|
Name string `json:"name"`
|
||
|
|
Status string `json:"status"`
|
||
|
|
CreatedAt time.Time `json:"created_at"`
|
||
|
|
UpdatedAt time.Time `json:"updated_at"`
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```go
|
||
|
|
// pkg/domain/member/status.go — Value object (with tests)
|
||
|
|
package member
|
||
|
|
|
||
|
|
type Status string
|
||
|
|
|
||
|
|
const (
|
||
|
|
StatusActive Status = "active"
|
||
|
|
StatusInactive Status = "inactive"
|
||
|
|
)
|
||
|
|
|
||
|
|
func (s Status) IsValid() bool {
|
||
|
|
switch s {
|
||
|
|
case StatusActive, StatusInactive:
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewStatus(s string) (Status, error) {
|
||
|
|
status := Status(s)
|
||
|
|
if !status.IsValid() {
|
||
|
|
return "", fmt.Errorf("invalid status: %s", s)
|
||
|
|
}
|
||
|
|
return status, nil
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```go
|
||
|
|
// pkg/domain/member/status_test.go — Value object tests
|
||
|
|
package member
|
||
|
|
|
||
|
|
func TestStatus_IsValid(t *testing.T) {
|
||
|
|
assert.True(t, StatusActive.IsValid())
|
||
|
|
assert.True(t, StatusInactive.IsValid())
|
||
|
|
assert.False(t, Status("unknown").IsValid())
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestNewStatus(t *testing.T) {
|
||
|
|
status, err := NewStatus("active")
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.Equal(t, StatusActive, status)
|
||
|
|
|
||
|
|
_, err = NewStatus("unknown")
|
||
|
|
assert.Error(t, err)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```go
|
||
|
|
// pkg/domain/repository/user.go — Repository interface
|
||
|
|
package repository
|
||
|
|
|
||
|
|
type UserRepository interface {
|
||
|
|
GetByID(ctx context.Context, id string) (*entity.User, error)
|
||
|
|
GetByEmail(ctx context.Context, email string) (*entity.User, error)
|
||
|
|
Create(ctx context.Context, user *entity.User) error
|
||
|
|
Update(ctx context.Context, user *entity.User) error
|
||
|
|
Delete(ctx context.Context, id string) error
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```go
|
||
|
|
// pkg/domain/usecase/user.go — Use Case interface
|
||
|
|
package usecase
|
||
|
|
|
||
|
|
type UserUsecase interface {
|
||
|
|
CreateUser(ctx context.Context, input CreateUserInput) (*entity.User, error)
|
||
|
|
GetUser(ctx context.Context, id string) (*entity.User, error)
|
||
|
|
UpdateUser(ctx context.Context, id string, input UpdateUserInput) (*entity.User, error)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```go
|
||
|
|
// pkg/domain/errors.go — Domain sentinel errors
|
||
|
|
package domain
|
||
|
|
|
||
|
|
import "errors"
|
||
|
|
|
||
|
|
var (
|
||
|
|
ErrUserNotFound = errors.New("user not found")
|
||
|
|
ErrInvalidInput = errors.New("invalid input")
|
||
|
|
ErrDuplicateEmail = errors.New("email already exists")
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
### `pkg/usecase/` — Business Logic Implementation
|
||
|
|
|
||
|
|
One functional domain per file, test file in same directory:
|
||
|
|
|
||
|
|
```go
|
||
|
|
// pkg/usecase/account.go — Use Case entry point
|
||
|
|
package usecase
|
||
|
|
|
||
|
|
type AccountUsecase struct {
|
||
|
|
userRepo repository.UserRepository
|
||
|
|
accountRepo repository.AccountRepository
|
||
|
|
redis *redis.Client
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewAccountUsecase(
|
||
|
|
userRepo repository.UserRepository,
|
||
|
|
accountRepo repository.AccountRepository,
|
||
|
|
redis *redis.Client,
|
||
|
|
) *AccountUsecase {
|
||
|
|
return &AccountUsecase{
|
||
|
|
userRepo: userRepo,
|
||
|
|
accountRepo: accountRepo,
|
||
|
|
redis: redis,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```go
|
||
|
|
// pkg/usecase/create_user.go — Single operation
|
||
|
|
package usecase
|
||
|
|
|
||
|
|
func (uc *AccountUsecase) CreateUser(ctx context.Context, input CreateUserInput) (*entity.User, error) {
|
||
|
|
if err := input.Validate(); err != nil {
|
||
|
|
return nil, fmt.Errorf("validate input: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
existing, _ := uc.userRepo.GetByEmail(ctx, input.Email)
|
||
|
|
if existing != nil {
|
||
|
|
return nil, domain.ErrDuplicateEmail
|
||
|
|
}
|
||
|
|
|
||
|
|
user, err := entity.NewUser(input.Email, input.Password, input.Name)
|
||
|
|
if err != nil {
|
||
|
|
return nil, fmt.Errorf("create user: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := uc.userRepo.Create(ctx, user); err != nil {
|
||
|
|
return nil, fmt.Errorf("save user: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
return user, nil
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```go
|
||
|
|
// pkg/usecase/create_user_test.go — Test in same directory
|
||
|
|
package usecase
|
||
|
|
|
||
|
|
func TestAccountUsecase_CreateUser_Success(t *testing.T) {
|
||
|
|
mockUserRepo := new(mock.UserRepository)
|
||
|
|
uc := NewAccountUsecase(mockUserRepo, nil, nil)
|
||
|
|
|
||
|
|
mockUserRepo.On("GetByEmail", mock.Anything, "test@example.com").Return(nil, nil)
|
||
|
|
mockUserRepo.On("Create", mock.Anything, mock.AnythingOfType("*entity.User")).Return(nil)
|
||
|
|
|
||
|
|
user, err := uc.CreateUser(context.Background(), input)
|
||
|
|
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.NotNil(t, user)
|
||
|
|
mockUserRepo.AssertExpectations(t)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `pkg/mock/` — Auto-Generated Mocks
|
||
|
|
|
||
|
|
```go
|
||
|
|
// pkg/mock/repository/user.go — Generated by mockery
|
||
|
|
//go:generate mockery --name=UserRepository --output=../../mock/repository --outpkg=mock_repository
|
||
|
|
|
||
|
|
package mock_repository
|
||
|
|
|
||
|
|
import (
|
||
|
|
"github.com/stretchr/testify/mock"
|
||
|
|
"your-project/pkg/domain/repository"
|
||
|
|
)
|
||
|
|
|
||
|
|
type UserRepository struct {
|
||
|
|
mock.Mock
|
||
|
|
}
|
||
|
|
|
||
|
|
func (m *UserRepository) GetByID(ctx context.Context, id string) (*entity.User, error) {
|
||
|
|
args := m.Called(ctx, id)
|
||
|
|
if args.Get(0) == nil {
|
||
|
|
return nil, args.Error(1)
|
||
|
|
}
|
||
|
|
return args.Get(0).(*entity.User), args.Error(1)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `internal/logic/` — Handler Logic
|
||
|
|
|
||
|
|
One logic file per use case (go-zero style):
|
||
|
|
|
||
|
|
```go
|
||
|
|
// internal/logic/account/create_user_logic.go
|
||
|
|
package account
|
||
|
|
|
||
|
|
type CreateUserLogic struct {
|
||
|
|
ctx context.Context
|
||
|
|
svcCtx *svc.ServiceContext
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewCreateUserLogic(ctx context.Context, svcCtx *svc.ServiceContext) *CreateUserLogic {
|
||
|
|
return &CreateUserLogic{
|
||
|
|
ctx: ctx,
|
||
|
|
svcCtx: svcCtx,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (l *CreateUserLogic) CreateUser(req *types.CreateUserReq) (*types.UserResp, error) {
|
||
|
|
user, err := l.svcCtx.UserUsecase.CreateUser(l.ctx, usecase.CreateUserInput{
|
||
|
|
Email: req.Email,
|
||
|
|
Password: req.Password,
|
||
|
|
Name: req.Name,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
return nil, err
|
||
|
|
}
|
||
|
|
|
||
|
|
return &types.UserResp{
|
||
|
|
ID: user.ID,
|
||
|
|
Email: user.Email,
|
||
|
|
Name: user.Name,
|
||
|
|
}, nil
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `internal/svc/` — Dependency Injection Container
|
||
|
|
|
||
|
|
```go
|
||
|
|
// internal/svc/service_context.go
|
||
|
|
package svc
|
||
|
|
|
||
|
|
type ServiceContext struct {
|
||
|
|
Config config.Config
|
||
|
|
UserUsecase usecase.UserUsecase
|
||
|
|
AccountUsecase usecase.AccountUsecase
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewServiceContext(c config.Config) *ServiceContext {
|
||
|
|
db := mongo.NewClient(c.Mongo.URI)
|
||
|
|
redisClient := redis.NewClient(c.Redis)
|
||
|
|
|
||
|
|
userRepo := repository.NewUserRepository(db)
|
||
|
|
accountRepo := repository.NewAccountRepository(db)
|
||
|
|
|
||
|
|
return &ServiceContext{
|
||
|
|
Config: c,
|
||
|
|
UserUsecase: usecase.NewUserUsecase(userRepo, redisClient),
|
||
|
|
AccountUsecase: usecase.NewAccountUsecase(userRepo, accountRepo, redisClient),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### `pkg/repository/` — Infrastructure Implementation
|
||
|
|
|
||
|
|
```go
|
||
|
|
// pkg/repository/user.go
|
||
|
|
package repository
|
||
|
|
|
||
|
|
type userRepository struct {
|
||
|
|
db *mongo.Database
|
||
|
|
}
|
||
|
|
|
||
|
|
func NewUserRepository(db *mongo.Database) domain.Repository.UserRepository {
|
||
|
|
return &userRepository{db: db}
|
||
|
|
}
|
||
|
|
|
||
|
|
func (r *userRepository) GetByID(ctx context.Context, id string) (*entity.User, error) {
|
||
|
|
var user entity.User
|
||
|
|
err := r.db.Collection("users").FindOne(ctx, bson.M{"_id": id}).Decode(&user)
|
||
|
|
if err != nil {
|
||
|
|
if err == mongo.ErrNoDocuments {
|
||
|
|
return nil, domain.ErrUserNotFound
|
||
|
|
}
|
||
|
|
return nil, fmt.Errorf("find user by id: %w", err)
|
||
|
|
}
|
||
|
|
return &user, nil
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
```go
|
||
|
|
// pkg/repository/user_test.go
|
||
|
|
package repository
|
||
|
|
|
||
|
|
func TestUserRepository_GetByID_Success(t *testing.T) {
|
||
|
|
db := startMongoContainer(t)
|
||
|
|
defer db.Client().Disconnect(context.Background())
|
||
|
|
|
||
|
|
repo := NewUserRepository(db)
|
||
|
|
// ...
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Coding Standards
|
||
|
|
|
||
|
|
### File Naming
|
||
|
|
|
||
|
|
```
|
||
|
|
Value objects and enums: pkg/domain/{module}/{name}.go + _test.go
|
||
|
|
Entity: pkg/domain/entity/{name}.go
|
||
|
|
Repository interface: pkg/domain/repository/{name}.go
|
||
|
|
Usecase interface: pkg/domain/usecase/{module}.go
|
||
|
|
Usecase implementation: pkg/usecase/{operation}.go + _test.go
|
||
|
|
Usecase utilities: pkg/usecase/{module}_utils.go + _test.go
|
||
|
|
Repository impl: pkg/repository/{name}.go + _test.go
|
||
|
|
Mock: pkg/mock/repository/{name}.go
|
||
|
|
pkg/mock/usecase/{module}.go
|
||
|
|
Handler logic: internal/logic/{module}/{operation}_logic.go
|
||
|
|
Server: internal/server/{module}/{module}_server.go
|
||
|
|
Service Context: internal/svc/service_context.go
|
||
|
|
Protobuf definitions: generate/protobuf/{module}.proto
|
||
|
|
Config files: etc/{service}.yaml
|
||
|
|
Dockerfile: build/Dockerfile
|
||
|
|
```
|
||
|
|
|
||
|
|
### Naming Conventions
|
||
|
|
|
||
|
|
```go
|
||
|
|
// Package: lowercase, semantically clear
|
||
|
|
package usecase // not usecases
|
||
|
|
package repository // not repositories
|
||
|
|
package entity // not entities
|
||
|
|
|
||
|
|
// Entity struct: PascalCase, no suffix
|
||
|
|
type User struct { ... } // not UserModel, UserEntity
|
||
|
|
|
||
|
|
// Value Object: base type alias + methods
|
||
|
|
type Status string // not StatusEnum
|
||
|
|
|
||
|
|
// Interface: defined in pkg/domain/, semantic naming
|
||
|
|
type UserRepository interface { ... } // not UserRepo or UserRepositoryI
|
||
|
|
|
||
|
|
// Use Case struct: {Module}Usecase
|
||
|
|
type AccountUsecase struct { ... }
|
||
|
|
|
||
|
|
// Use Case methods: verb prefix
|
||
|
|
func (uc *AccountUsecase) CreateUser(ctx context.Context, ...) (*entity.User, error)
|
||
|
|
func (uc *AccountUsecase) GetUser(ctx context.Context, id string) (*entity.User, error)
|
||
|
|
|
||
|
|
// Logic struct: {Operation}Logic
|
||
|
|
type CreateUserLogic struct { ... }
|
||
|
|
|
||
|
|
// Error: Err prefix
|
||
|
|
var ErrUserNotFound = errors.New("user not found")
|
||
|
|
|
||
|
|
// Constant: PascalCase (exported) or camelCase (internal)
|
||
|
|
const MaxRetryCount = 3
|
||
|
|
const defaultPageSize = 20
|
||
|
|
```
|
||
|
|
|
||
|
|
### Error Handling
|
||
|
|
|
||
|
|
```go
|
||
|
|
// Sentinel errors — pkg/domain/errors.go
|
||
|
|
var (
|
||
|
|
ErrUserNotFound = errors.New("user not found")
|
||
|
|
ErrInvalidInput = errors.New("invalid input")
|
||
|
|
ErrDuplicateEmail = errors.New("email already exists")
|
||
|
|
)
|
||
|
|
|
||
|
|
// Error wrapping — always use %w
|
||
|
|
if err != nil {
|
||
|
|
return fmt.Errorf("create user: %w", err)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Error checking — always use errors.Is
|
||
|
|
if errors.Is(err, domain.ErrUserNotFound) {
|
||
|
|
// handle not found
|
||
|
|
}
|
||
|
|
|
||
|
|
// Repository errors — pkg/repository/error.go
|
||
|
|
var (
|
||
|
|
ErrMongoConnection = errors.New("mongo connection failed")
|
||
|
|
ErrRedisConnection = errors.New("redis connection failed")
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
### Interface Design
|
||
|
|
|
||
|
|
```go
|
||
|
|
// Interface defined in pkg/domain/ (consumer side)
|
||
|
|
// Implementation defined in pkg/ (provider side)
|
||
|
|
|
||
|
|
// Accept interfaces, return structs
|
||
|
|
func NewUserUsecase(repo repository.UserRepository, redis *redis.Client) *UserUsecase {
|
||
|
|
return &UserUsecase{repo: repo, redis: redis}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Keep interfaces small (1-3 methods)
|
||
|
|
type UserRepository interface {
|
||
|
|
GetByID(ctx context.Context, id string) (*entity.User, error)
|
||
|
|
Create(ctx context.Context, user *entity.User) error
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## TDD Standards
|
||
|
|
|
||
|
|
This skill integrates with `tdd` skill, following shared TDD principles.
|
||
|
|
|
||
|
|
### Test Pyramid
|
||
|
|
|
||
|
|
```
|
||
|
|
/\
|
||
|
|
/ \
|
||
|
|
/ E2E \ <- Few critical flows
|
||
|
|
/--------\
|
||
|
|
/Integration\ <- DB + Redis (testcontainers)
|
||
|
|
/--------------\
|
||
|
|
/ Unit Tests \ <- Most, 80%+ coverage
|
||
|
|
/--------------------\
|
||
|
|
```
|
||
|
|
|
||
|
|
### Test Location
|
||
|
|
|
||
|
|
```
|
||
|
|
Test files in same directory as source:
|
||
|
|
|
||
|
|
pkg/domain/member/status_test.go ← Value object tests
|
||
|
|
pkg/usecase/create_user_test.go ← Use Case tests
|
||
|
|
pkg/repository/user_test.go ← Repository tests
|
||
|
|
pkg/repository/start_mongo_container_test.go ← testcontainers startup
|
||
|
|
```
|
||
|
|
|
||
|
|
### Vertical Slice TDD
|
||
|
|
|
||
|
|
Each slice follows this order:
|
||
|
|
|
||
|
|
```
|
||
|
|
Slice: User registration
|
||
|
|
1. RED: Write TestStatus_IsValid (value object)
|
||
|
|
2. GREEN: Write Status.IsValid()
|
||
|
|
3. RED: Write TestAccountUsecase_CreateUser_Success
|
||
|
|
4. GREEN: Write domain/entity, domain/usecase interface, pkg/usecase implementation, mock
|
||
|
|
5. RED: Write TestAccountUsecase_CreateUser_DuplicateEmail
|
||
|
|
6. GREEN: Add duplicate check
|
||
|
|
7. RED: Write TestUserRepository_Create (DB test)
|
||
|
|
8. GREEN: Write pkg/repository/user.go
|
||
|
|
9. RED: Write TestCreateUserLogic (handler test)
|
||
|
|
10. GREEN: Write internal/logic/account/create_user_logic.go
|
||
|
|
11. REFACTOR: Clean up everything
|
||
|
|
```
|
||
|
|
|
||
|
|
### Mock Strategy
|
||
|
|
|
||
|
|
```go
|
||
|
|
// Use mockery to auto-generate mocks
|
||
|
|
// Add go:generate directive in interface file
|
||
|
|
//go:generate mockery --name=UserRepository --output=../../mock/repository --outpkg=mock_repository
|
||
|
|
|
||
|
|
// Unit tests use mock
|
||
|
|
func TestAccountUsecase_CreateUser_Success(t *testing.T) {
|
||
|
|
mockRepo := new(mock_repository.UserRepository)
|
||
|
|
uc := usecase.NewAccountUsecase(mockRepo, nil, nil)
|
||
|
|
|
||
|
|
mockRepo.On("GetByEmail", mock.Anything, "test@example.com").Return(nil, nil)
|
||
|
|
mockRepo.On("Create", mock.Anything, mock.AnythingOfType("*entity.User")).Return(nil)
|
||
|
|
|
||
|
|
user, err := uc.CreateUser(context.Background(), input)
|
||
|
|
|
||
|
|
assert.NoError(t, err)
|
||
|
|
assert.NotNil(t, user)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### testcontainers Strategy
|
||
|
|
|
||
|
|
```go
|
||
|
|
// pkg/repository/start_mongo_container_test.go
|
||
|
|
func startMongoContainer(t *testing.T) *mongo.Database {
|
||
|
|
ctx := context.Background()
|
||
|
|
req := testcontainers.ContainerRequest{
|
||
|
|
Image: "mongo:7",
|
||
|
|
ExposedPorts: []string{"27017/tcp"},
|
||
|
|
WaitingFor: wait.ForListeningPort("27017/tcp"),
|
||
|
|
}
|
||
|
|
|
||
|
|
mongoC, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
|
||
|
|
ContainerRequest: req,
|
||
|
|
Started: true,
|
||
|
|
})
|
||
|
|
require.NoError(t, err)
|
||
|
|
|
||
|
|
t.Cleanup(func() {
|
||
|
|
mongoC.Terminate(ctx)
|
||
|
|
})
|
||
|
|
|
||
|
|
// ... return connected database
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Coverage Requirements
|
||
|
|
|
||
|
|
- Value objects (`pkg/domain/member/`): >= 90%
|
||
|
|
- Use Case (`pkg/usecase/`): >= 90%
|
||
|
|
- Repository (`pkg/repository/`): >= 80%
|
||
|
|
- Logic (`internal/logic/`): >= 80% (mainly integration tests)
|
||
|
|
- Critical paths: Integration tests required
|
||
|
|
|
||
|
|
## Vertical Slice Template
|
||
|
|
|
||
|
|
File list for each vertical slice:
|
||
|
|
|
||
|
|
```
|
||
|
|
Slice: {operation}_{entity}
|
||
|
|
|
||
|
|
New/modified files:
|
||
|
|
├── pkg/domain/entity/{entity}.go ← Entity definition
|
||
|
|
├── pkg/domain/member/{value_object}.go ← Value object (if needed)
|
||
|
|
├── pkg/domain/member/{value_object}_test.go ← Value object tests
|
||
|
|
├── pkg/domain/repository/{entity}.go ← Repository interface
|
||
|
|
├── pkg/domain/usecase/{module}.go ← Use Case interface
|
||
|
|
├── pkg/usecase/{operation}.go ← Use Case implementation
|
||
|
|
├── pkg/usecase/{operation}_test.go ← Use Case tests
|
||
|
|
├── pkg/mock/repository/{entity}.go ← Repository mock
|
||
|
|
├── pkg/repository/{entity}.go ← Repository implementation
|
||
|
|
├── pkg/repository/{entity}_test.go ← Repository tests
|
||
|
|
├── internal/logic/{module}/{operation}_logic.go ← Handler logic
|
||
|
|
└── internal/svc/service_context.go ← Update DI
|
||
|
|
```
|
||
|
|
|
||
|
|
## Completion Checklist
|
||
|
|
|
||
|
|
### After Each Slice
|
||
|
|
- [ ] Value object tests pass
|
||
|
|
- [ ] Use Case tests pass
|
||
|
|
- [ ] Repository tests pass (with DB)
|
||
|
|
- [ ] Error handling complete
|
||
|
|
- [ ] Dependency direction correct (domain has no external dependencies)
|
||
|
|
|
||
|
|
### After All Complete
|
||
|
|
- [ ] Project structure follows Domain-Driven + go-zero style
|
||
|
|
- [ ] `pkg/domain/` contains all Entity, Value Object, interface definitions
|
||
|
|
- [ ] `pkg/usecase/` contains all business logic implementations
|
||
|
|
- [ ] `pkg/repository/` contains all infrastructure implementations
|
||
|
|
- [ ] `internal/logic/` contains all Handler logic
|
||
|
|
- [ ] `internal/svc/` contains complete dependency injection setup
|
||
|
|
- [ ] Unit tests >= 80% coverage
|
||
|
|
- [ ] Business logic >= 90% coverage
|
||
|
|
- [ ] Integration tests pass (critical paths)
|
||
|
|
- [ ] Error handling consistent and uses `%w` wrapping
|
||
|
|
|
||
|
|
## Related Skills
|
||
|
|
|
||
|
|
- **Prerequisite**: `prd-to-plan` (implementation plan), `be-api-design` (API spec), `dba-schema` (DB Schema)
|
||
|
|
- **辅助**: `tdd` (TDD Red-Green-Refactor process), `design-an-interface` (interface design)
|
||
|
|
- **Follow-up**: `qa` (QA testing)
|
||
|
|
|
||
|
|
## Rollback Mechanism
|
||
|
|
|
||
|
|
```
|
||
|
|
QA failed (Stage 10)
|
||
|
|
↓
|
||
|
|
Orchestrator re-assigns fix task
|
||
|
|
↓
|
||
|
|
Backend Agent fixes bug + adds regression test
|
||
|
|
↓
|
||
|
|
Re-enter QA (Stage 10)
|
||
|
|
|
||
|
|
Code Review rejected (Stage 10)
|
||
|
|
↓
|
||
|
|
Handle PR feedback
|
||
|
|
↓
|
||
|
|
Re-enter QA (Stage 10) for verification
|
||
|
|
|
||
|
|
Implementation plan not feasible
|
||
|
|
↓
|
||
|
|
Return to Task Breakdown (Stage 8) for re-decomposition
|