814 lines
24 KiB
Markdown
814 lines
24 KiB
Markdown
---
|
||
name: go-backend-dev
|
||
description: "Backend Agent 使用此技能實作 Golang 後端。根據實作計畫和 API 規格,使用 Domain-Driven + go-zero 風格架構和 TDD 流程產出 production-ready 程式碼。觸發時機:Task Breakdown 完成後(Stage 9)。"
|
||
---
|
||
|
||
# /go-backend-dev — Golang 後端實作
|
||
|
||
Backend Agent 使用此技能實作 Golang 後端。
|
||
|
||
## 職責
|
||
|
||
1. 根據實作計畫建立專案結構(Domain-Driven + go-zero 風格)
|
||
2. 使用 TDD 流程實作功能(Red-Green-Refactor)
|
||
3. 按垂直切片逐步交付(端到端,非逐層)
|
||
4. 實作 Domain / Usecase / Logic / Repository 各層
|
||
5. 撰寫單元測試和整合測試
|
||
|
||
## 輸入
|
||
|
||
- 實作計畫 (`./plans/{feature}.md`)
|
||
- API 規格 (`docs/api/{date}-{feature}.yaml`)
|
||
- DB Schema (`docs/db/{date}-{feature}.sql`)
|
||
|
||
## 輸出
|
||
|
||
- Golang 程式碼結構
|
||
- 測試程式碼(單元測試 >= 80%,業務邏輯 >= 90%)
|
||
- Protobuf 定義(如需 gRPC)
|
||
|
||
## 流程
|
||
|
||
```
|
||
讀取實作計畫 + API 規格 + DB Schema
|
||
↓
|
||
識別垂直切片(每個切片 = 端到端功能)
|
||
↓
|
||
對每個切片執行 TDD 循環:
|
||
├── RED: 寫測試 → 測試失敗
|
||
├── GREEN: 寫最少程式碼 → 測試通過
|
||
└── REFACTOR: 重構 → 測試仍然通過
|
||
↓
|
||
切片內建構順序:
|
||
domain (entity/value object/interface)
|
||
→ pkg/domain/usecase (介面)
|
||
→ pkg/domain/repository (介面)
|
||
→ pkg/usecase (實作)
|
||
→ pkg/mock (mock)
|
||
→ internal/logic (handler 邏輯)
|
||
→ pkg/repository (基礎設施實作)
|
||
↓
|
||
所有切片完成 → 執行整合測試
|
||
↓
|
||
確認交付物檢查清單
|
||
```
|
||
|
||
### 步驟說明
|
||
|
||
**1. 讀取輸入**
|
||
|
||
同時閱讀三個文件:
|
||
- 實作計畫:了解垂直切片分解和優先順序
|
||
- API 規格:了解端點、請求/回應結構
|
||
- DB Schema:了解資料表結構和關係
|
||
|
||
**2. 識別垂直切片**
|
||
|
||
不是水平切片(一層一層做),而是垂直切片(端到端):
|
||
|
||
```
|
||
✅ 正確方式(垂直):
|
||
切片 1: 使用者註冊 (domain.entity + domain.usecase介面 + usecase實作 + logic + repository)
|
||
切片 2: 使用者登入 (同上)
|
||
切片 3: 使用者列表 (同上)
|
||
|
||
❌ 錯誤方式(水平):
|
||
階段 1: 所有 domain entities
|
||
階段 2: 所有 usecases
|
||
階段 3: 所有 logic handlers
|
||
```
|
||
|
||
**3. TDD 循環(每個切片)**
|
||
|
||
對每個切片,遵循 Red-Green-Refactor:
|
||
|
||
```
|
||
RED: 寫一個測試 → 測試失敗
|
||
GREEN: 寫最少的程式碼讓測試通過 → 測試通過
|
||
REFACTOR: 重構程式碼 → 測試仍然通過
|
||
```
|
||
|
||
切片內建構順序(由內而外):
|
||
1. `pkg/domain/entity/` — 定義 Entity 和 Value Object
|
||
2. `pkg/domain/member/` — 定義值物件和列舉(含測試)
|
||
3. `pkg/domain/usecase/` — 定義 Use Case 介面
|
||
4. `pkg/domain/repository/` — 定義 Repository 介面
|
||
5. `pkg/usecase/` — 實作業務邏輯(先寫測試)
|
||
6. `pkg/mock/` — 產生 mock
|
||
7. `internal/logic/` — Handler 邏輯
|
||
8. `pkg/repository/` — 基礎設施實作(含 DB 測試)
|
||
|
||
**4. 測試**
|
||
|
||
每個切片完成後,確保:
|
||
- 單元測試通過(`pkg/usecase/*_test.go`)
|
||
- 值物件測試通過(`pkg/domain/member/*_test.go`)
|
||
- Repository 測試通過(`pkg/repository/*_test.go`)
|
||
- 整合測試通過(關鍵路徑)
|
||
- 測試覆蓋率達標
|
||
|
||
**5. 完成驗證**
|
||
|
||
最後確認所有交付物完整。
|
||
|
||
## 專案結構
|
||
|
||
```
|
||
project-root/
|
||
├── build/
|
||
│ └── Dockerfile # 建置映像
|
||
│
|
||
├── etc/
|
||
│ └── {service}.example.yaml # 範例設定檔
|
||
│
|
||
├── generate/
|
||
│ └── protobuf/
|
||
│ └── {service}.proto # Protobuf 定義(如需 gRPC)
|
||
│
|
||
├── internal/ # 應用層(不對外暴露)
|
||
│ ├── config/
|
||
│ │ └── config.go # 應用配置
|
||
│ ├── logic/
|
||
│ │ └── {module}/
|
||
│ │ ├── create_{entity}_logic.go # 每個 use case 一個 logic 檔案
|
||
│ │ ├── get_{entity}_logic.go
|
||
│ │ ├── update_{entity}_logic.go
|
||
│ │ └── ...
|
||
│ ├── server/
|
||
│ │ └── {module}/
|
||
│ │ └── {module}_server.go # Server 定義(HTTP/gRPC)
|
||
│ └── svc/
|
||
│ └── service_context.go # 依賴注入容器
|
||
│
|
||
├── pkg/ # 領域層(可對外暴露)
|
||
│ ├── domain/
|
||
│ │ ├── config/
|
||
│ │ │ └── config.go # Domain 配置
|
||
│ │ ├── entity/
|
||
│ │ │ ├── {entity}.go # Entity 定義
|
||
│ │ │ ├── {entity}_uid_table.go # UID 對照表
|
||
│ │ │ └── auto_id.go # 自動 ID 產生
|
||
│ │ ├── {module}/
|
||
│ │ │ ├── {value_object}.go # 值物件和列舉
|
||
│ │ │ └── {value_object}_test.go # 值物件測試
|
||
│ │ ├── repository/
|
||
│ │ │ ├── {entity}.go # Repository 介面
|
||
│ │ │ └── ...
|
||
│ │ ├── usecase/
|
||
│ │ │ ├── {module}.go # Use Case 介面
|
||
│ │ │ └── ...
|
||
│ │ ├── errors.go # Domain sentinel errors
|
||
│ │ ├── const.go # Domain 常數
|
||
│ │ └── redis.go # Redis domain 定義
|
||
│ ├── mock/
|
||
│ │ ├── repository/
|
||
│ │ │ ├── {entity}.go # Repository mock
|
||
│ │ │ └── ...
|
||
│ │ └── usecase/
|
||
│ │ └── {module}.go # Use Case mock
|
||
│ ├── repository/
|
||
│ │ ├── {entity}.go # Repository 實作
|
||
│ │ ├── {entity}_test.go # Repository 測試
|
||
│ │ ├── {entity}_uid.go # UID Repository 實作
|
||
│ │ ├── {entity}_uid_test.go
|
||
│ │ ├── error.go # Repository 錯誤定義
|
||
│ │ └── start_{db}_container_test.go # testcontainers 啟動
|
||
│ └── usecase/
|
||
│ ├── {module}.go # Use Case 實作
|
||
│ ├── {operation}.go # 特定操作
|
||
│ ├── {operation}_test.go # Use Case 測試
|
||
│ └── {utils}.go # 工具函式
|
||
│
|
||
├── {service}.go # 應用程式進入點
|
||
├── Makefile
|
||
├── go.mod
|
||
├── go.sum
|
||
├── docker-compose.yml
|
||
└── readme.md
|
||
```
|
||
|
||
## 依賴方向規則
|
||
|
||
```
|
||
pkg/domain/ ← 無外部依賴(最內層,純定義)
|
||
↑
|
||
pkg/domain/usecase/ ← Use Case 介面(只有介面定義)
|
||
pkg/domain/repository/ ← Repository 介面(只有介面定義)
|
||
↑
|
||
pkg/usecase/ ← 依賴 domain 介面(業務邏輯實作)
|
||
pkg/mock/ ← 依賴 domain 介面(測試 mock)
|
||
↑
|
||
internal/logic/ ← 依賴 usecase 實作(handler 邏輯)
|
||
internal/server/ ← 依賴 logic(HTTP/gRPC server)
|
||
internal/svc/ ← 依賴所有(DI 容器,組裝依賴)
|
||
↑
|
||
pkg/repository/ ← 依賴 domain 介面(基礎設施實作)
|
||
```
|
||
|
||
```
|
||
┌─────────────────────────────────┐
|
||
│ pkg/domain/ │ ← 純定義,無依賴
|
||
│ ├── entity/ │
|
||
│ ├── {module}/ (value objects)│
|
||
│ ├── repository/ (interfaces) │
|
||
│ ├── usecase/ (interfaces) │
|
||
│ ├── errors.go │
|
||
│ └── const.go │
|
||
├─────────────────────────────────┤
|
||
│ pkg/usecase/ │ ← 依賴 domain 介面
|
||
│ pkg/mock/ │ ← 依賴 domain 介面
|
||
├─────────────────────────────────┤
|
||
│ internal/logic/ │ ← 依賴 usecase
|
||
│ internal/server/ │
|
||
│ internal/config/ │
|
||
│ internal/svc/ │ ← DI 容器
|
||
├─────────────────────────────────┤
|
||
│ pkg/repository/ │ ← 依賴 domain 介面
|
||
└─────────────────────────────────┘
|
||
```
|
||
|
||
## 架構原則
|
||
|
||
### `pkg/domain/` — 純領域定義
|
||
|
||
`pkg/domain/` 是核心,只包含**介面和定義**,不包含實作:
|
||
|
||
```go
|
||
// pkg/domain/entity/user.go — Entity 定義
|
||
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 — 值物件(含測試)
|
||
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 — 值物件測試
|
||
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 介面
|
||
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 介面
|
||
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/` — 業務邏輯實作
|
||
|
||
每個檔案一個功能領域,測試檔案同目錄:
|
||
|
||
```go
|
||
// pkg/usecase/account.go — Use Case 進入點
|
||
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 — 單一操作
|
||
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 — 測試同目錄
|
||
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/` — 自動產生的 Mock
|
||
|
||
```go
|
||
// pkg/mock/repository/user.go — 由 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 邏輯
|
||
|
||
每個 use case 一個 logic 檔案(go-zero 風格):
|
||
|
||
```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/` — 依賴注入容器
|
||
|
||
```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/` — 基礎設施實作
|
||
|
||
```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)
|
||
// ...
|
||
}
|
||
```
|
||
|
||
## 編碼規範
|
||
|
||
### 檔案命名
|
||
|
||
```
|
||
值物件和列舉: pkg/domain/{module}/{name}.go + _test.go
|
||
Entity: pkg/domain/entity/{name}.go
|
||
Repository 介面:pkg/domain/repository/{name}.go
|
||
Usecase 介面: pkg/domain/usecase/{module}.go
|
||
Usecase 實作: pkg/usecase/{operation}.go + _test.go
|
||
Usecase 工具: pkg/usecase/{module}_utils.go + _test.go
|
||
Repository 實作:pkg/repository/{name}.go + _test.go
|
||
Mock: pkg/mock/repository/{name}.go
|
||
pkg/mock/usecase/{module}.go
|
||
Handler 邏輯: internal/logic/{module}/{operation}_logic.go
|
||
Server: internal/server/{module}/{module}_server.go
|
||
Service Context:internal/svc/service_context.go
|
||
Protobuf 定義: generate/protobuf/{module}.proto
|
||
設定檔: etc/{service}.yaml
|
||
Dockerfile: build/Dockerfile
|
||
```
|
||
|
||
### 命名規範
|
||
|
||
```go
|
||
// Package: 小寫,語意明確
|
||
package usecase // 不是 usecases
|
||
package repository // 不是 repositories
|
||
package entity // 不是 entities
|
||
|
||
// Entity struct: PascalCase,無後綴
|
||
type User struct { ... } // 不是 UserModel, UserEntity
|
||
|
||
// Value Object: 基礎型別別名 + 方法
|
||
type Status string // 不是 StatusEnum
|
||
|
||
// Interface (介面): 放在 pkg/domain/ 下,語意命名
|
||
type UserRepository interface { ... } // 不是 UserRepo 或 UserRepositoryI
|
||
|
||
// Use Case struct: {Module}Usecase
|
||
type AccountUsecase struct { ... }
|
||
|
||
// Use Case 方法: 動詞開頭
|
||
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 前綴
|
||
var ErrUserNotFound = errors.New("user not found")
|
||
|
||
// Constant: PascalCase(exported)或 camelCase(internal)
|
||
const MaxRetryCount = 3
|
||
const defaultPageSize = 20
|
||
```
|
||
|
||
### 錯誤處理
|
||
|
||
```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")
|
||
)
|
||
```
|
||
|
||
### 介面設計
|
||
|
||
```go
|
||
// 介面定義在 pkg/domain/(消費者端)
|
||
// 實作定義在 pkg/ 下(提供者端)
|
||
|
||
// 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 規範
|
||
|
||
此技能與 `tdd` 技能整合,遵循共同的 TDD 原則。
|
||
|
||
### 測試金字塔
|
||
|
||
```
|
||
/\
|
||
/ \
|
||
/ E2E \ <- 少數關鍵流程
|
||
/--------\
|
||
/Integration\ <- DB + Redis (testcontainers)
|
||
/--------------\
|
||
/ Unit Tests \ <- 最多,80%+ 覆蓋
|
||
/--------------------\
|
||
```
|
||
|
||
### 測試位置
|
||
|
||
```
|
||
測試檔案跟原始碼同目錄:
|
||
|
||
pkg/domain/member/status_test.go ← 值物件測試
|
||
pkg/usecase/create_user_test.go ← Use Case 測試
|
||
pkg/repository/user_test.go ← Repository 測試
|
||
pkg/repository/start_mongo_container_test.go ← testcontainers 啟動
|
||
```
|
||
|
||
### 垂直切片 TDD
|
||
|
||
每個切片按照以下順序:
|
||
|
||
```
|
||
切片: 使用者註冊
|
||
1. RED: 寫 TestStatus_IsValid (值物件)
|
||
2. GREEN: 寫 Status.IsValid()
|
||
3. RED: 寫 TestAccountUsecase_CreateUser_Success
|
||
4. GREEN: 寫 domain/entity, domain/usecase介面, pkg/usecase實作, mock
|
||
5. RED: 寫 TestAccountUsecase_CreateUser_DuplicateEmail
|
||
6. GREEN: 加入重複檢查
|
||
7. RED: 寫 TestUserRepository_Create (DB 測試)
|
||
8. GREEN: 寫 pkg/repository/user.go
|
||
9. RED: 寫 TestCreateUserLogic (handler 測試)
|
||
10. GREEN: 寫 internal/logic/account/create_user_logic.go
|
||
11. REFACTOR: 清理全部
|
||
```
|
||
|
||
### Mock 策略
|
||
|
||
```go
|
||
// 使用 mockery 自動產生 mock
|
||
// 在接口檔案加上 go:generate 指令
|
||
//go:generate mockery --name=UserRepository --output=../../mock/repository --outpkg=mock_repository
|
||
|
||
// 單元測試使用 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 策略
|
||
|
||
```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
|
||
}
|
||
```
|
||
|
||
### 覆蓋率要求
|
||
|
||
- 值物件 (`pkg/domain/member/`): >= 90%
|
||
- Use Case (`pkg/usecase/`): >= 90%
|
||
- Repository (`pkg/repository/`): >= 80%
|
||
- Logic (`internal/logic/`): >= 80%(整合測試為主)
|
||
- Critical paths: Integration tests required
|
||
|
||
## 垂直切片模板
|
||
|
||
每個垂直切片的檔案清單:
|
||
|
||
```
|
||
切片: {operation}_{entity}
|
||
|
||
新增/修改的檔案:
|
||
├── pkg/domain/entity/{entity}.go ← Entity 定義
|
||
├── pkg/domain/member/{value_object}.go ← 值物件(如需)
|
||
├── pkg/domain/member/{value_object}_test.go ← 值物件測試
|
||
├── pkg/domain/repository/{entity}.go ← Repository 介面
|
||
├── pkg/domain/usecase/{module}.go ← Use Case 介面
|
||
├── pkg/usecase/{operation}.go ← Use Case 實作
|
||
├── pkg/usecase/{operation}_test.go ← Use Case 測試
|
||
├── pkg/mock/repository/{entity}.go ← Repository mock
|
||
├── pkg/repository/{entity}.go ← Repository 實作
|
||
├── pkg/repository/{entity}_test.go ← Repository 測試
|
||
├── internal/logic/{module}/{operation}_logic.go ← Handler 邏輯
|
||
└── internal/svc/service_context.go ← 更新 DI
|
||
```
|
||
|
||
## 完成檢查清單
|
||
|
||
### 每個切片完成後
|
||
- [ ] 值物件測試通過
|
||
- [ ] Use Case 測試通過
|
||
- [ ] Repository 測試通過(含 DB)
|
||
- [ ] 錯誤處理完整
|
||
- [ ] 依賴方向正確(domain 無外部依賴)
|
||
|
||
### 全部完成後
|
||
- [ ] 專案結構符合 Domain-Driven + go-zero 風格
|
||
- [ ] `pkg/domain/` 包含所有 Entity、Value Object、介面定義
|
||
- [ ] `pkg/usecase/` 包含所有業務邏輯實作
|
||
- [ ] `pkg/repository/` 包含所有基礎設施實作
|
||
- [ ] `internal/logic/` 包含所有 Handler 邏輯
|
||
- [ ] `internal/svc/` 包含完整的依賴注入設定
|
||
- [ ] 單元測試 >= 80% 覆蓋率
|
||
- [ ] 業務邏輯 >= 90% 覆蓋率
|
||
- [ ] 整合測試通過(關鍵路徑)
|
||
- [ ] 錯誤處理一致且使用 `%w` wrapping
|
||
|
||
## 相依技能
|
||
|
||
- **前置**: `prd-to-plan` (實作計畫), `be-api-design` (API 規格), `dba-schema` (DB Schema)
|
||
- **輔助**: `tdd` (TDD Red-Green-Refactor 流程), `design-an-interface` (介面設計)
|
||
- **後續**: `qa` (QA 測試)
|
||
|
||
## 退回機制
|
||
|
||
```
|
||
QA 失敗 (Stage 10)
|
||
↓
|
||
Orchestrator 重新分配修復任務
|
||
↓
|
||
Backend Agent 修復 Bug + 新增回歸測試
|
||
↓
|
||
重新進入 QA (Stage 10)
|
||
|
||
Code Review 退回 (Stage 11)
|
||
↓
|
||
處理 PR 回饋
|
||
↓
|
||
重新進入 QA (Stage 10) 驗證
|
||
|
||
實作計畫不可行
|
||
↓
|
||
退回 Task Breakdown (Stage 8) 重新分解
|
||
```
|