こんにちは。Tomoyuki(@tomoyuki65)です。
これまではクリーンアーキテクチャを参考にしてGo言語のAPIの作り方を解説してきましたが、実務においてはDDD(ドメイン駆動設計)と呼ばれる方法で作られていることが多いです。
そんなDDDは、ソフトウェアが扱う対象の「ビジネス領域」や「問題領域」そのものをドメインとして定義し、その分野の専門家と開発者が協力してビジネスルールを設計に落とし込む手法です。
例えばオンラインショッピングサイトなら「商品管理」や「注文処理」、会計ソフトなら「経理」や「請求」がドメインにあたります。
このようにビジネスの中心的な課題に焦点を当て、専門家と開発者が密に連携することで、認識のズレを防ぎながらビジネスの成長に合わせてソフトウェアを柔軟に進化させていくことが可能になります。
ということでこの記事では、Go言語のGinでDDD構成のバックエンドAPIを開発する方法についてまとめます。
Go言語(Golang)のGinでDDD(ドメイン駆動設計)構成のバックエンドAPIを開発する方法まとめ
今回はGo言語でよく使われているGinというフレームワークを使いますが、まずは以下のコマンド実行して各種ファイルを作成します。
$ mkdir go-gin-domain && cd go-gin-domain
$ mkdir -p docker/local/go && touch docker/local/go/Dockerfile
$ mkdir src && touch src/.env src/.env.testing src/main.go
$ touch compose.yml
※いつものようにDockerを使って環境構築するため、事前にDockerが使える環境を準備して下さい。
次に作成したファイルをそれぞれ以下のように記述します。
・「docker/local/go/Dockerfile」
FROM golang:1.24-alpine3.21
WORKDIR /go/src
COPY ./src .
# go.modがあれば依存関係をインストール
RUN if [ -f ./go.mod ]; then \
go install; \
fi
# 開発用のライブラリをインストール
RUN go install github.com/air-verse/air@latest
RUN go install honnef.co/go/tools/cmd/staticcheck@latest
RUN go install go.uber.org/mock/mockgen@latest
RUN go install github.com/swaggo/swag/cmd/swag@latest
EXPOSE 8080
※ホットリロードに「air」、静的コード解析に「staticcheck」、テスト用のモックファイル作成に「mockgen」、OpenAPI用に「swag」をインストールしています。
・「src/.env」
ENV=local
PORT=8080
・「src/.env.testing」
ENV=testing
PORT=8080
・「src/main.go」
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello World !!")
}
・「compose.yml」
services:
api:
container_name: go-gin-d-api
build:
context: .
dockerfile: ./docker/local/go/Dockerfile
command: air -c .air.toml
volumes:
- ./src:/go/src
ports:
- "8080:8080"
tty: true
stdin_open: true
次に以下のコマンドを実行し、コンテナのビルドをします。
$ docker compose build --no-cache
次に以下のコマンドを実行し、goの初期化をします。
$ docker compose run --rm api go mod init go-gin-domain
$ docker compose run --rm api go mod tidy
$ docker compose run --rm api air init
共通設定のファイルを追加
次に以下のコマンドを実行し、共通設定のファイルを追加します。
$ mkdir -p src/internal/presentation/middleware && touch src/internal/presentation/middleware/middleware.go
$ mkdir -p src/internal/application/usecase/logger && touch src/internal/application/usecase/logger/logger.go
$ mkdir -p src/internal/infrastructure/logger && touch src/internal/infrastructure/logger/logger_slog.go
$ mkdir -p src/internal/infrastructure/database && touch src/internal/infrastructure/database/dummy.go
※今回は直接DBは使わないので、ダミーの設定ファイルを作成して使います。
次に作成したファイルをそれぞれ以下のように記述します。
・「src/internal/presentation/middleware/middleware.go」
package middleware
import (
"context"
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
type contextKey string
const (
RequestId contextKey = "Request-Id"
XRequestSource contextKey = "X-Request-Source"
UID contextKey = "UID"
)
type Middleware struct{}
func NewMiddleware() *Middleware {
return &Middleware{}
}
// リクエスト用
func (m *Middleware) Request() gin.HandlerFunc {
return func(c *gin.Context) {
// 一意のIDを取得
uuid := uuid.New().String()
// 共通コンテキストにX-Request-Idを設定
ctx := c.Request.Context()
ctx = context.WithValue(ctx, RequestId, uuid)
// リクエストヘッダーからX-Request-Sourceを取得
xRequestSource := c.GetHeader(string(XRequestSource))
if xRequestSource == "" {
xRequestSource = "-"
}
// 共通コンテキストにX-Request-Sourceを設定
ctx = context.WithValue(ctx, XRequestSource, xRequestSource)
// 共通コンテキストの設定
c.Request = c.Request.WithContext(ctx)
c.Next()
}
}
// カスタムロガー
func (m *Middleware) CustomLogger() gin.HandlerFunc {
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
// Request-Idの取得
requestID, ok := param.Request.Context().Value(RequestId).(string)
if !ok {
requestID = "-"
}
// カラーの取得
var statusColor, methodColor, resetColor string
if param.IsOutputColor() {
statusColor = param.StatusCodeColor()
methodColor = param.MethodColor()
resetColor = param.ResetColor()
}
// ログのフォーマットを定義
// [GIN] 2006/01/02 - 15:04:05 | {Request-Id} | 200 | 1.2345ms | 127.0.0.1 | GET /path
return fmt.Sprintf("[GIN] %v| %s |%s %3d %s| %13v | %15s |%s %-7s %s %#v\n%s",
param.TimeStamp.Format("2006/01/02 - 15:04:05"),
requestID,
statusColor,
param.StatusCode,
resetColor,
param.Latency,
param.ClientIP,
methodColor,
param.Method,
resetColor,
param.Path,
param.ErrorMessage,
)
})
}
// 認証用
func (m *Middleware) Auth() gin.HandlerFunc {
return func(c *gin.Context) {
// Bearerトークン取得
authHeader := c.GetHeader("Authorization")
if authHeader == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"message": "認証用トークンが設定されていません。",
})
return
}
token := strings.TrimPrefix(authHeader, "Bearer ")
if token == "" {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"message": "認証用トークンが設定されていません。",
})
return
}
// TODO: 認証チェックを追加する
// TODO: 認証済みならuidを取得
uid := "-"
// 共通コンテキストにuidを設定
ctx := c.Request.Context()
ctx = context.WithValue(ctx, UID, uid)
// 共通コンテキストの設定
c.Request = c.Request.WithContext(ctx)
c.Next()
}
}
※リクエスト用、Ginのロガー用、認証用のミドルウェアの例を定義しています。
・「src/internal/application/usecase/logger/logger.go」
package logger
import (
"context"
)
type Logger interface {
Info(ctx context.Context, message string)
Warn(ctx context.Context, message string)
Error(ctx context.Context, message string)
}
※ここではインターフェースのみ定義します。
・「src/internal/infrastructure/logger/logger_slog.go」
package logger
import (
"context"
"log/slog"
"os"
logger_usecase "go-gin-domain/internal/application/usecase/logger"
"go-gin-domain/internal/presentation/middleware"
)
// slogの設定
type SlogHandler struct {
slog.Handler
}
func (h *SlogHandler) Handle(ctx context.Context, r slog.Record) error {
requestId, ok := ctx.Value(middleware.RequestId).(string)
if ok {
r.AddAttrs(slog.Attr{Key: "requestId", Value: slog.String("requestId", requestId).Value})
}
xRequestSource, ok := ctx.Value(middleware.XRequestSource).(string)
if ok {
r.AddAttrs(slog.Attr{Key: "xRequestSource", Value: slog.String("xRequestSource", xRequestSource).Value})
}
uid, ok := ctx.Value(middleware.UID).(string)
if ok {
r.AddAttrs(slog.Attr{Key: "UID", Value: slog.String("UID", uid).Value})
}
return h.Handler.Handle(ctx, r)
}
var slogHandler = &SlogHandler{
slog.NewTextHandler(os.Stdout, nil),
}
var logger = slog.New(slogHandler)
// ロガーの設定
type slogLogger struct{}
func NewSlogLogger() logger_usecase.Logger {
return &slogLogger{}
}
func (l *slogLogger) Info(ctx context.Context, message string) {
logger.InfoContext(ctx, message)
}
func (l *slogLogger) Warn(ctx context.Context, message string) {
logger.WarnContext(ctx, message)
}
func (l *slogLogger) Error(ctx context.Context, message string) {
logger.ErrorContext(ctx, message)
}
※インフラストラクチャ層でロガーの実装を行います。そしてロガーにはslogを使います。
・「src/internal/infrastructure/database/dummy.go」
package database
import (
"context"
"fmt"
"go-gin-domain/internal/application/usecase/logger"
)
type DummyConfig struct {
Dummy string
}
func NewDummyConnection(cfg DummyConfig, logger logger.Logger) (string, error) {
dsn := fmt.Sprintf("dummy=%s", cfg.Dummy)
// ログ出力
ctx := context.Background()
msg := fmt.Sprintf("Successfully connected to %s", "Dummy")
logger.Info(ctx, msg)
return dsn, nil
}
※今回はダミーのため文字列を返していますが、実際にDBを使う場合はDBのインスタンスを返すように修正して下さい。
次に以下のコマンドを実行し、ロガーのインターフェース定義からテストコード用のモックファイルを作成します。
$ docker compose run --rm api mockgen -source=./internal/application/usecase/logger/logger.go -destination=./internal/application/usecase/logger/mock_logger/mock_logger.go
次に以下のコマンドを実行し、go.modの修正およびコンテナの再ビルドを行います。
$ docker compose run --rm api go mod tidy
$ docker compose build --no-cache
DDD(ドメイン駆動設計)のディレクトリ構成について
この後にDDD(ドメイン駆動設計)でAPIを作成していきますが、ディレクトリ構成としてはDDDの思想に基づいたレイヤードアーキテクチャを採用しています。
/src
└── /internal
├── /application(アプリケーション層)
| └── usecase(ユースケース層)
|
├── /domain(ドメイン層)
| ├── model(ドメインモデルの定義。ビジネスロジックは可能な限りドメインに集約させる。)
| ├── repository(リポジトリのインターフェース定義)
| └── (仮)service(外部サービスのインターフェース定義)
|
├── /infrastructure(インフラストラクチャ層)
| ├── database(データベース設定)
| ├── logger(ロガーの実装。インターフェース部分はユースケース層で定義。)
| ├── persistence(リポジトリの実装。DB操作による永続化層。)
| ├── (仮)cache(キャッシュを含めたリポジトリの実装。インターフェースはリポジトリと同一。)
| └── (仮)externalapi(外部サービスの実装)
|
├── /presentation(プレゼンテーション層)
| ├── handler(ハンドラー層)
| ├── middleware(ミドルウェアの定義)
| └── router(ルーター設定。レジストリのコントローラーを利用して設定する。)
|
└── /registry(レジストリ層。依存注入によるハンドラーのインスタンスをコントローラーにまとめる。)
※(仮)のものは将来的に追加する想定の例です。
ユーザードメインを例にAPIを作る
次に以下の手順でユーザードメインを例にAPIを作成します。
ドメインの定義
まずは以下のコマンドを実行し、各種ファイルを作成します。
$ mkdir -p src/internal/domain/user
$ touch src/internal/domain/user/user_model.go src/internal/domain/user/user_model_test.go
$ touch src/internal/domain/user/user_repository.go
次に作成したファイルをそれぞれ以下のように記述します。
・「src/internal/domain/user/user_model.go」
package user
import (
"fmt"
"strings"
"time"
)
type User struct {
ID int64 `json:"-"`
UID string `json:"uid"`
LastName string `json:"last_name"`
FirstName string `json:"first_name"`
Email string `json:"email"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt *time.Time `json:"deleted_at"`
}
func NewUser(uid, lastName, firstName, email string) *User {
return &User{
ID: 0,
UID: uid,
LastName: lastName,
FirstName: firstName,
Email: email,
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
DeletedAt: nil,
}
}
// プロフィール更新
func (u *User) UpdateProfile(lastName, firstName, email string) error {
// パラメータチェック
var errMsg []string
if lastName == "" {
errMsg = append(errMsg, "last_nameは必須です。")
}
if firstName == "" {
errMsg = append(errMsg, "first_nameは必須です。")
}
if email == "" {
errMsg = append(errMsg, "emailは必須です。")
}
if len(errMsg) > 0 {
msg := fmt.Sprintf("バリデーションエラー: %s", strings.Join(errMsg, ", "))
return fmt.Errorf("%s", msg)
}
// 更新
u.LastName = lastName
u.FirstName = firstName
u.Email = email
u.UpdatedAt = time.Now()
return nil
}
// 論理削除設定
func (u *User) SetDelete() {
// 現在の日時を文字列で取得
date := time.Now()
dateString := date.Format("2006-01-02 15:04:05")
// 更新用のemailの値を設定
updateEmail := u.Email + dateString
// 更新
u.Email = updateEmail
u.UpdatedAt = date
u.DeletedAt = &date
}
※ドメインに関するビジネスロジックについては、可能な限りドメインのメソッドして定義するようにします。
・「src/internal/domain/user/user_model_test.go」
package user
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
func TestNewUser(t *testing.T) {
t.Run("新規ユーザー作成", func(t *testing.T) {
uid := "xxxx-xxxx-xxxx-0001"
lastName := "田中"
firstName := "太郎"
email := "t.tanaka@example.com"
// 処理実行
user := NewUser(uid, lastName, firstName, email)
// 検証
assert.NotNil(t, user)
assert.Equal(t, user.ID, int64(0))
assert.Equal(t, uid, user.UID)
assert.Equal(t, lastName, user.LastName)
assert.Equal(t, firstName, user.FirstName)
assert.Equal(t, email, user.Email)
assert.True(t, user.CreatedAt.IsZero())
assert.True(t, user.UpdatedAt.IsZero())
assert.Nil(t, user.DeletedAt)
})
}
func TestUser_UpdateProfile(t *testing.T) {
baseUser := func() *User {
return &User{
ID: 1,
UID: "xxxx-xxxx-xxxx-0001",
LastName: "田中",
FirstName: "太郎",
Email: "t.tanaka@example.com",
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
DeletedAt: nil,
}
}
t.Run("プロフィール更新処理が正常終了", func(t *testing.T) {
user := baseUser()
oldUpdatedAt := user.UpdatedAt
newLastName := "佐藤"
newFirstName := "二郎"
newEmail := "z.satou@example.com"
// 処理実行
err := user.UpdateProfile(newLastName, newFirstName, newEmail)
// 検証
assert.NoError(t, err)
assert.Equal(t, newLastName, user.LastName)
assert.Equal(t, newFirstName, user.FirstName)
assert.Equal(t, newEmail, user.Email)
assert.True(t, user.UpdatedAt.After(oldUpdatedAt))
})
t.Run("プロフィール更新処理でlast_nameが空の場合エラー", func(t *testing.T) {
user := baseUser()
newLastName := ""
newFirstName := "二郎"
newEmail := "z.satou@example.com"
// 処理実行
err := user.UpdateProfile(newLastName, newFirstName, newEmail)
// 検証
assert.Error(t, err)
assert.Contains(t, err.Error(), "last_nameは必須です。")
})
t.Run("プロフィール更新処理でfirst_nameが空の場合エラー", func(t *testing.T) {
user := baseUser()
newLastName := "佐藤"
newFirstName := ""
newEmail := "z.satou@example.com"
// 処理実行
err := user.UpdateProfile(newLastName, newFirstName, newEmail)
// 検証
assert.Error(t, err)
assert.Contains(t, err.Error(), "first_nameは必須です。")
})
t.Run("プロフィール更新処理でemailが空の場合エラー", func(t *testing.T) {
user := baseUser()
newLastName := "佐藤"
newFirstName := "二郎"
newEmail := ""
// 処理実行
err := user.UpdateProfile(newLastName, newFirstName, newEmail)
// 検証
assert.Error(t, err)
assert.Contains(t, err.Error(), "emailは必須です。")
})
t.Run("プロフィール更新処理で複数項目が空の場合エラー", func(t *testing.T) {
user := baseUser()
// 処理実行
err := user.UpdateProfile("", "", "")
// 検証
assert.Error(t, err)
assert.Contains(t, err.Error(), "last_nameは必須です。")
assert.Contains(t, err.Error(), "first_nameは必須です。")
assert.Contains(t, err.Error(), "emailは必須です。")
})
}
func TestUser_SetDelete(t *testing.T) {
t.Run("論理削除設定がされること", func(t *testing.T) {
user := &User{
ID: 1,
UID: "xxxx-xxxx-xxxx-0001",
LastName: "田中",
FirstName: "太郎",
Email: "t.tanaka@example.com",
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
DeletedAt: nil,
}
initialEmail := user.Email
initialUpdatedAt := user.UpdatedAt
// 処理実行
user.SetDelete()
// 検証
assert.NotEqual(t, initialEmail, user.Email)
assert.True(t, user.UpdatedAt.After(initialUpdatedAt))
assert.NotNil(t, user.DeletedAt)
assert.Equal(t, user.UpdatedAt, *user.DeletedAt)
})
}
・「src/internal/domain/user/user_repository.go」
package user
import (
"context"
)
type UserRepository interface {
Create(ctx context.Context, user *User) (*User, error)
FindAll(ctx context.Context) ([]*User, error)
FindByUID(ctx context.Context, uid string) (*User, error)
Save(ctx context.Context, user *User) (*User, error)
}
※DB操作に関するものはリポジトリのインターフェースを定義します。実装は後述のインフラストラクチャ層で行います。
次に以下のコマンドを実行し、フォーマット修正および静的コード解析を行い、警告が出ないことを確認します。
$ docker compose run --rm api go mod tidy
$ docker compose run --rm api go fmt ./...
$ docker compose run --rm api staticcheck ./...
次に以下のコマンドでテストコードを実行します。
$ docker compose run --rm api go test -v $(docker compose run --rm api go list -f '{{if or .TestGoFiles .XTestGoFiles}}{{.ImportPath}}{{end}}' ./...)
テスト実行後、下図のように全てのテストがPASSすればOKです。
次に以下のコマンドを実行し、後述のテストコード用のモックファイルを作成します。
$ docker compose run --rm api mockgen -source=./internal/domain/user/user_repository.go -destination=./internal/domain/user/mock_user_repository/mock_user_repository.go
リポジトリの実装
次にDB操作に関するリポジトリの実装をするため、以下のコマンドを実行してファイルを作成します。
$ mkdir -p src/internal/infrastructure/persistence/user && touch src/internal/infrastructure/persistence/user/user_repository.go
次に作成したファイルを以下のように記述します。
・「src/internal/infrastructure/persistence/user/user_repository.go」
package user
import (
"context"
"fmt"
"time"
logger_usecase "go-gin-domain/internal/application/usecase/logger"
domain "go-gin-domain/internal/domain/user"
)
type userRepository struct {
db string // 型はDummy
logger logger_usecase.Logger
}
func NewUserRepository(db string, logger logger_usecase.Logger) domain.UserRepository {
return &userRepository{
db: db,
logger: logger,
}
}
func (r *userRepository) Create(ctx context.Context, user *domain.User) (*domain.User, error) {
// 戻り値の例
createUser := &domain.User{
ID: 1,
UID: user.UID,
LastName: user.LastName,
FirstName: user.FirstName,
Email: user.Email,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
DeletedAt: nil,
}
dummy := &domain.User{}
if createUser == dummy {
msg := fmt.Sprintf("[%s] user is nil", r.db)
r.logger.Error(ctx, msg)
}
return createUser, nil
}
func (r *userRepository) FindAll(ctx context.Context) ([]*domain.User, error) {
// 戻り値の例
users := []*domain.User{
{
ID: 1,
UID: "xxxx-xxxx-xxxx-0001",
LastName: "田中",
FirstName: "太郎",
Email: "t.tanaka@example.com",
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
DeletedAt: nil,
},
{
ID: 2,
UID: "xxxx-xxxx-xxxx-0002",
LastName: "佐藤",
FirstName: "一郎",
Email: "i.satou@example.com",
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
DeletedAt: nil,
},
}
if len(users) == 0 {
msg := fmt.Sprintf("[%s] users is nil", r.db)
r.logger.Error(ctx, msg)
}
return users, nil
}
func (r *userRepository) FindByUID(ctx context.Context, uid string) (*domain.User, error) {
// 戻り値の例
var user *domain.User
if uid == "xxxx-xxxx-xxxx-0001" {
user = &domain.User{
ID: 1,
UID: "xxxx-xxxx-xxxx-0001",
LastName: "田中",
FirstName: "太郎",
Email: "t.tanaka@example.com",
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
DeletedAt: nil,
}
} else {
user = nil
}
return user, nil
}
func (r *userRepository) Save(ctx context.Context, user *domain.User) (*domain.User, error) {
// 戻り値の例
return user, nil
}
※今回は直接DBは使わないため、リポジトリの実装はサンプルです。実際にはr.dbを使ってDB操作を行うのと、それに関するテストコードも書くようにして下さい。
ユースケースの定義
次にユースケースを定義するため、以下のコマンドを実行して各種ファイルを作成します。
$ mkdir -p src/internal/application/usecase/user && touch src/internal/application/usecase/user/user.go
$ touch src/internal/application/usecase/user/user_create.go src/internal/application/usecase/user/user_create_test.go
$ touch src/internal/application/usecase/user/user_find_all.go src/internal/application/usecase/user/user_find_all_test.go
$ touch src/internal/application/usecase/user/user_find_by_uid.go src/internal/application/usecase/user/user_find_by_uid_test.go
$ touch src/internal/application/usecase/user/user_update.go src/internal/application/usecase/user/user_update_test.go
$ touch src/internal/application/usecase/user/user_delete.go src/internal/application/usecase/user/user_delete_test.go
次に作成したファイルをそれぞれ以下のように記述します。
・「src/internal/application/usecase/user/user.go」
package user
import (
"context"
"go-gin-domain/internal/application/usecase/logger"
domain_user "go-gin-domain/internal/domain/user"
)
type UserUsecase interface {
Create(ctx context.Context, lastName, firstName, email string) (*domain_user.User, error)
FindAll(ctx context.Context) ([]*domain_user.User, error)
FindByUID(ctx context.Context, uid string) (*domain_user.User, error)
Update(ctx context.Context, uid, lastName, firstName, email string) (*domain_user.User, error)
Delete(ctx context.Context, uid string) (*domain_user.User, error)
}
type userUsecase struct {
userRepo domain_user.UserRepository
logger logger.Logger
}
func NewUserUsecase(userRepo domain_user.UserRepository, logger logger.Logger) UserUsecase {
return &userUsecase{
userRepo: userRepo,
logger: logger,
}
}
※このファイルでユースケースの構造体やインターフェース定義をまとめ、インターフェースに定義した各メソッドはファイルを分割して記述します。
・「src/internal/application/usecase/user/user_create.go」
package user
import (
"context"
domain_user "go-gin-domain/internal/domain/user"
"github.com/google/uuid"
)
func (u *userUsecase) Create(ctx context.Context, lastName, firstName, email string) (*domain_user.User, error) {
// UIDの設定(仮)
uid := uuid.New().String()
// 新規ユーザー作成
user := domain_user.NewUser(uid, lastName, firstName, email)
return u.userRepo.Create(ctx, user)
}
・「src/internal/application/usecase/user/user_create_test.go」
package user
import (
"context"
"fmt"
"testing"
"time"
mockLogger "go-gin-domain/internal/application/usecase/logger/mock_logger"
domain_user "go-gin-domain/internal/domain/user"
mockUser "go-gin-domain/internal/domain/user/mock_user_repository"
"github.com/joho/godotenv"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)
// 初期処理
func init() {
// テスト用の環境変数ファイル「.env.testing」を読み込んで使用する。
if err := godotenv.Load("../../../../.env.testing"); err != nil {
fmt.Println(".env.testingの読み込みに失敗しました。")
}
}
func TestUserUsecase_Create(t *testing.T) {
// リポジトリのモック
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mockUser.NewMockUserRepository(ctrl)
// ロガーのモック
mockLogger := mockLogger.NewMockLogger(ctrl)
t.Run("正常終了すること", func(t *testing.T) {
// モック化
expectedUser := &domain_user.User{
ID: 1,
UID: "xxxx-xxxx-xxxx-0001",
LastName: "田中",
FirstName: "太郎",
Email: "t.tanaka@example.com",
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
DeletedAt: nil,
}
mockRepo.EXPECT().Create(gomock.Any(), gomock.Any()).Return(expectedUser, nil)
// ユースケースのインスタンス化
userUsecase := NewUserUsecase(mockRepo, mockLogger)
// テストの実行
ctx := context.Background()
lastName := "田中"
firstName := "太郎"
email := "t.tanaka@example.com"
user, err := userUsecase.Create(ctx, lastName, firstName, email)
// 検証
assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, expectedUser.ID, user.ID)
assert.Equal(t, expectedUser.UID, user.UID)
assert.Equal(t, expectedUser.LastName, user.LastName)
assert.Equal(t, expectedUser.FirstName, user.FirstName)
assert.Equal(t, expectedUser.Email, user.Email)
assert.NotNil(t, user.CreatedAt)
assert.NotNil(t, user.UpdatedAt)
assert.Nil(t, user.DeletedAt)
})
}
・「src/internal/application/usecase/user/user_find_all.go」
package user
import (
"context"
domain_user "go-gin-domain/internal/domain/user"
)
func (u *userUsecase) FindAll(ctx context.Context) ([]*domain_user.User, error) {
return u.userRepo.FindAll(ctx)
}
・「src/internal/application/usecase/user/user_find_all_test.go」
package user
import (
"context"
"fmt"
"testing"
"time"
mockLogger "go-gin-domain/internal/application/usecase/logger/mock_logger"
domain_user "go-gin-domain/internal/domain/user"
mockUser "go-gin-domain/internal/domain/user/mock_user_repository"
"github.com/joho/godotenv"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)
// 初期処理
func init() {
// テスト用の環境変数ファイル「.env.testing」を読み込んで使用する。
if err := godotenv.Load("../../../../.env.testing"); err != nil {
fmt.Println(".env.testingの読み込みに失敗しました。")
}
}
func TestUserUsecase_FindAll(t *testing.T) {
// リポジトリのモック
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mockUser.NewMockUserRepository(ctrl)
// ロガーのモック
mockLogger := mockLogger.NewMockLogger(ctrl)
t.Run("正常終了すること", func(t *testing.T) {
// モック化
expectedUsers := []*domain_user.User{
{
ID: 1,
UID: "xxxx-xxxx-xxxx-0001",
LastName: "田中",
FirstName: "太郎",
Email: "t.tanaka@example.com",
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
DeletedAt: nil,
},
{
ID: 2,
UID: "xxxx-xxxx-xxxx-0002",
LastName: "佐藤",
FirstName: "一郎",
Email: "i.satou@example.com",
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
DeletedAt: nil,
},
}
mockRepo.EXPECT().FindAll(gomock.Any()).Return(expectedUsers, nil)
// ユースケースのインスタンス化
userUsecase := NewUserUsecase(mockRepo, mockLogger)
// テストの実行
ctx := context.Background()
users, err := userUsecase.FindAll(ctx)
// 検証
assert.NoError(t, err)
assert.NotNil(t, users)
assert.Equal(t, len(expectedUsers), len(users))
assert.Equal(t, expectedUsers[0].ID, users[0].ID)
assert.Equal(t, expectedUsers[0].UID, users[0].UID)
assert.Equal(t, expectedUsers[0].LastName, users[0].LastName)
assert.Equal(t, expectedUsers[0].FirstName, users[0].FirstName)
assert.Equal(t, expectedUsers[0].Email, users[0].Email)
assert.Equal(t, expectedUsers[0].CreatedAt, users[0].CreatedAt)
assert.Equal(t, expectedUsers[0].UpdatedAt, users[0].UpdatedAt)
assert.Equal(t, expectedUsers[0].DeletedAt, users[0].DeletedAt)
assert.Equal(t, expectedUsers[1].ID, users[1].ID)
assert.Equal(t, expectedUsers[1].UID, users[1].UID)
assert.Equal(t, expectedUsers[1].LastName, users[1].LastName)
assert.Equal(t, expectedUsers[1].FirstName, users[1].FirstName)
assert.Equal(t, expectedUsers[1].Email, users[1].Email)
assert.Equal(t, expectedUsers[1].CreatedAt, users[1].CreatedAt)
assert.Equal(t, expectedUsers[1].UpdatedAt, users[1].UpdatedAt)
assert.Equal(t, expectedUsers[1].DeletedAt, users[1].DeletedAt)
})
}
・「src/internal/application/usecase/user/user_find_by_uid.go」
package user
import (
"context"
domain_user "go-gin-domain/internal/domain/user"
)
func (u *userUsecase) FindByUID(ctx context.Context, uid string) (*domain_user.User, error) {
return u.userRepo.FindByUID(ctx, uid)
}
・「src/internal/application/usecase/user/user_find_by_uid_test.go」
package user
import (
"context"
"fmt"
"testing"
"time"
mockLogger "go-gin-domain/internal/application/usecase/logger/mock_logger"
domain_user "go-gin-domain/internal/domain/user"
mockUser "go-gin-domain/internal/domain/user/mock_user_repository"
"github.com/joho/godotenv"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)
// 初期処理
func init() {
// テスト用の環境変数ファイル「.env.testing」を読み込んで使用する。
if err := godotenv.Load("../../../../.env.testing"); err != nil {
fmt.Println(".env.testingの読み込みに失敗しました。")
}
}
func TestUserUsecase_FindByUID(t *testing.T) {
// リポジトリのモック
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mockUser.NewMockUserRepository(ctrl)
// ロガーのモック
mockLogger := mockLogger.NewMockLogger(ctrl)
t.Run("正常終了すること", func(t *testing.T) {
// モック化
expectedUser := &domain_user.User{
ID: 1,
UID: "xxxx-xxxx-xxxx-0001",
LastName: "田中",
FirstName: "太郎",
Email: "t.tanaka@example.com",
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
DeletedAt: nil,
}
mockRepo.EXPECT().FindByUID(gomock.Any(), gomock.Any()).Return(expectedUser, nil)
// ユースケースのインスタンス化
userUsecase := NewUserUsecase(mockRepo, mockLogger)
// テストの実行
ctx := context.Background()
uid := "xxxx-xxxx-xxxx-0001"
user, err := userUsecase.FindByUID(ctx, uid)
// 検証
assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, expectedUser.ID, user.ID)
assert.Equal(t, expectedUser.UID, user.UID)
assert.Equal(t, expectedUser.LastName, user.LastName)
assert.Equal(t, expectedUser.FirstName, user.FirstName)
assert.Equal(t, expectedUser.Email, user.Email)
assert.NotNil(t, user.CreatedAt)
assert.NotNil(t, user.UpdatedAt)
assert.Nil(t, user.DeletedAt)
})
}
・「src/internal/application/usecase/user/user_update.go」
package user
import (
"context"
"fmt"
domain_user "go-gin-domain/internal/domain/user"
)
func (u *userUsecase) Update(ctx context.Context, uid, lastName, firstName, email string) (*domain_user.User, error) {
user, err := u.userRepo.FindByUID(ctx, uid)
if err != nil {
return nil, err
}
// 対象ユーザーが存在しない場合はエラー
if user == nil {
msg := fmt.Sprintf("対象ユーザーが存在しません。: UID=%s", uid)
u.logger.Error(ctx, msg)
return nil, fmt.Errorf("%s", msg)
}
// プロフィール更新
err = user.UpdateProfile(lastName, firstName, email)
if err != nil {
return nil, err
}
return u.userRepo.Save(ctx, user)
}
・「src/internal/application/usecase/user/user_update_test.go」
package user
import (
"context"
"fmt"
"testing"
"time"
mockLogger "go-gin-domain/internal/application/usecase/logger/mock_logger"
domain_user "go-gin-domain/internal/domain/user"
mockUser "go-gin-domain/internal/domain/user/mock_user_repository"
"github.com/joho/godotenv"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)
// 初期処理
func init() {
// テスト用の環境変数ファイル「.env.testing」を読み込んで使用する。
if err := godotenv.Load("../../../../.env.testing"); err != nil {
fmt.Println(".env.testingの読み込みに失敗しました。")
}
}
func TestUserUsecase_Update(t *testing.T) {
// リポジトリのモック
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mockUser.NewMockUserRepository(ctrl)
// ロガーのモック
mockLogger := mockLogger.NewMockLogger(ctrl)
t.Run("正常終了すること", func(t *testing.T) {
// モック化
findUser := &domain_user.User{
ID: 1,
UID: "xxxx-xxxx-xxxx-0001",
LastName: "田中",
FirstName: "太郎",
Email: "t.tanaka@example.com",
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
DeletedAt: nil,
}
mockRepo.EXPECT().FindByUID(gomock.Any(), gomock.Any()).Return(findUser, nil)
expectedUser := &domain_user.User{
ID: 1,
UID: "xxxx-xxxx-xxxx-0001",
LastName: "佐藤",
FirstName: "二郎",
Email: "z.satou@example.com",
CreatedAt: time.Time{},
UpdatedAt: time.Now(),
DeletedAt: nil,
}
mockRepo.EXPECT().Save(gomock.Any(), gomock.Any()).Return(expectedUser, nil)
// ユースケースのインスタンス化
userUsecase := NewUserUsecase(mockRepo, mockLogger)
// テストの実行
ctx := context.Background()
uid := "xxxx-xxxx-xxxx-0001"
lastName := "佐藤"
firstName := "二郎"
email := "z.satou@example.com"
user, err := userUsecase.Update(ctx, uid, lastName, firstName, email)
// 検証
assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, expectedUser.ID, user.ID)
assert.Equal(t, expectedUser.UID, user.UID)
assert.Equal(t, expectedUser.LastName, user.LastName)
assert.Equal(t, expectedUser.FirstName, user.FirstName)
assert.Equal(t, expectedUser.Email, user.Email)
assert.NotNil(t, user.CreatedAt)
assert.NotNil(t, user.UpdatedAt)
assert.NotEqual(t, user.UpdatedAt, user.CreatedAt)
assert.Nil(t, user.DeletedAt)
})
t.Run("対象ユーザー取得でエラーの場合にエラーを返すこと", func(t *testing.T) {
// モック化
err := fmt.Errorf("Internal Server Error")
mockRepo.EXPECT().FindByUID(gomock.Any(), gomock.Any()).Return(nil, err)
// ユースケースのインスタンス化
userUsecase := NewUserUsecase(mockRepo, mockLogger)
// テストの実行
ctx := context.Background()
uid := "xxxx-xxxx-xxxx-0001"
lastName := "佐藤"
firstName := "二郎"
email := "z.satou@example.com"
user, err := userUsecase.Update(ctx, uid, lastName, firstName, email)
// 検証
assert.Error(t, err)
assert.Nil(t, user)
})
t.Run("対象ユーザーが存在しない場合にエラーを返すこと", func(t *testing.T) {
// モック化
mockRepo.EXPECT().FindByUID(gomock.Any(), gomock.Any()).Return(nil, nil)
mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).Return()
// ユースケースのインスタンス化
userUsecase := NewUserUsecase(mockRepo, mockLogger)
// テストの実行
ctx := context.Background()
uid := "xxxx-xxxx-xxxx-0001"
lastName := "佐藤"
firstName := "二郎"
email := "z.satou@example.com"
user, err := userUsecase.Update(ctx, uid, lastName, firstName, email)
// 検証
assert.Error(t, err)
assert.Nil(t, user)
})
t.Run("プロフィール更新でエラーの場合にエラーを返すこと", func(t *testing.T) {
// モック化
findUser := &domain_user.User{
ID: 1,
UID: "xxxx-xxxx-xxxx-0001",
LastName: "田中",
FirstName: "太郎",
Email: "t.tanaka@example.com",
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
DeletedAt: nil,
}
mockRepo.EXPECT().FindByUID(gomock.Any(), gomock.Any()).Return(findUser, nil)
// ユースケースのインスタンス化
userUsecase := NewUserUsecase(mockRepo, mockLogger)
// テストの実行
ctx := context.Background()
uid := "xxxx-xxxx-xxxx-0001"
lastName := ""
firstName := "二郎"
email := "z.satou@example.com"
user, err := userUsecase.Update(ctx, uid, lastName, firstName, email)
// 検証
assert.Error(t, err)
assert.Nil(t, user)
})
}
・「src/internal/application/usecase/user/user_delete.go」
package user
import (
"context"
"fmt"
domain_user "go-gin-domain/internal/domain/user"
)
func (u *userUsecase) Delete(ctx context.Context, uid string) (*domain_user.User, error) {
user, err := u.userRepo.FindByUID(ctx, uid)
if err != nil {
return nil, err
}
// 対象ユーザーが存在しない場合はエラー
if user == nil {
msg := fmt.Sprintf("対象ユーザーが存在しません。: UID=%s", uid)
u.logger.Error(ctx, msg)
return nil, fmt.Errorf("%s", msg)
}
// 論理削除設定
user.SetDelete()
return u.userRepo.Save(ctx, user)
}
・「src/internal/application/usecase/user/user_delete_test.go」
package user
import (
"context"
"fmt"
"testing"
"time"
mockLogger "go-gin-domain/internal/application/usecase/logger/mock_logger"
domain_user "go-gin-domain/internal/domain/user"
mockUser "go-gin-domain/internal/domain/user/mock_user_repository"
"github.com/joho/godotenv"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)
// 初期処理
func init() {
// テスト用の環境変数ファイル「.env.testing」を読み込んで使用する。
if err := godotenv.Load("../../../../.env.testing"); err != nil {
fmt.Println(".env.testingの読み込みに失敗しました。")
}
}
func TestUserUsecase_Delete(t *testing.T) {
// リポジトリのモック
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockRepo := mockUser.NewMockUserRepository(ctrl)
// ロガーのモック
mockLogger := mockLogger.NewMockLogger(ctrl)
t.Run("正常終了すること", func(t *testing.T) {
// モック化
findUser := &domain_user.User{
ID: 1,
UID: "xxxx-xxxx-xxxx-0001",
LastName: "佐藤",
FirstName: "二郎",
Email: "z.satou@example.com",
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
DeletedAt: nil,
}
mockRepo.EXPECT().FindByUID(gomock.Any(), gomock.Any()).Return(findUser, nil)
date := time.Now()
dateString := date.Format("2006-01-02 15:04:05")
updateEmail := "z.satou@example.com" + dateString
expectedUser := &domain_user.User{
ID: 1,
UID: "xxxx-xxxx-xxxx-0001",
LastName: "佐藤",
FirstName: "二郎",
Email: updateEmail,
CreatedAt: time.Time{},
UpdatedAt: date,
DeletedAt: &date,
}
mockRepo.EXPECT().Save(gomock.Any(), gomock.Any()).Return(expectedUser, nil)
// ユースケースのインスタンス化
userUsecase := NewUserUsecase(mockRepo, mockLogger)
// テストの実行
ctx := context.Background()
uid := "xxxx-xxxx-xxxx-0001"
user, err := userUsecase.Delete(ctx, uid)
// 検証
assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, expectedUser.ID, user.ID)
assert.Equal(t, expectedUser.UID, user.UID)
assert.Equal(t, expectedUser.LastName, user.LastName)
assert.Equal(t, expectedUser.FirstName, user.FirstName)
assert.Equal(t, expectedUser.Email, user.Email)
assert.NotNil(t, user.CreatedAt)
assert.NotNil(t, user.UpdatedAt)
assert.NotEqual(t, user.UpdatedAt, user.CreatedAt)
assert.NotNil(t, user.DeletedAt)
assert.Equal(t, *user.DeletedAt, user.UpdatedAt)
})
t.Run("対象ユーザー取得でエラーの場合にエラーを返すこと", func(t *testing.T) {
// モック化
err := fmt.Errorf("Internal Server Error")
mockRepo.EXPECT().FindByUID(gomock.Any(), gomock.Any()).Return(nil, err)
// ユースケースのインスタンス化
userUsecase := NewUserUsecase(mockRepo, mockLogger)
// テストの実行
ctx := context.Background()
uid := "xxxx-xxxx-xxxx-0001"
user, err := userUsecase.Delete(ctx, uid)
// 検証
assert.Error(t, err)
assert.Nil(t, user)
})
t.Run("対象ユーザーが存在しない場合にエラーを返すこと", func(t *testing.T) {
// モック化
mockRepo.EXPECT().FindByUID(gomock.Any(), gomock.Any()).Return(nil, nil)
mockLogger.EXPECT().Error(gomock.Any(), gomock.Any()).Return()
// ユースケースのインスタンス化
userUsecase := NewUserUsecase(mockRepo, mockLogger)
// テストの実行
ctx := context.Background()
uid := "xxxx-xxxx-xxxx-0001"
user, err := userUsecase.Delete(ctx, uid)
// 検証
assert.Error(t, err)
assert.Nil(t, user)
})
}
次に以下のコマンドを実行し、フォーマット修正および静的コード解析を行い、警告が出ないことを確認します。
$ docker compose run --rm api go mod tidy
$ docker compose run --rm api go fmt ./...
$ docker compose run --rm api staticcheck ./...
次に以下のコマンドでテストコードを実行します。
$ docker compose run --rm api go test -v $(docker compose run --rm api go list -f '{{if or .TestGoFiles .XTestGoFiles}}{{.ImportPath}}{{end}}' ./...)
テスト実行後、下図のように全てのテストがPASSすればOKです。
次に以下のコマンドを実行し、後述のテストコード用のモックファイルを作成します。
$ docker compose run --rm api mockgen -source=./internal/application/usecase/user/user.go -destination=./internal/application/usecase/user/mock_user/mock_user.go
ハンドラーの定義
次にハンドラーを定義するため、以下のコマンドを実行して各種ファイルを作成します。
$ mkdir -p src/internal/presentation/handler/user
$ touch src/internal/presentation/handler/user/user_handler.go src/internal/presentation/handler/user/user_handler_test.go
次に作成したファイルをそれぞれ以下のように記述します。
・「src/internal/presentation/handler/user/user_handler.go」
package user
import (
"fmt"
"net/http"
"strings"
usecase "go-gin-domain/internal/application/usecase/user"
"github.com/gin-gonic/gin"
)
type UserHandler interface {
Create(c *gin.Context)
FindAll(c *gin.Context)
FindByUID(c *gin.Context)
Update(c *gin.Context)
Delete(c *gin.Context)
}
type userHandler struct {
userUsecase usecase.UserUsecase
}
func NewUserHandler(
userUsecase usecase.UserUsecase,
) UserHandler {
return &userHandler{
userUsecase: userUsecase,
}
}
type CreateUserRequestBody struct {
LastName string `json:"last_name" binding:"required"`
FirstName string `json:"first_name" binding:"required"`
Email string `json:"email" binding:"required,email"`
}
type UpdateUserRequestBody struct {
LastName string `json:"last_name" binding:"required"`
FirstName string `json:"first_name" binding:"required"`
Email string `json:"email" binding:"required,email"`
}
func (h *userHandler) Create(c *gin.Context) {
// 共通コンテキスト
ctx := c.Request.Context()
// バリデーションチェック
var reqBody CreateUserRequestBody
if err := c.ShouldBindJSON(&reqBody); err != nil {
msg := fmt.Sprintf("バリデーションエラー: %s", err.Error())
c.JSON(http.StatusUnprocessableEntity, gin.H{
"message": msg,
})
return
}
user, err := h.userUsecase.Create(ctx, reqBody.LastName, reqBody.FirstName, reqBody.Email)
if err != nil {
msg := fmt.Sprintf("Internal Server Error: %s", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{
"message": msg,
})
return
}
c.JSON(http.StatusCreated, user)
}
func (h *userHandler) FindAll(c *gin.Context) {
// 共通コンテキスト
ctx := c.Request.Context()
users, err := h.userUsecase.FindAll(ctx)
if err != nil {
msg := fmt.Sprintf("Internal Server Error: %s", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{
"message": msg,
})
return
}
c.JSON(http.StatusOK, users)
}
func (h *userHandler) FindByUID(c *gin.Context) {
// 共通コンテキスト
ctx := c.Request.Context()
// バリデーションチェック
uid := c.Param("uid")
if strings.TrimSpace(uid) == "" {
msg := fmt.Sprintf("バリデーションエラー: %s", "uid is required")
c.JSON(http.StatusUnprocessableEntity, gin.H{
"message": msg,
})
return
}
user, err := h.userUsecase.FindByUID(ctx, uid)
if err != nil {
msg := fmt.Sprintf("Internal Server Error: %s", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{
"message": msg,
})
return
}
// userがnilの場合に空のオブジェクトを返す
if user == nil {
c.JSON(http.StatusOK, map[string]interface{}{})
return
}
c.JSON(http.StatusOK, user)
}
func (h *userHandler) Update(c *gin.Context) {
// 共通コンテキスト
ctx := c.Request.Context()
// バリデーションチェック
uid := c.Param("uid")
if strings.TrimSpace(uid) == "" {
msg := fmt.Sprintf("バリデーションエラー: %s", "uid is required")
c.JSON(http.StatusUnprocessableEntity, gin.H{
"message": msg,
})
return
}
var reqBody UpdateUserRequestBody
if err := c.ShouldBindJSON(&reqBody); err != nil {
msg := fmt.Sprintf("バリデーションエラー: %s", err.Error())
c.JSON(http.StatusUnprocessableEntity, gin.H{
"message": msg,
})
return
}
user, err := h.userUsecase.Update(ctx, uid, reqBody.LastName, reqBody.FirstName, reqBody.Email)
if err != nil {
msg := fmt.Sprintf("Internal Server Error: %s", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{
"message": msg,
})
return
}
c.JSON(http.StatusOK, user)
}
func (h *userHandler) Delete(c *gin.Context) {
// 共通コンテキスト
ctx := c.Request.Context()
// バリデーションチェック
uid := c.Param("uid")
if strings.TrimSpace(uid) == "" {
msg := fmt.Sprintf("バリデーションエラー: %s", "uid is required")
c.JSON(http.StatusUnprocessableEntity, gin.H{
"message": msg,
})
return
}
user, err := h.userUsecase.Delete(ctx, uid)
if err != nil {
msg := fmt.Sprintf("Internal Server Error: %s", err.Error())
c.JSON(http.StatusInternalServerError, gin.H{
"message": msg,
})
return
}
c.JSON(http.StatusOK, user)
}
・「src/internal/presentation/handler/user/user_handler_test.go」
package user
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"time"
mockUser "go-gin-domain/internal/application/usecase/user/mock_user"
domain_user "go-gin-domain/internal/domain/user"
"go-gin-domain/internal/presentation/middleware"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)
// 初期処理
func init() {
// テスト用の環境変数ファイル「.env.testing」を読み込んで使用する。
if err := godotenv.Load("../../../../.env.testing"); err != nil {
fmt.Println(".env.testingの読み込みに失敗しました。")
}
}
// テスト用Ginの初期化処理
func initTestGin() (*gin.Engine, *gin.RouterGroup) {
r := gin.New()
// ミドルウェアの設定
m := middleware.NewMiddleware()
r.Use(m.Request())
r.Use(m.CustomLogger())
r.Use(gin.Recovery())
apiV1 := r.Group("/api/v1")
return r, apiV1
}
func TestUserHandler_Create(t *testing.T) {
// Ginのテストモードに設定
gin.SetMode(gin.TestMode)
// ユースケースのモック
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockUserUsecase := mockUser.NewMockUserUsecase(ctrl)
t.Run("ステータス201で正常終了すること", func(t *testing.T) {
// モック化
expectedUser := &domain_user.User{
ID: 1,
UID: "xxxx-xxxx-xxxx-0001",
LastName: "田中",
FirstName: "太郎",
Email: "t.tanaka@example.com",
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
DeletedAt: nil,
}
mockUserUsecase.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(expectedUser, nil)
// ルーター設定
r, apiV1 := initTestGin()
h := NewUserHandler(mockUserUsecase)
apiV1.POST("/user", h.Create)
// リクエスト設定
path := "/api/v1/user"
reqBody := CreateUserRequestBody{
LastName: "田中",
FirstName: "太郎",
Email: "t.tanaka@example.com",
}
jsonReqBody, err := json.Marshal(reqBody)
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodPost, path, bytes.NewBuffer(jsonReqBody))
// テストの実行
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// 検証
assert.Equal(t, http.StatusCreated, w.Code)
var data map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &data)
assert.NoError(t, err)
assert.NotContains(t, data, "id")
assert.NotNil(t, data["uid"])
assert.Equal(t, expectedUser.LastName, data["last_name"])
assert.Equal(t, expectedUser.FirstName, data["first_name"])
assert.Equal(t, expectedUser.Email, data["email"])
assert.NotNil(t, data["created_at"])
assert.NotNil(t, data["updated_at"])
assert.Nil(t, data["deleted_at"])
})
t.Run("ユースケースでエラーが発生した場合にステータス500を返すこと", func(t *testing.T) {
// モック化
err := fmt.Errorf("Internal Server Error")
mockUserUsecase.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, err)
// ルーター設定
r, apiV1 := initTestGin()
h := NewUserHandler(mockUserUsecase)
apiV1.POST("/user", h.Create)
// リクエスト設定
path := "/api/v1/user"
reqBody := CreateUserRequestBody{
LastName: "田中",
FirstName: "太郎",
Email: "t.tanaka@example.com",
}
jsonReqBody, err := json.Marshal(reqBody)
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodPost, path, bytes.NewBuffer(jsonReqBody))
// テストの実行
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// 検証
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, w.Body.String(), "Internal Server Error")
})
t.Run("バリデーションチェックでエラーの場合にステータス422を返すこと", func(t *testing.T) {
// ルーター設定
r, apiV1 := initTestGin()
h := NewUserHandler(mockUserUsecase)
apiV1.POST("/user", h.Create)
// リクエスト設定
path := "/api/v1/user"
reqBody := CreateUserRequestBody{
LastName: "",
FirstName: "太郎",
Email: "t.tanaka@example.com",
}
jsonReqBody, err := json.Marshal(reqBody)
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodPost, path, bytes.NewBuffer(jsonReqBody))
// テストの実行
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// 検証
assert.Equal(t, http.StatusUnprocessableEntity, w.Code)
assert.Contains(t, w.Body.String(), "バリデーションエラー")
})
}
func TestUserHandler_FindAll(t *testing.T) {
// Ginのテストモードに設定
gin.SetMode(gin.TestMode)
// ユースケースのモック
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockUserUsecase := mockUser.NewMockUserUsecase(ctrl)
t.Run("ステータス200で正常終了すること", func(t *testing.T) {
// モック化
expectedUsers := []*domain_user.User{
{
ID: 1,
UID: "xxxx-xxxx-xxxx-0001",
LastName: "田中",
FirstName: "太郎",
Email: "t.tanaka@example.com",
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
DeletedAt: nil,
},
{
ID: 2,
UID: "xxxx-xxxx-xxxx-0002",
LastName: "佐藤",
FirstName: "一郎",
Email: "i.satou@example.com",
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
DeletedAt: nil,
},
}
mockUserUsecase.EXPECT().FindAll(gomock.Any()).Return(expectedUsers, nil)
// ルーター設定
r, apiV1 := initTestGin()
m := middleware.NewMiddleware()
h := NewUserHandler(mockUserUsecase)
apiV1.GET("/users", m.Auth(), h.FindAll)
// リクエスト設定
path := "/api/v1/users"
req := httptest.NewRequest(http.MethodGet, path, nil)
req.Header.Set("Authorization", "Bearer xxxxxx")
// テストの実行
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// 検証
assert.Equal(t, http.StatusOK, w.Code)
var list []map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &list)
assert.NoError(t, err)
assert.NotContains(t, list[0], "id")
assert.Equal(t, expectedUsers[0].UID, list[0]["uid"])
assert.Equal(t, expectedUsers[0].LastName, list[0]["last_name"])
assert.Equal(t, expectedUsers[0].FirstName, list[0]["first_name"])
assert.Equal(t, expectedUsers[0].Email, list[0]["email"])
assert.NotNil(t, list[0]["created_at"])
assert.NotNil(t, list[0]["updated_at"])
assert.Nil(t, list[0]["deleted_at"])
assert.NotContains(t, list[1], "id")
assert.Equal(t, expectedUsers[1].UID, list[1]["uid"])
assert.Equal(t, expectedUsers[1].LastName, list[1]["last_name"])
assert.Equal(t, expectedUsers[1].FirstName, list[1]["first_name"])
assert.Equal(t, expectedUsers[1].Email, list[1]["email"])
assert.NotNil(t, list[1]["created_at"])
assert.NotNil(t, list[1]["updated_at"])
assert.Nil(t, list[1]["deleted_at"])
})
t.Run("ユースケースでエラーが発生した場合にステータス500を返すこと", func(t *testing.T) {
// モック化
err := fmt.Errorf("Internal Server Error")
mockUserUsecase.EXPECT().FindAll(gomock.Any()).Return(nil, err)
// ルーター設定
r, apiV1 := initTestGin()
m := middleware.NewMiddleware()
h := NewUserHandler(mockUserUsecase)
apiV1.GET("/users", m.Auth(), h.FindAll)
// リクエスト設定
path := "/api/v1/users"
req := httptest.NewRequest(http.MethodGet, path, nil)
req.Header.Set("Authorization", "Bearer xxxxxx")
// テストの実行
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// 検証
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, w.Body.String(), "Internal Server Error")
})
}
func TestUserHandler_FindByUID(t *testing.T) {
// Ginのテストモードに設定
gin.SetMode(gin.TestMode)
// ユースケースのモック
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockUserUsecase := mockUser.NewMockUserUsecase(ctrl)
t.Run("ステータス200で正常終了すること", func(t *testing.T) {
// モック化
expectedUser := &domain_user.User{
ID: 1,
UID: "xxxx-xxxx-xxxx-0001",
LastName: "田中",
FirstName: "太郎",
Email: "t.tanaka@example.com",
CreatedAt: time.Time{},
UpdatedAt: time.Time{},
DeletedAt: nil,
}
mockUserUsecase.EXPECT().FindByUID(gomock.Any(), gomock.Any()).Return(expectedUser, nil)
// ルーター設定
r, apiV1 := initTestGin()
m := middleware.NewMiddleware()
h := NewUserHandler(mockUserUsecase)
apiV1.GET("/user/:uid", m.Auth(), h.FindByUID)
// リクエスト設定
path := "/api/v1/user/xxxx-xxxx-xxxx-0001"
req := httptest.NewRequest(http.MethodGet, path, nil)
req.Header.Set("Authorization", "Bearer xxxxxx")
// テストの実行
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// 検証
assert.Equal(t, http.StatusOK, w.Code)
var data map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &data)
assert.NoError(t, err)
assert.NotContains(t, data, "id")
assert.NotNil(t, data["uid"])
assert.Equal(t, expectedUser.LastName, data["last_name"])
assert.Equal(t, expectedUser.FirstName, data["first_name"])
assert.Equal(t, expectedUser.Email, data["email"])
assert.NotNil(t, data["created_at"])
assert.NotNil(t, data["updated_at"])
assert.Nil(t, data["deleted_at"])
})
t.Run("対象ユーザーが存在しない場合にステータス200で空のオブジェクトを返すこと", func(t *testing.T) {
// モック化
mockUserUsecase.EXPECT().FindByUID(gomock.Any(), gomock.Any()).Return(nil, nil)
// ルーター設定
r, apiV1 := initTestGin()
m := middleware.NewMiddleware()
h := NewUserHandler(mockUserUsecase)
apiV1.GET("/user/:uid", m.Auth(), h.FindByUID)
// リクエスト設定
path := "/api/v1/user/xxxx-xxxx-xxxx-0002"
req := httptest.NewRequest(http.MethodGet, path, nil)
req.Header.Set("Authorization", "Bearer xxxxxx")
// テストの実行
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// 検証
assert.Equal(t, http.StatusOK, w.Code)
var data map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &data)
assert.NoError(t, err)
assert.Empty(t, data)
})
t.Run("ユースケースでエラーが発生した場合にステータス500を返すこと", func(t *testing.T) {
// モック化
err := fmt.Errorf("Internal Server Error")
mockUserUsecase.EXPECT().FindByUID(gomock.Any(), gomock.Any()).Return(nil, err)
// ルーター設定
r, apiV1 := initTestGin()
m := middleware.NewMiddleware()
h := NewUserHandler(mockUserUsecase)
apiV1.GET("/user/:uid", m.Auth(), h.FindByUID)
// リクエスト設定
path := "/api/v1/user/xxxx-xxxx-xxxx-0002"
req := httptest.NewRequest(http.MethodGet, path, nil)
req.Header.Set("Authorization", "Bearer xxxxxx")
// テストの実行
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// 検証
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, w.Body.String(), "Internal Server Error")
})
t.Run("バリデーションチェックでエラーの場合にステータス422を返すこと", func(t *testing.T) {
// ルーター設定
r, apiV1 := initTestGin()
m := middleware.NewMiddleware()
h := NewUserHandler(mockUserUsecase)
apiV1.GET("/user/:uid", m.Auth(), h.FindByUID)
// リクエスト設定
path := "/api/v1/user/ "
req := httptest.NewRequest(http.MethodGet, path, nil)
req.Header.Set("Authorization", "Bearer xxxxxx")
// テストの実行
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// 検証
assert.Equal(t, http.StatusUnprocessableEntity, w.Code)
assert.Contains(t, w.Body.String(), "バリデーションエラー")
})
}
func TestUserHandler_Update(t *testing.T) {
// Ginのテストモードに設定
gin.SetMode(gin.TestMode)
// ユースケースのモック
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockUserUsecase := mockUser.NewMockUserUsecase(ctrl)
t.Run("ステータス200で正常終了すること", func(t *testing.T) {
// モック化
expectedUser := &domain_user.User{
ID: 1,
UID: "xxxx-xxxx-xxxx-0001",
LastName: "佐藤",
FirstName: "二郎",
Email: "z.satou@example.com",
CreatedAt: time.Time{},
UpdatedAt: time.Now(),
DeletedAt: nil,
}
mockUserUsecase.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(expectedUser, nil)
// ルーター設定
r, apiV1 := initTestGin()
h := NewUserHandler(mockUserUsecase)
apiV1.PUT("/user/:uid", h.Update)
// リクエスト設定
path := "/api/v1/user/xxxx-xxxx-xxxx-0001"
reqBody := UpdateUserRequestBody{
LastName: "佐藤",
FirstName: "二郎",
Email: "z.satou@example.com",
}
jsonReqBody, err := json.Marshal(reqBody)
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodPut, path, bytes.NewBuffer(jsonReqBody))
req.Header.Set("Authorization", "Bearer xxxxxx")
// テストの実行
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// 検証
assert.Equal(t, http.StatusOK, w.Code)
var data map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &data)
assert.NoError(t, err)
assert.NotContains(t, data, "id")
assert.NotNil(t, data["uid"])
assert.Equal(t, expectedUser.LastName, data["last_name"])
assert.Equal(t, expectedUser.FirstName, data["first_name"])
assert.Equal(t, expectedUser.Email, data["email"])
assert.NotNil(t, data["created_at"])
assert.NotNil(t, data["updated_at"])
assert.NotEqual(t, data["updated_at"], data["created_at"])
assert.Nil(t, data["deleted_at"])
})
t.Run("ユースケースでエラーが発生した場合にステータス500を返すこと", func(t *testing.T) {
// モック化
err := fmt.Errorf("Internal Server Error")
mockUserUsecase.EXPECT().Update(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, err)
// ルーター設定
r, apiV1 := initTestGin()
h := NewUserHandler(mockUserUsecase)
apiV1.PUT("/user/:uid", h.Update)
// リクエスト設定
path := "/api/v1/user/xxxx-xxxx-xxxx-0001"
reqBody := UpdateUserRequestBody{
LastName: "佐藤",
FirstName: "二郎",
Email: "z.satou@example.com",
}
jsonReqBody, err := json.Marshal(reqBody)
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodPut, path, bytes.NewBuffer(jsonReqBody))
req.Header.Set("Authorization", "Bearer xxxxxx")
// テストの実行
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// 検証
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, w.Body.String(), "Internal Server Error")
})
t.Run("UIDのバリデーションチェックでエラーの場合にステータス422を返すこと", func(t *testing.T) {
// ルーター設定
r, apiV1 := initTestGin()
h := NewUserHandler(mockUserUsecase)
apiV1.PUT("/user/:uid", h.Update)
// リクエスト設定
path := "/api/v1/user/ "
reqBody := UpdateUserRequestBody{
LastName: "佐藤",
FirstName: "二郎",
Email: "z.satou@example.com",
}
jsonReqBody, err := json.Marshal(reqBody)
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodPut, path, bytes.NewBuffer(jsonReqBody))
req.Header.Set("Authorization", "Bearer xxxxxx")
// テストの実行
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// 検証
assert.Equal(t, http.StatusUnprocessableEntity, w.Code)
assert.Contains(t, w.Body.String(), "バリデーションエラー")
})
t.Run("リクエストボディのバリデーションチェックでエラーの場合にステータス422を返すこと", func(t *testing.T) {
// ルーター設定
r, apiV1 := initTestGin()
h := NewUserHandler(mockUserUsecase)
apiV1.PUT("/user/:uid", h.Update)
// リクエスト設定
path := "/api/v1/user/xxxx-xxxx-xxxx-0001"
reqBody := UpdateUserRequestBody{
LastName: "佐藤",
FirstName: "",
Email: "z.satou@example.com",
}
jsonReqBody, err := json.Marshal(reqBody)
if err != nil {
t.Fatal(err)
}
req := httptest.NewRequest(http.MethodPut, path, bytes.NewBuffer(jsonReqBody))
req.Header.Set("Authorization", "Bearer xxxxxx")
// テストの実行
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// 検証
assert.Equal(t, http.StatusUnprocessableEntity, w.Code)
assert.Contains(t, w.Body.String(), "バリデーションエラー")
})
}
func TestUserHandler_Delete(t *testing.T) {
// Ginのテストモードに設定
gin.SetMode(gin.TestMode)
// ユースケースのモック
ctrl := gomock.NewController(t)
defer ctrl.Finish()
mockUserUsecase := mockUser.NewMockUserUsecase(ctrl)
t.Run("ステータス200で正常終了すること", func(t *testing.T) {
// モック化
date := time.Now()
dateString := date.Format("2006-01-02 15:04:05")
updateEmail := "z.satou@example.com" + dateString
expectedUser := &domain_user.User{
ID: 1,
UID: "xxxx-xxxx-xxxx-0001",
LastName: "佐藤",
FirstName: "二郎",
Email: updateEmail,
CreatedAt: time.Time{},
UpdatedAt: date,
DeletedAt: &date,
}
mockUserUsecase.EXPECT().Delete(gomock.Any(), gomock.Any()).Return(expectedUser, nil)
// ルーター設定
r, apiV1 := initTestGin()
h := NewUserHandler(mockUserUsecase)
apiV1.DELETE("/user/:uid", h.Delete)
// リクエスト設定
path := "/api/v1/user/xxxx-xxxx-xxxx-0001"
req := httptest.NewRequest(http.MethodDelete, path, nil)
req.Header.Set("Authorization", "Bearer xxxxxx")
// テストの実行
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// 検証
assert.Equal(t, http.StatusOK, w.Code)
var data map[string]interface{}
err := json.Unmarshal(w.Body.Bytes(), &data)
assert.NoError(t, err)
assert.NotContains(t, data, "id")
assert.NotNil(t, data["uid"])
assert.Equal(t, expectedUser.LastName, data["last_name"])
assert.Equal(t, expectedUser.FirstName, data["first_name"])
assert.Equal(t, expectedUser.Email, data["email"])
assert.NotNil(t, data["created_at"])
assert.NotNil(t, data["updated_at"])
assert.NotEqual(t, data["updated_at"], data["created_at"])
assert.NotNil(t, data["deleted_at"])
assert.Equal(t, data["deleted_at"], data["updated_at"])
})
t.Run("ユースケースでエラーが発生した場合にステータス500を返すこと", func(t *testing.T) {
// モック化
err := fmt.Errorf("Internal Server Error")
mockUserUsecase.EXPECT().Delete(gomock.Any(), gomock.Any()).Return(nil, err)
// ルーター設定
r, apiV1 := initTestGin()
h := NewUserHandler(mockUserUsecase)
apiV1.DELETE("/user/:uid", h.Delete)
// リクエスト設定
path := "/api/v1/user/xxxx-xxxx-xxxx-0001"
req := httptest.NewRequest(http.MethodDelete, path, nil)
req.Header.Set("Authorization", "Bearer xxxxxx")
// テストの実行
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// 検証
assert.Equal(t, http.StatusInternalServerError, w.Code)
assert.Contains(t, w.Body.String(), "Internal Server Error")
})
t.Run("バリデーションチェックでエラーの場合にステータス422を返すこと", func(t *testing.T) {
// ルーター設定
r, apiV1 := initTestGin()
h := NewUserHandler(mockUserUsecase)
apiV1.DELETE("/user/:uid", h.Delete)
// リクエスト設定
path := "/api/v1/user/ "
req := httptest.NewRequest(http.MethodDelete, path, nil)
req.Header.Set("Authorization", "Bearer xxxxxx")
// テストの実行
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
// 検証
assert.Equal(t, http.StatusUnprocessableEntity, w.Code)
assert.Contains(t, w.Body.String(), "バリデーションエラー")
})
}
レジストリ登録
次にルーター設定で使うためのレジストリ登録をするため、以下のコマンドを実行してファイルを作成します。
$ mkdir -p src/internal/registry && touch src/internal/registry/registry.go
次に作成したファイルを以下のように記述します。
・「src/internal/registry/registry.go」
package registry
import (
"context"
"fmt"
usecase_user "go-gin-domain/internal/application/usecase/user"
"go-gin-domain/internal/infrastructure/database"
"go-gin-domain/internal/infrastructure/logger"
persistence_user "go-gin-domain/internal/infrastructure/persistence/user"
handler_user "go-gin-domain/internal/presentation/handler/user"
)
// ハンドラーをまとめるコントローラー構造体
type Controller struct {
User handler_user.UserHandler
}
func NewController() *Controller {
// コンテキスト
ctx := context.Background()
// ロガー設定
logger := logger.NewSlogLogger()
// DB設定(今回はダミー設定とする)
cfg := database.DummyConfig{
Dummy: "dummy",
}
db_dummy, err := database.NewDummyConnection(cfg, logger)
if err != nil {
msg := fmt.Sprintf("エラー: %s", err.Error())
logger.Error(ctx, msg)
}
// userドメインのハンドラー設定
userRepo := persistence_user.NewUserRepository(db_dummy, logger)
userUsecase := usecase_user.NewUserUsecase(userRepo, logger)
userHandler := handler_user.NewUserHandler(userUsecase)
return &Controller{
User: userHandler,
}
}
※ルーター設定時にインスタンス化したハンドラーが必要になるため、このファイルでまとめるようにしています。
ルーター設定
次にルーター設定をするため、以下のコマンドを実行してファイルを作成します。
$ mkdir -p src/internal/presentation/router && touch src/internal/presentation/router/router.go
・「src/internal/presentation/router/router.go」
package router
import (
"go-gin-domain/internal/presentation/middleware"
"go-gin-domain/internal/registry"
"github.com/gin-gonic/gin"
)
func SetupRouter(c *registry.Controller, m *middleware.Middleware) *gin.Engine {
r := gin.New()
// 共通ミドルウェアの適用
r.Use(m.Request())
r.Use(m.CustomLogger())
r.Use(gin.Recovery())
// ルーティングの設定
apiV1 := r.Group("/api/v1")
apiV1.POST("/user", c.User.Create)
apiV1.GET("/users", m.Auth(), c.User.FindAll)
apiV1.GET("/user/:uid", m.Auth(), c.User.FindByUID)
apiV1.PUT("/user/:uid", m.Auth(), c.User.Update)
apiV1.DELETE("/user/:uid", m.Auth(), c.User.Delete)
return r
}
main.goの修正
次にファイル「src/main.go」を以下のように修正します。
・「src/main.go」
package main
import (
"fmt"
"log/slog"
"os"
"go-gin-domain/internal/presentation/middleware"
"go-gin-domain/internal/presentation/router"
"go-gin-domain/internal/registry"
"github.com/joho/godotenv"
)
func main() {
// .env ファイルの読み込み
err := godotenv.Load()
if err != nil {
slog.Error(".envファイルの読み込みに失敗しました。")
}
// ポート番号の設定
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
startPort := fmt.Sprintf(":%s", port)
// サーバー起動ログ出力
env := os.Getenv("ENV")
slog.Info(fmt.Sprintf("[ENV=%s] Start Gin Server Port: %s", env, port))
// サーバー起動
c := registry.NewController()
m := middleware.NewMiddleware()
r := router.SetupRouter(c, m)
r.Run(startPort)
}
次に以下のコマンドを実行し、フォーマット修正および静的コード解析を行い、警告が出ないことを確認します。
$ docker compose run --rm api go mod tidy
$ docker compose run --rm api go fmt ./...
$ docker compose run --rm api staticcheck ./...
コンテナの再ビルドと起動
次に以下のコマンドを実行し、コンテナを再びルドします。
$ docker compose down
$ docker compose build --no-cache
次に以下のコマンドを実行し、コンテナを起動します。
$ docker compose up -d
次に以下のコマンドを実行し、ログ出力を確認します。
$ docker compose logs
ログ出力を確認し、エラーがなければOKです。
ユーザードメインのAPIを試す
次に上記で作成したユーザードメインのAPIをPostmanを使って試します。
まずはPOSTメソッドで「http://localhost:8080/api/v1/user」を実行し、下図のようにステータスコード201で想定通りの結果になればOKです。
次にBearertトークン未設定でGETメソッドの「http://localhost:8080/api/v1/users」を実行し、下図のようにステータスコード401エラーになればOKです。
次にBearertトークンを設定してGETメソッドの「http://localhost:8080/api/v1/users」を実行し、下図のようにステータスコード200で想定通りの結果になればOKです。
次にBearertトークンを設定してGETメソッドの「http://localhost:8080/api/v1/user/{対象のuid}」を実行し、下図のようにステータスコード200で想定通りの結果になればOKです。
次にBearertトークンを設定してPUTメソッドの「http://localhost:8080/api/v1/user/{対象のuid}」を実行し、下図のようにステータスコード200で想定通りの結果になればOKです。
次にBearertトークンを設定してDELETEメソッドの「http://localhost:8080/api/v1/user/{対象のuid}」を実行し、下図のようにステータスコード200で想定通りの結果になればOKです。
テストコードのカバレッジを確認
次に上記で追加したハンドラーのテストも試してみますが、Goならオプション「-cover」を使ってカバレッジ率(テストがどれくらい網羅されているか)も確認することができます。
$ docker compose exec api go test -v $(docker compose exec api go list -f '{{if or .TestGoFiles .XTestGoFiles}}{{.ImportPath}}{{end}}' ./...)
また、以下のコマンドを実行し、カバレッジの対象箇所をファイル出力し、それをHTMLファイルに変換してブラウザで確認することも可能です。
$ docker compose exec api go test -v -coverprofile=internal/coverage.out $(docker compose exec api go list -f '{{if or .TestGoFiles .XTestGoFiles}}{{.ImportPath}}{{end}}' ./...)
$ docker compose exec api go tool cover -html=internal/coverage.out -o=internal/coverage.html
コマンド実行後、以下のようにファイルが作成されます。
ファイル「src/internal/coverage.html」をブラウザで確認すると下図のように表示されます。
左上のリストから対象のコードを選択可能で、カバー済みの箇所は緑色の文字で表示され、もし未対応の箇所があったら赤色の文字で表示されます。
※上図はVSCodeの拡張機能「HTML Preview」を使ってプレビューしています。
データベースやOpenAPIについて
今回はデータベースやOpenAPIに関する部分は省略しています。必要な場合は以下の記事を参考にしてみて下さい。

最後に
今回はGo言語のGinでDDD構成のバックエンドAPIを開発する方法について解説しました。
実際にGo言語の仕事を探すとDDD構成を採用しているケースが多く、実務においてはドメイン駆動設計での開発経験が求められることが多いと思います。
特にドメイン部分は専門性が問われるため、実際にはもっと複雑になると思いますが、基本的な開発方法についてはまとめられたと思うので、Go言語によるDDD(ドメイン駆動設計)について学びたい方はぜひ参考にしてみて下さい!
コメント