Goアプリケーションセットアップ手順¶
1. 概要¶
この文書では、Go-DDDマーケットプレイスアプリケーションのバックエンド部分(app/backend)におけるGoアプリケーションのセットアップ手順と必要なモジュールの設定方法について説明します。このセットアップ手順に従うことで、ドメイン駆動設計(DDD)の原則に基づいたGoアプリケーションの開発環境を構築することができます。
1.1 アプリケーション構造¶
以下はGoアプリケーションの基本構造を表すコンポーネント図です:
1.2 セットアッププロセス¶
以下はGoアプリケーションのセットアッププロセスを表すアクティビティ図です:
2. セットアップ手順¶
2.1 前提条件¶
Goアプリケーションをセットアップする前に、以下のツールがインストールされていることを確認してください:
- Go 1.23.0以上: 最新のGo言語環境
- Docker: コンテナ化されたデータベースなどの依存サービスを実行するため
- Git: バージョン管理のため
2.2 プロジェクト構造の作成¶
以下のコマンドを実行して、プロジェクト構造を作成します:
mkdir -p app/backend/{cmd/{marketplace,gen},internal/{domain,application,infrastructure,interface},docs,features}
この構造は、ドメイン駆動設計(DDD)の原則に基づいており、以下のディレクトリが含まれています:
- cmd: アプリケーションのエントリーポイント
- marketplace: メインアプリケーション
- gen: コード生成ツール
- internal: 内部パッケージ
- domain: ドメインモデル(エンティティ、値オブジェクト、ドメインサービス)
- application: アプリケーションサービス(ユースケース)
- infrastructure: インフラストラクチャ(リポジトリの実装、外部サービスとの連携)
- interface: インターフェース(コントローラー、プレゼンター)
- docs: Swaggerドキュメント
- features: Cucumberテスト機能
2.3 Go Modulesの初期化¶
プロジェクトディレクトリに移動し、Go Modulesを初期化します:
cd app/backend
go mod init github.com/sklinkert/go-ddd
2.4 必要なパッケージのインストール¶
以下のコマンドを実行して、必要なパッケージをインストールします:
# Webフレームワーク
go get github.com/labstack/echo/v4
# ORM
go get github.com/jinzhu/gorm
go get gorm.io/gorm
go get gorm.io/driver/postgres
go get gorm.io/driver/sqlite
# コード生成
go get gorm.io/gen
go get gorm.io/plugin/dbresolver
# UUID生成
go get github.com/google/uuid
# Swagger
go get github.com/swaggo/swag/cmd/swag
go get github.com/swaggo/echo-swagger
# テスト
go get github.com/stretchr/testify
go get github.com/cucumber/godog
go get github.com/testcontainers/testcontainers-go
2.5 データベース設定¶
PostgreSQLデータベースを使用するための設定を行います。docker-compose.ymlファイルを作成または編集して、以下の内容を追加します:
version: '3'
services:
postgres:
image: postgres:13
environment:
POSTGRES_USER: root
POSTGRES_PASSWORD: password
POSTGRES_DB: mydb
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
volumes:
postgres_data:
2.6 アプリケーション構造の実装¶
2.6.1 ドメインモデルの実装¶
ドメインモデル(エンティティ、値オブジェクト)を実装します。例えば、Product(商品)エンティティを作成します:
// internal/domain/entities/product.go
package entities
import (
"time"
)
type Product struct {
Id string
Name string
Description string
Price float64
SellerId string
CreatedAt time.Time
UpdatedAt time.Time
}
2.6.2 リポジトリインターフェースの定義¶
ドメインリポジトリのインターフェースを定義します:
// internal/domain/repositories/product_repository.go
package repositories
import (
"github.com/sklinkert/go-ddd/internal/domain/entities"
)
type ProductRepository interface {
FindAll() ([]entities.Product, error)
FindById(id string) (*entities.Product, error)
Create(product *entities.Product) error
Update(product *entities.Product) error
Delete(id string) error
}
2.6.3 リポジトリの実装¶
リポジトリの実装を作成します:
// internal/infrastructure/db/postgres/product_repository.go
package postgres
import (
"github.com/sklinkert/go-ddd/internal/domain/entities"
"gorm.io/gorm"
"time"
)
type DBProduct struct {
Id string `gorm:"primaryKey"`
Name string
Description string
Price float64
SellerId string
CreatedAt time.Time
UpdatedAt time.Time
}
type GormProductRepository struct {
db *gorm.DB
}
func NewGormProductRepository(db *gorm.DB) *GormProductRepository {
return &GormProductRepository{db: db}
}
func (r *GormProductRepository) FindAll() ([]entities.Product, error) {
var dbProducts []DBProduct
result := r.db.Find(&dbProducts)
if result.Error != nil {
return nil, result.Error
}
products := make([]entities.Product, len(dbProducts))
for i, dbProduct := range dbProducts {
products[i] = toDomainProduct(dbProduct)
}
return products, nil
}
// 他のメソッドも同様に実装
2.6.4 アプリケーションサービスの実装¶
アプリケーションサービス(ユースケース)を実装します:
// internal/application/services/product_service.go
package services
import (
"github.com/sklinkert/go-ddd/internal/domain/entities"
"github.com/sklinkert/go-ddd/internal/domain/repositories"
"github.com/google/uuid"
"time"
)
type ProductService struct {
productRepo repositories.ProductRepository
sellerRepo repositories.SellerRepository
}
func NewProductService(productRepo repositories.ProductRepository, sellerRepo repositories.SellerRepository) *ProductService {
return &ProductService{
productRepo: productRepo,
sellerRepo: sellerRepo,
}
}
func (s *ProductService) GetAllProducts() ([]entities.Product, error) {
return s.productRepo.FindAll()
}
func (s *ProductService) CreateProduct(product *entities.Product) error {
product.Id = uuid.New().String()
product.CreatedAt = time.Now()
product.UpdatedAt = time.Now()
return s.productRepo.Create(product)
}
// 他のメソッドも同様に実装
2.6.5 コントローラーの実装¶
RESTful APIコントローラーを実装します:
// internal/interface/api/rest/product_controller.go
package rest
import (
"github.com/labstack/echo/v4"
"github.com/sklinkert/go-ddd/internal/application/services"
"github.com/sklinkert/go-ddd/internal/domain/entities"
"net/http"
)
type ProductController struct {
productService *services.ProductService
}
func NewProductController(e *echo.Echo, productService *services.ProductService) {
controller := &ProductController{
productService: productService,
}
e.GET("/api/v1/products", controller.GetAllProducts)
e.POST("/api/v1/products", controller.CreateProduct)
// 他のエンドポイントも同様に設定
}
// @Summary 全ての商品を取得
// @Description 全ての商品のリストを取得します
// @Tags products
// @Accept json
// @Produce json
// @Success 200 {array} entities.Product
// @Router /api/v1/products [get]
func (c *ProductController) GetAllProducts(ctx echo.Context) error {
products, err := c.productService.GetAllProducts()
if err != nil {
return ctx.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
}
return ctx.JSON(http.StatusOK, products)
}
// 他のハンドラーも同様に実装
2.6.6 メインアプリケーションの実装¶
メインアプリケーションのエントリーポイントを実装します:
// cmd/marketplace/main.go
// @title Marketplace API
// @version 1.0
// @description This is a marketplace API server.
// @host localhost:9090
// @BasePath /api/v1
package main
import (
_ "github.com/jinzhu/gorm/dialects/postgres"
"github.com/labstack/echo/v4"
_ "github.com/sklinkert/go-ddd/docs" // Swaggerドキュメントのインポート
"github.com/sklinkert/go-ddd/internal/application/services"
postgres2 "github.com/sklinkert/go-ddd/internal/infrastructure/db/postgres"
"github.com/sklinkert/go-ddd/internal/interface/api/rest"
echoSwagger "github.com/swaggo/echo-swagger"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"log"
)
func main() {
dsn := "host=localhost user=root password=password dbname=mydb port=5432 sslmode=disable TimeZone=Asia/Shanghai"
port := ":9090"
gormDB, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
// データベースマイグレーション
err = gormDB.AutoMigrate()
if err != nil {
log.Fatalf("Failed to migrate database: %v", err)
}
// リポジトリの初期化
productRepo := postgres2.NewGormProductRepository(gormDB)
sellerRepo := postgres2.NewGormSellerRepository(gormDB)
// サービスの初期化
productService := services.NewProductService(productRepo, sellerRepo)
sellerService := services.NewSellerService(sellerRepo)
// Echoサーバーの初期化
e := echo.New()
// Swagger UIのエンドポイントを設定
e.GET("/swagger/*", echoSwagger.WrapHandler)
// コントローラーの初期化
rest.NewProductController(e, productService)
rest.NewSellerController(e, sellerService)
// サーバーの起動
if err := e.Start(port); err != nil {
log.Fatalf("Failed to start server: %v", err)
}
}
2.7 Swaggerドキュメントの設定¶
Swaggerドキュメントを生成するためのツールをセットアップします:
// cmd/gen/generate_swagger.go
package main
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"runtime"
)
func main() {
// バックエンドディレクトリへのパスを取得
currentDir, err := os.Getwd()
if err != nil {
log.Fatalf("Failed to get current directory: %v", err)
}
// スクリプトがapp/backend/cmd/genにあると仮定
backendDir := filepath.Join(currentDir, "app", "backend")
// すでにバックエンドディレクトリにいるかチェック
if _, err := os.Stat(filepath.Join(currentDir, "cmd", "marketplace", "main.go")); err == nil {
// すでにバックエンドディレクトリにいる
backendDir = currentDir
} else if _, err := os.Stat(filepath.Join(currentDir, "app", "backend", "cmd", "marketplace", "main.go")); err == nil {
// プロジェクトルートにいる
backendDir = filepath.Join(currentDir, "app", "backend")
} else if _, err := os.Stat(filepath.Join(currentDir, "..", "..", "cmd", "marketplace", "main.go")); err == nil {
// app/backend/cmd/genにいる
backendDir = filepath.Join(currentDir, "..", "..")
}
// バックエンドディレクトリに移動
err = os.Chdir(backendDir)
if err != nil {
log.Fatalf("Failed to change directory to %s: %v", backendDir, err)
}
log.Printf("Swaggerドキュメントを生成中...")
log.Printf("API情報を生成中、検索ディレクトリ:%s", backendDir)
// OSに応じたスクリプトを作成して実行
isWindows := runtime.GOOS == "windows"
var cmd *exec.Cmd
var scriptPath string
if isWindows {
// Windows用のPowerShellスクリプト
scriptPath = filepath.Join(os.TempDir(), "run_swag.ps1")
scriptContent := `
$env:PATH = "$env:PATH;$(go env GOPATH)\bin"
go install github.com/swaggo/swag/cmd/swag@latest
swag init -g cmd/marketplace/main.go -o docs
`
err = os.WriteFile(scriptPath, []byte(scriptContent), 0755)
if err != nil {
log.Fatalf("Failed to create temporary script: %v", err)
}
defer os.Remove(scriptPath)
// PowerShellスクリプトを実行
cmd = exec.Command("powershell", "-ExecutionPolicy", "Bypass", "-File", scriptPath)
} else {
// Unix用のシェルスクリプト
scriptPath = filepath.Join(os.TempDir(), "run_swag.sh")
scriptContent := `#!/bin/bash
export PATH=$PATH:$(go env GOPATH)/bin
go install github.com/swaggo/swag/cmd/swag@latest
swag init -g cmd/marketplace/main.go -o docs
`
err = os.WriteFile(scriptPath, []byte(scriptContent), 0755)
if err != nil {
log.Fatalf("Failed to create temporary script: %v", err)
}
defer os.Remove(scriptPath)
// シェルスクリプトを実行
cmd = exec.Command("/bin/bash", scriptPath)
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
// PATH環境変数を適切に設定
goPath := filepath.Join(os.Getenv("HOME"), "go", "bin")
if isWindows {
cmd.Env = append(os.Environ(), fmt.Sprintf("PATH=%s;%s", os.Getenv("PATH"), goPath))
} else {
cmd.Env = append(os.Environ(), fmt.Sprintf("PATH=%s:%s", os.Getenv("PATH"), goPath))
}
log.Println("Swaggerドキュメントを生成中...")
err = cmd.Run()
if err != nil {
log.Fatalf("Swaggerドキュメントの生成に失敗しました: %v", err)
}
log.Println("Swaggerドキュメントが正常に生成されました!")
}
Swaggerドキュメントを生成するには、以下のコマンドを実行します:
cd app/backend
go run cmd/gen/generate_swagger.go
2.8 テスト環境の構築¶
2.8.1 ユニットテストの実装¶
ユニットテストを実装します:
// internal/domain/entities/product_test.go
package entities
import (
"testing"
"time"
)
func TestProduct(t *testing.T) {
product := Product{
Id: "test-id",
Name: "Test Product",
Description: "This is a test product",
Price: 100.0,
SellerId: "seller-id",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
if product.Id != "test-id" {
t.Errorf("Expected Id to be 'test-id', got '%s'", product.Id)
}
if product.Name != "Test Product" {
t.Errorf("Expected Name to be 'Test Product', got '%s'", product.Name)
}
if product.Price != 100.0 {
t.Errorf("Expected Price to be 100.0, got '%f'", product.Price)
}
}
2.8.2 統合テストの実装¶
Testcontainersを使用してサービスの統合テストを実装します:
// internal/application/services/product_service_integration_test.go
package services
import (
"context"
"fmt"
"testing"
"github.com/google/uuid"
"github.com/sklinkert/go-ddd/internal/application/command"
"github.com/sklinkert/go-ddd/internal/application/common"
"github.com/sklinkert/go-ddd/internal/application/interfaces"
"github.com/sklinkert/go-ddd/internal/infrastructure/db/postgres"
"github.com/stretchr/testify/assert"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
pgdriver "gorm.io/driver/postgres"
"gorm.io/gorm"
"time"
)
func setupTestDatabase(t *testing.T) (*gorm.DB, func()) {
ctx := context.Background()
// Define PostgreSQL container
pgReq := testcontainers.ContainerRequest{
Image: "postgres:15-alpine",
ExposedPorts: []string{"5432/tcp"},
Env: map[string]string{
"POSTGRES_USER": "postgres",
"POSTGRES_PASSWORD": "postgres",
"POSTGRES_DB": "testdb",
},
WaitingFor: wait.ForLog("database system is ready to accept connections").
WithOccurrence(2).WithStartupTimeout(5 * time.Second),
}
// Start PostgreSQL container
pgContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: pgReq,
Started: true,
})
if err != nil {
t.Fatalf("Failed to start PostgreSQL container: %s", err)
}
// Get container host and port
host, err := pgContainer.Host(ctx)
if err != nil {
t.Fatalf("Failed to get PostgreSQL container host: %s", err)
}
port, err := pgContainer.MappedPort(ctx, "5432")
if err != nil {
t.Fatalf("Failed to get PostgreSQL container port: %s", err)
}
// Create connection string
dsn := fmt.Sprintf("host=%s port=%s user=postgres password=postgres dbname=testdb sslmode=disable", host, port.Port())
// Connect to the PostgreSQL database
database, err := gorm.Open(pgdriver.Open(dsn), &gorm.Config{})
if err != nil {
t.Fatalf("Failed to connect to database: %s", err)
}
// AutoMigrate our models
err = database.AutoMigrate(&postgres.Product{}, &postgres.Seller{})
if err != nil {
t.Fatalf("Failed to migrate database: %s", err)
}
// Cleanup function
cleanup := func() {
// Clean up database
database.Exec("DELETE FROM products")
database.Exec("DELETE FROM sellers")
// Stop and remove PostgreSQL container
if err := pgContainer.Terminate(ctx); err != nil {
t.Fatalf("Failed to terminate container: %s", err)
}
}
return database, cleanup
}
func createTestSeller(t *testing.T, sellerService interfaces.SellerService) *common.SellerResult {
sellerName := "Test Seller " + uuid.New().String()
result, err := sellerService.CreateSeller(&command.CreateSellerCommand{
Name: sellerName,
})
assert.NoError(t, err)
assert.NotNil(t, result)
assert.NotNil(t, result.Result)
assert.Equal(t, sellerName, result.Result.Name)
return result.Result
}
func TestProductService_Integration_CreateProduct(t *testing.T) {
// Setup test database with Testcontainers
db, cleanup := setupTestDatabase(t)
defer cleanup()
// Create repositories
productRepo := postgres.NewGormProductRepository(db)
sellerRepo := postgres.NewGormSellerRepository(db)
// Create services
productService := NewProductService(productRepo, sellerRepo)
sellerService := NewSellerService(sellerRepo)
// Create a seller first
seller := createTestSeller(t, sellerService)
// Test creating a product
productName := "Test Product"
productPrice := 99.99
createProductCmd := &command.CreateProductCommand{
Name: productName,
Price: productPrice,
SellerId: seller.Id,
}
result, err := productService.CreateProduct(createProductCmd)
assert.NoError(t, err)
assert.NotNil(t, result)
assert.NotNil(t, result.Result)
assert.Equal(t, productName, result.Result.Name)
assert.Equal(t, productPrice, result.Result.Price)
assert.Equal(t, seller.Id, result.Result.Seller.Id)
}
2.8.3 受け入れテストの実装¶
Cucumberを使用した受け入れテストを実装します。日本語でフィーチャを記述するには、ファイルの先頭に # language: ja を追加します:
# features/product/product_controller.feature
# language: ja
フィーチャ: 商品コントローラーAPI
APIを通じて商品を管理するために
クライアントとして
HTTPリクエストを介して商品の作成、読み取り、更新、削除ができる必要があります
シナリオ: APIを介して新しい商品を作成する
前提 APIのための商品詳細を持っています
| 名前 | 価格 | 出品者ID |
| テスト商品 | 10.99 | 00000000-0000-0000-0000-000000000001 |
もし 商品詳細を含めて"/api/v1/products"にPOSTリクエストを送信します
ならば レスポンスステータスコードは201であるべきです
かつ レスポンスは作成された商品詳細を含むべきです
シナリオ: APIを介してすべての商品を取得する
前提 システムに商品があります
もし "/api/v1/products"にGETリクエストを送信します
ならば レスポンスステータスコードは200であるべきです
かつ レスポンスは商品のリストを含むべきです
シナリオ: APIを介してIDで商品を取得する
前提 システムにID "00000000-0000-0000-0000-000000000001"の商品があります
もし "/api/v1/products/00000000-0000-0000-0000-000000000001"にGETリクエストを送信します
ならば レスポンスステータスコードは200であるべきです
かつ レスポンスは商品詳細を含むべきです
コントローラーを使用したステップ実装の例:
// features/steps/controller_steps.go
package steps
import (
"bytes"
"encoding/json"
"fmt"
"github.com/cucumber/godog"
"github.com/labstack/echo/v4"
"net/http"
"net/http/httptest"
"strconv"
)
// ControllerContext はコントローラー関連のステップの状態を保持します
type ControllerContext struct {
echoInstance *echo.Echo
requestBody map[string]interface{}
response *httptest.ResponseRecorder
}
// RegisterSteps はgodogスイートにコントローラーステップを登録します
func (c *ControllerContext) RegisterSteps(ctx *godog.ScenarioContext) {
// 日本語ステップ定義
ctx.Step(`^APIのための商品詳細を持っています$`, c.iHaveProductDetailsForAPI)
ctx.Step(`^商品詳細を含めて"([^"]*)"にPOSTリクエストを送信します$`, c.iSendAPOSTRequestToWithTheProductDetails)
ctx.Step(`^レスポンスステータスコードは(\d+)であるべきです$`, c.theResponseStatusCodeShouldBe)
ctx.Step(`^レスポンスは作成された商品詳細を含むべきです$`, c.theResponseShouldContainTheCreatedProductDetails)
ctx.Step(`^システムに商品があります$`, c.thereAreProductsInTheSystem)
ctx.Step(`^"([^"]*)"にGETリクエストを送信します$`, c.iSendAGETRequestTo)
ctx.Step(`^レスポンスは商品のリストを含むべきです$`, c.theResponseShouldContainAListOfProducts)
ctx.Step(`^システムにID "([^"]*)"の商品があります$`, c.thereIsAProductWithIDInTheSystem)
ctx.Step(`^レスポンスは商品詳細を含むべきです$`, c.theResponseShouldContainTheProductDetails)
}
func (c *ControllerContext) iHaveProductDetailsForAPI(table *godog.Table) error {
c.requestBody = make(map[string]interface{})
// テーブルから商品詳細を取得
for i := 1; i < len(table.Rows); i++ {
row := table.Rows[i]
for j, cell := range row.Cells {
header := table.Rows[0].Cells[j].Value
// 日本語のヘッダーを英語のフィールド名にマッピング
switch header {
case "名前":
c.requestBody["Name"] = cell.Value
case "価格":
price, _ := strconv.ParseFloat(cell.Value, 64)
c.requestBody["Price"] = price
case "出品者ID":
c.requestBody["SellerId"] = cell.Value
}
}
}
return nil
}
func (c *ControllerContext) iSendAPOSTRequestToWithTheProductDetails(path string) error {
// リクエストボディをJSONに変換
jsonBody, _ := json.Marshal(c.requestBody)
// HTTPリクエストを作成して送信
req := httptest.NewRequest(http.MethodPost, path, bytes.NewReader(jsonBody))
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
c.response = httptest.NewRecorder()
c.echoInstance.ServeHTTP(c.response, req)
return nil
}
// その他のメソッドも同様に実装
このように、コントローラーを使用した受け入れテストでは、実際のHTTPリクエストとレスポンスをシミュレートし、APIエンドポイントの動作をテストします。リポジトリを直接テストする代わりに、コントローラーを通じてアプリケーション全体の動作を検証します。
受け入れテストでは、モックの代わりにTestContainersを使用して実際のデータベースを使用したテストを行うことができます。これにより、より実際の環境に近いテストが可能になります。
// features/steps/controller_steps.go
func (c *ControllerContext) setupTestDatabase() error {
// PostgreSQLコンテナを定義
pgReq := testcontainers.ContainerRequest{
Image: "postgres:13",
ExposedPorts: []string{"5432/tcp"},
Env: map[string]string{
"POSTGRES_USER": "testuser",
"POSTGRES_PASSWORD": "testpass",
"POSTGRES_DB": "testdb",
},
WaitingFor: wait.ForLog("database system is ready to accept connections").
WithOccurrence(2).WithStartupTimeout(5 * time.Second),
}
// PostgreSQLコンテナを起動
container, err := testcontainers.GenericContainer(c.ctx, testcontainers.GenericContainerRequest{
ContainerRequest: pgReq,
Started: true,
})
if err != nil {
return fmt.Errorf("failed to start PostgreSQL container: %w", err)
}
c.container = container
// コンテナのホストとポートを取得
host, err := container.Host(c.ctx)
if err != nil {
return fmt.Errorf("failed to get PostgreSQL container host: %w", err)
}
port, err := container.MappedPort(c.ctx, "5432")
if err != nil {
return fmt.Errorf("failed to get PostgreSQL container port: %w", err)
}
// データベース接続文字列を作成
dsn := fmt.Sprintf("host=%s port=%s user=testuser password=testpass dbname=testdb sslmode=disable", host, port.Port())
// PostgreSQLデータベースに接続
db, err := gorm.Open(pgdriver.Open(dsn), &gorm.Config{})
if err != nil {
return fmt.Errorf("failed to connect to database: %w", err)
}
c.db = db
// モデルをマイグレーション
err = db.AutoMigrate(&postgres.Product{}, &postgres.Seller{})
if err != nil {
return fmt.Errorf("failed to migrate database: %w", err)
}
return nil
}
func (c *ControllerContext) setupController() error {
// TestContainersを使用してテストデータベースをセットアップ
if err := c.setupTestDatabase(); err != nil {
return err
}
// リポジトリを作成
productRepo := postgres.NewGormProductRepository(c.db)
sellerRepo := postgres.NewGormSellerRepository(c.db)
// サービスを作成
c.productService = services.NewProductService(productRepo, sellerRepo)
c.sellerService = services.NewSellerService(sellerRepo)
// Echoインスタンスを作成
c.echoInstance = echo.New()
// 商品コントローラーを作成
c.productController = rest.NewProductController(c.echoInstance, c.productService)
// レスポンスレコーダーを初期化
c.response = httptest.NewRecorder()
return nil
}
// テスト後のクリーンアップ
func (c *ControllerContext) RegisterSteps(ctx *godog.ScenarioContext) {
// ... 他のステップ定義 ...
ctx.BeforeScenario(func(*godog.Scenario) {
if err := c.setupController(); err != nil {
panic(fmt.Sprintf("Failed to setup controller: %v", err))
}
})
ctx.AfterScenario(func(*godog.Scenario, error) {
// データベースをクリーンアップ
if c.db != nil {
c.db.Exec("DELETE FROM products")
c.db.Exec("DELETE FROM sellers")
}
// PostgreSQLコンテナを停止して削除
if c.container != nil {
if err := c.container.Terminate(c.ctx); err != nil {
fmt.Printf("Failed to terminate container: %v\n", err)
}
}
})
}
このように、TestContainersを使用したBDDテストでは、実際のデータベースを使用してテストを行うことができます。これにより、モックを使用する場合よりも実際の環境に近いテストが可能になり、より信頼性の高いテスト結果を得ることができます。
2.9 アプリケーションの実行¶
セットアップが完了したら、アプリケーションを実行します:
# データベースを起動
docker-compose up -d postgres
# Swaggerドキュメントを生成
cd app/backend
go run cmd/gen/generate_swagger.go
# アプリケーションを実行
go run cmd/marketplace/main.go
アプリケーションが起動したら、以下のURLにアクセスしてSwagger UIを確認できます:
http://localhost:9090/swagger/index.html
3. ディレクトリ構造の詳細¶
完成したアプリケーションのディレクトリ構造は以下のようになります:
app/backend/
├── cmd/
│ ├── marketplace/
│ │ └── main.go
│ └── gen/
│ └── generate_swagger.go
├── docs/
│ ├── swagger.json
│ ├── swagger.yaml
│ └── docs.go
├── features/
│ ├── product.feature
│ └── steps/
│ └── product_steps.go
├── internal/
│ ├── domain/
│ │ ├── entities/
│ │ │ ├── product.go
│ │ │ └── seller.go
│ │ └── repositories/
│ │ ├── product_repository.go
│ │ └── seller_repository.go
│ ├── application/
│ │ └── services/
│ │ ├── product_service.go
│ │ └── seller_service.go
│ ├── infrastructure/
│ │ └── db/
│ │ └── postgres/
│ │ ├── product_repository.go
│ │ └── seller_repository.go
│ └── interface/
│ └── api/
│ └── rest/
│ ├── product_controller.go
│ └── seller_controller.go
├── go.mod
└── go.sum
4. まとめ¶
この文書では、Go-DDDマーケットプレイスアプリケーションのバックエンド部分(app/backend)におけるGoアプリケーションのセットアップ手順と必要なモジュールの設定方法について説明しました。
セットアップ手順は以下の通りです:
- プロジェクト構造の作成
- Go Modulesの初期化
- 必要なパッケージのインストール
- データベース設定
- アプリケーション構造の実装
- ドメインモデル
- リポジトリインターフェース
- リポジトリの実装
- アプリケーションサービス
- コントローラー
- メインアプリケーション
- Swaggerドキュメントの設定
- テスト環境の構築
- ユニットテスト
- 統合テスト
- 受け入れテスト
- アプリケーションの実行
このセットアップ手順に従うことで、ドメイン駆動設計(DDD)の原則に基づいたGoアプリケーションの開発環境を構築することができます。