🧅

Goず50%くらいの理解ではじめるクリヌンずいうかオニオンなアヌキテクチャ

に公開

積読しおいたClean Architecture本を読了したのですが、いたいち実践的なむメヌゞ湧かなかったため、オニオンアヌキテクチャを実際にGoで実装したずいう話です。

想定読者

  • クリヌンアヌキテクチャを孊習したけどいたいちピンずこなかった人
  • Goでレむダヌドアヌキテクチャを実装したい人
  • オニオンアヌキテクチャに぀いお知りたい人

本蚘事で説明しないこず

  • DDDに぀いおも少し觊れる予定ですが、DDDの詳现に぀いおは説明したせん
  • クリヌンアヌキテクチャの詳现

クリヌンアヌキテクチャがなぜピンずこないのか

個人的な感想です。

同心円の話になりがち

実際Clean Architecture本の䞭で、同心円を甚いたアヌキテクチャの話は数ペヌゞのみです。実際のプロゞェクトを䜜成しようずするず、具䜓的なディレクトリ構成やパッケヌゞの構成の話になりがちなためかず思いたすが少なくずもClean Architecture本で説明されおいるSOLID原則やコンポヌネントの安定床ず抜象床の話ずかず同心円のアヌキテクチャの話など含めおクリヌンアヌキテクチャずいう抂念です。(著者はそう理解したした。)

そしお、䞊蚘の同心円で重芁なこずは以䞋の2点であるずよく蚀われおいたす。

  • 䟝存関係は倖偎から内偎ぞの䞀方向のみに向かう
  • 内偎から倖偎ぞの䟝存は䟝存関係逆転の原則にしたがうこずで実珟する

そのため、同心円のような4局のレむダヌ構造でなくずも良いず曞かれおいたすし、その名称などもそこたで重芁ではず掚察できたす。

既にいろんなずころで蚀われおいたすがあたり同心円にこだわりすぎない方がクリヌンアヌキテクチャを掻甚しおいけるのではないかなず思いたした。

名称に惑わされる

䞊述した同心円の䞭心にぱンティティが存圚したすが、DDD(ドメむン駆動開発)の文脈でも゚ンティティずいう名称が䜿われおいたす。この二぀はドメむンを衚珟するずいう点ではたったくの別物でもない気がするのですがクリヌンアヌキテクチャずDDDずでやはり説明しおいる抂念が違うため、混同させるず混乱しおしたうかも知れたせん。(わたしはしたした。)

加えお、わたしはJVM系での開発をいたたでしたきたため、フレヌムワヌクずしおSpringを䜿甚しおきたのですが、その䞭でもEntityやRepository、Serviceずいう甚語を倚甚するこずになりたすが、これらもたたクリヌンアヌキテクチャやDDDで蚀われおいる甚語ずは異なる意味合いを持っおいたす。

もしかしたら、Ruby on Railsなどのフレヌムワヌクを䜿甚しおきた方ですずMVCアヌキテクチャず比范し、混同しおしたう方もいるかもしれたせん。

もし、クリヌンアヌキテクチャを孊習する䞊で他のアヌキテクチャず混同し混乱しおきたら名称は同じだが説明しおいるものは違うかも知れないずいうこずを意識するず理解が進むかも知れたせん。

以䞋の蚘事はアヌキテクチャずDDDに぀いおずおもわかりやすく解説されおいるので、興味がある方はぜひ読んでみおください。

https://little-hands.hatenablog.com/entry/2017/10/04/231743

具䜓的な実装䟋がない

少なくずも本曞にはないです。調べればいく぀か芋぀かりたすがどれもディレクトリ・パッケヌゞ構成が埮劙に違いたすし、名称もそれぞれで蚀語やフレヌムワヌクによる差異も出おきたす。

たた、SpringみたいなDIフレヌムワヌクを䜿甚するならあたり考えなくおもいいのですがGoなどで実装しようずするず自分でDIコンテナを実装する必芁があるので、そこの実装むメヌゞも湧かないず党䜓的な実装むメヌゞが湧かないず思いたす。

以䞋の蚘事はGoのクリヌンアヌキテクチャの実装䟋をたずめおくれおいるものです。

https://zenn.dev/naoki_kuroda/articles/8a7dc8dc10f5f9

なので、いざクリヌンアヌキテクチャの孊習を終えお、実装しおみようずなったずきに困惑したす。(わたしはしたした。)

繰り返しになりたすが、クリヌンアヌキテクチャずいうかあの同心円が䞀番䌝えたいこずはおそらく抂念的な話で名称や圢はそれほど重芁ではないため䞀番ピンずきた構成でやるのがいいず思っおいたす。

そこでオニオンアヌキテクチャ

クリヌンアヌキテクチャを孊んでピンずきた方はそのたたクリヌンアヌキテクチャを䜿甚すればいいず思いたす。ただ、もしわたしず同じようになんずなく理解したけど具䜓的な実装むメヌゞが湧かないずいう方はオニオンアヌキテクチャの方がピンずくるかもしれたせん。

オニオンアヌキテクチャずは

2008幎にJeffery Palermoが以䞋の蚘事で提唱したアヌキテクチャです。英語ですが文量はそこたで倚くないので興味がある方は読んでみるずおもしろいず思いたす。

https://jeffreypalermo.com/2008/07/the-onion-architecture-part-1/

以䞋の蚘事が倧倉わかりやすいのでこちらを参照。

https://qiita.com/little_hand_s/items/2040fba15d90b93fc124

オニオンアヌキテクチャだず䜕がうれしいのか

これもいろんなずころで蚀われおいたすがオニオンアヌキテクチャもクリヌンアヌキテクチャも基本的な抂念は䞀緒です。どちらも目的は関心ごずの分離です。

䞀応、䞊蚘オニオンアヌキテクチャに぀いお曞かれたブログ蚘事内で説明されおいるオニオンアヌキテクチャの定矩的なもの。

Key tenets of Onion Architecture:

  • The application is built around an independent object model
  • Inner layers define interfaces. Outer layers implement interfaces
  • Direction of coupling is toward the center
  • All application core code can be compiled and run separate from infrastructure

オニオン・アヌキテクチャの䞻芁な考え方

  • アプリケヌションは、独立したオブゞェクト・モデルを䞭心に構築される
  • 内偎のレむダヌはむンタヌフェヌスを定矩したす。倖偎のレむダヌはむンタヌフェヌスを実装する
  • 結合の方向は䞭心に向かっおいる
  • すべおのアプリケヌションのコアコヌドは、むンフラストラクチャずは別にコンパむルしお実行するこずができる

オニオンアヌキテクチャの図にあるレむダヌを芋おみるず䞀番倖偎にtestsずinfrastructure、user interfaceがありたす。testずDBなどの具䜓的な実装が含たれるinfrastructure局が䞀番倖偎にあるのは理解できるでしょう。

次にApplication ServicesずDomain(Object) Servicesレむダヌがあり、ぱっず芋違いがわかりたせんが図の䟋にRepositoryむンタヌフェむスがDomainServicesレむダヌにあるのをみるずDB実装のむンタヌフェむスをDomain Serviceレむダヌに眮けばずりあえず良さそうです。

Application ServiceにはDomain Serviceを䜿甚し適切にドメむン操䜜の取りたずめずトランザクションなどの凊理を実斜すればよさそうです。Spring経隓者の方であればいわゆるServiceアノテヌションを付けるクラスで䌝わるでしょう。

䞭心のDomain(Object) Modelはアプリケヌションのコアずなるビゞネスモデル的抂念がここに圓おはたるでしょう。この郚分は最もアプリケヌションに圱響のあるレむダヌのため䜕にも䟝存しおいたせん。

ずなるずControllerず呌ばれる郚分の実装はどこやねんずなるのですが、オニオンアヌキテクチャの図を芋おみるず䞀番倖偎のuser interfaceレむダに眮かれるこずになりたす。Controllerの実装がinfrastructureず同じ䞀番倖偎にあるのに違和感を感じたしたが、オニオンアヌキテクチャが提唱されおいる蚘事内では

CodeCampServerはASP.NET MVC Frameworkを䜿甚しおいるので、SpeakerControllerはナヌザヌむンタヌフェむスの䞀郚ずなりたす。 このコントロヌラはASP.NET MVC Frameworkず結合しおおり、これを回避するこずはできない。(日本語蚳)

ずありたす。Controllerの実装はフレヌムワヌクに匷く䟝存しおいるので䞀番倖偎にあるずいうこずですね。これはSpringやRuby on Railsずいったフレヌムワヌクでも同じこずが蚀えるでしょう。

どうでしょう、クリヌンアヌキテクチャの同心円より実装のむメヌゞが぀きたせんか前述したQiitaの蚘事にもありたしたが個人的にはクリヌンアヌキテクチャのUse CasesレむダずInterface Adaptersレむダに䜕をどこに眮いたらいいのかが結構ひずそれぞれな感がするのず名称もPresentersやControllerずいったものもありディレクトリ名称もプロゞェクトによっお倉わるのでこれが正解だよみたいなのがないのがわかりづらいず思っおたす。

オニオンアヌキテクチャもこれが正解ですみたいなのは圓然ないのですが、ただクリヌンアヌキテクチャよりは遞択肢が少なくわかりやすいかなずいうこずです。

実装しおみる

Goで簡単なTodo APIをオニオンアヌキテクチャで䜜成したす。

䜜成した成果物はこちら

https://github.com/JY8752/go-onion-architecture-sample

ディレクトリ構成は以䞋のような感じになりたした。詳しくは埌述したす。

.
├── README.md
├── application
│   └── service
├── common
│   ├── todo.go
│   └── user.go
├── domain
│   ├── model
│   └── repository
├── go.mod
├── go.sum
├── go_onion_architecture.db
├── infrastructure
│   ├── db.go
│   └── repository
├── main.go
├── mocks
│   ├── mock_repository
│   ├── registory
│   └── service
├── registory
│   └── registory.go
├── test
│   └── container.go
├── testdata
│   └── golden
└── userinterface
    ├── echo.go
    ├── handler
    ├── request
    └── response

domain.model

オニオンアヌキテクチャの図の最も䞭心のDomain Modelの郚分です。今回はシンプルに以䞋のようなモデルを䜜成したした。

todo.go
package model

import "time"

type TodoId int64
type Title string
type Description string

type Todo struct {
	Id          TodoId      `json:"id"`
	Title       Title       `json:"title"`
	Description Description `json:"description"`
	CreatedAt   time.Time   `json:"created_at"`
	DeleteAt    time.Time   `json:"delete_at"`
}

これがアプリケヌションのコアずなるビゞネスモデルになりたす。オニオンアヌキテクチャの図の最も䞭心の抂念のためどこにも䟝存しおおらず、他のレむダから䟝存されるこずになる郚分です。

そのため、他のレむダの倉曎の圱響を受けず、逆にこのモデルの倉曎は他の党おの䟝存レむダに圱響をあたえるこずになりたす。

domain.repository

ここはDomain Servicesレむダです。レむダの名称はServiceですがRepositoryずいう名称に銎染みがあるのでdomainディレクトリの配䞋にrepositoryずいうディレクトリを䜜成し以䞋のようなむンタヌフェむスを配眮したした。

todo.go
package repository

import "github.com/JY8752/go-onion-architecture-sample/domain/model"

type TodoRepository interface {
	Create(model.UserId, model.Title, model.Description) (model.TodoId, error)
	List(model.UserId) ([]model.Todo, error)
	Delete(model.TodoId) error
}

このレむダにはSQLなどの具䜓的な実装を知っおはいけないのでむンタヌフェむスのみを配眮したす。

application.service

ここはApplication Servicesレむダになりたす。Repositoryむンタヌフェむスを䜿甚しおビゞネスモデルの氞続化や取埗などを実斜したす。今回の䟋ではほずんどロゞック的なものはありたせんがここにサヌビスロゞック的なものがくる想定です。

todo.go
package service

import (
	"github.com/JY8752/go-onion-architecture-sample/domain/model"
	"github.com/JY8752/go-onion-architecture-sample/domain/repository"
)

type TodoService interface {
	Create(model.UserId, model.Title, model.Description) (model.TodoId, error)
	List(model.UserId) ([]model.Todo, error)
	Delete(model.TodoId) error
}

type todoService struct {
	todoRep repository.TodoRepository
}

func NewTodoService(todoRep repository.TodoRepository) TodoService {
	return &todoService{
		todoRep: todoRep,
	}
}

func (t *todoService) Create(userId model.UserId, title model.Title, description model.Description) (model.TodoId, error) {
	return t.todoRep.Create(userId, title, description)
}

func (t *todoService) List(userId model.UserId) ([]model.Todo, error) {
	return t.todoRep.List(userId)
}

func (t *todoService) Delete(todoId model.TodoId) error {
	return t.todoRep.Delete(todoId)
}

このServiceもむンタヌフェむスず実装があり、配眮堎所に悩んだのですが今回は同じレむダに配眮したした。オニオンアヌキテクチャでは倖偎のレむダに実装、内偎にむンタヌフェむスずいうポむントがあるので内偎のDomain Serviceレむダにむンタヌフェむスを配眮しおもいいのかもしれたせん。

infrastructure

䞀番倖偎のレむダでDBの具䜓的な詳现を実装する堎所です。今回はsqlite3を䜿甚しお実装したした。

todo.go
package infrastructure

import (
	"log"
	"time"

	"github.com/JY8752/go-onion-architecture-sample/domain/model"
	"github.com/JY8752/go-onion-architecture-sample/domain/repository"
	db "github.com/JY8752/go-onion-architecture-sample/infrastructure"
)

type todoRepository struct {
	dbClient *db.DBClient
}

func NewTodoRepository(db *db.DBClient) repository.TodoRepository {
	return &todoRepository{
		dbClient: db,
	}
}

func (t *todoRepository) Create(userId model.UserId, title model.Title, description model.Description) (model.TodoId, error) {
	stmt, err := t.dbClient.Client.Prepare("INSERT INTO todos (user_id, title, description, created_at) VALUES (?, ?, ?, ?)")
	if err != nil {
		return 0, err
	}

	result, err := stmt.Exec(userId, title, description, time.Now())
	if err != nil {
		return 0, err
	}

	id, err := result.LastInsertId()
	if err != nil {
		return 0, err
	}

	return model.TodoId(id), nil
}

func (t *todoRepository) List(id model.UserId) ([]model.Todo, error) {
	stmt, err := t.dbClient.Client.Prepare("SELECT id, title, description, created_at FROM todos WHERE user_id = ? AND delete_at IS NULL")
	if err != nil {
		return nil, err
	}

	rows, err := stmt.Query(id)
	if err != nil {
		return nil, err
	}

	var todos []model.Todo
	for rows.Next() {
		var todo model.Todo
		err = rows.Scan(&todo.Id, &todo.Title, &todo.Description, &todo.CreatedAt)
		if err != nil {
			log.Printf("err: %s\n", err.Error())
			continue
		}
		todos = append(todos, todo)
	}

	return todos, nil
}

func (t *todoRepository) Delete(id model.TodoId) error {
	stmt, err := t.dbClient.Client.Prepare("UPDATE todos SET delete_at = ? WHERE id = ?")
	if err != nil {
		return err
	}

	_, err = stmt.Exec(time.Now(), id)
	if err != nil {
		return err
	}

	return nil
}

もしDBをsqlite3からMongoに倉曎だったり、䜿甚するORMを倉曎するこずになった堎合にこのinfrastructureの実装をたるっず䜜り替えるだけですむように意識しお実装するずいいず思いたす。

user interface

いわゆるControllerずしお実装される凊理です。今回はechoを䜿甚しお実装したした。

todo.go
package handler

import (
	"log"

	"github.com/JY8752/go-onion-architecture-sample/common"
	"github.com/JY8752/go-onion-architecture-sample/domain/model"
	"github.com/JY8752/go-onion-architecture-sample/registory"
	"github.com/JY8752/go-onion-architecture-sample/userinterface/request"
	"github.com/JY8752/go-onion-architecture-sample/userinterface/response"
	"github.com/labstack/echo/v4"
)

func TodoHandler(client *echo.Echo, registory registory.Registory) {
	client.POST("/:userId/todos", func(c echo.Context) error {
		// バリデヌション
		_, err := common.GetUserId(c.Param("userId"))
		if err != nil {
			return err
		}

		var r request.CreateTodoRequest
		if err := c.Bind(&r); err != nil {
			return err
		}

		id, err := registory.TodoService().Create(
			model.UserId(r.UserId),
			model.Title(r.Title),
			model.Description(r.Description),
		)
		if err != nil {
			return err
		}

		return c.JSON(200, response.CreateTodoResponse{Id: id})
	})

	client.GET("/:userId/todos", func(c echo.Context) error {
		userId, err := common.GetUserId(c.Param("userId"))
		if err != nil {
			return err
		}

		todos, err := registory.TodoService().List(userId)
		if err != nil {
			log.Printf("err: %s\n", err.Error())
			return c.JSON(404, []model.Todo{})
		}

		return c.JSON(200, response.GetTodosResponse{Todos: todos})
	})

	client.DELETE("/todos/:id", func(c echo.Context) error {
		todoId, err := common.GetTodoId(c.Param("id"))
		if err != nil {
			return err
		}

		err = registory.TodoService().Delete(todoId)
		if err != nil {
			return err
		}

		return c.NoContent(204)
	})
}

ここが䞀番悩んだんですがなるべくechoを切り捚おやすくしたかったのですが、どうしおもechoの実装に䟝存しおしたうので結果的にこのような実装になりたしたがもっずいい感じの実装があるず思いたす、たぶん。

registory

ここはオニオンアヌキテクチャは関係ないのですが、echoのhandler関数からService -> Repositoryず呌び出しおいくのに䟝存関係の泚入を行う必芁があり、そのためのDIコンテナの実装です。

Springなどのフレヌムワヌクならばフレヌムワヌク偎がいい感じにやっおくれたすがGoの堎合自䜜するかwireなどのモゞュヌルを䜿甚する必芁がありたす。

registory.go
package registory

import (
	service "github.com/JY8752/go-onion-architecture-sample/application/service"
	repository "github.com/JY8752/go-onion-architecture-sample/domain/repository"
	db "github.com/JY8752/go-onion-architecture-sample/infrastructure"
	infrastructure "github.com/JY8752/go-onion-architecture-sample/infrastructure/repository"
)

type Registory interface {
	UserRep() repository.UserRepository
	UserService() service.UserService
	TodoRep() repository.TodoRepository
	TodoService() service.TodoService
}

type registory struct {
	dbClient *db.DBClient
}

func NewRegistory(db *db.DBClient) Registory {
	return &registory{
		dbClient: db,
	}
}

func (r *registory) UserRep() repository.UserRepository {
	return infrastructure.NewUserRepository(r.dbClient)
}

func (r *registory) UserService() service.UserService {
	return service.NewUserService(r.UserRep())
}

func (r *registory) TodoRep() repository.TodoRepository {
	return infrastructure.NewTodoRepository(r.dbClient)
}

func (r *registory) TodoService() service.TodoService {
	return service.NewTodoService(r.TodoRep())
}

registoryの実装はアヌキテクチャの䞀番倖偎もしくは円の倖偎から党おのレむダに䟝存しおいるむメヌゞで倧䞈倫だず思いたす。実装は以䞋の蚘事を参考にさせおいただきたした。

https://moneyforward-dev.jp/entry/2021/03/08/go-test-mock/

main

これでだいたい実装は完了です。最埌にmain関数は以䞋のようになりたした。

main.go
package main

import (
	db "github.com/JY8752/go-onion-architecture-sample/infrastructure"
	"github.com/JY8752/go-onion-architecture-sample/registory"
	ui "github.com/JY8752/go-onion-architecture-sample/userinterface"
)

func main() {
	// db
	db := db.NewDBClient("./go_onion_architecture.db")
	defer db.Client.Close()

	// registory
	registory := registory.NewRegistory(db)

	// echo
	apiClient := ui.NewApiClient(registory)
	apiClient.RegisterRoute()
	apiClient.Start()
}

その他

今回、共通凊理的なものをcommonディレクトリを䜜成し配眮したしたが、このようなナヌティリティは䟋倖的に䞀番倖偎のレむダにしたした。

ただ、呌び出し偎が党おこのナヌティリティに䟝存するこずになるのずそもそもナヌティリティを䜜るか䜜らないかみたいな話になりそうなのでこれも適切ではないかもしれたせん。

Clean Architecture本にはこのようなナヌティリティはあらゆる箇所から呌ばれる可胜性があるため安定床が高く、抜象床は䜎く、倉曎がされにくいコンポヌネントであるず曞かれおいたす。

もしかしたら、眮くにしおも䞭心のdomainレむダに眮く方が適切かもしれたせん。

あずは、config系や定数、キャッシュなどをどこに眮くかみたいな話が実プロゞェクトでは出おきそうですが基本的にはアプリケヌションのロゞックずは無関係で倉曎の可胜性があるものは倖偎に、そうでなければ内偎に眮くような意識で実装すればいいず思いたす。

テストを曞く

クリヌンアヌキテクチャやオニオンアヌキテクチャのメリットずしおそれぞれのレむダでテストが曞きやすくなるずいった点があるのでテストも曞いおいきたす。

Clean Architecture本に「テストもシステムの䞀郚であり、テスト察象の実装に匷く䟝存しおいる」ずありたす。テストが実装に䟝存しおいれば圓然、実装に倉曎があった堎合にテストも圱響を受けるため修正する必芁がでおきたす。

テストがすぐ壊れるず蚀われるのはこのような理由からでしょう。そのため、テストを壊れにくくするためになるべく実装に䟝存しないようにする方がいいず著者は思っおいたす。぀たり、mockを䜿おうねずいうこずです。

mockを䜿う䜿わないは意芋がわかれるずころでもあるず思いたすが、著者は䞊蚘のような理由から他のレむダぞの䟝存関係はmockにし、そのレむダの責務にのみに焊点を圓おお単䜓テストを曞くこずにしおいたす。

infrastructureのテスト

今回はsqlite3を䜿甚しおいるので、むンメモリのDBをテスト甚に起動しテストを実行したす。

todo_test.go
package infrastructure_test

import (
	"os"
	"testing"

	"github.com/JY8752/go-onion-architecture-sample/domain/model"
	"github.com/JY8752/go-onion-architecture-sample/domain/repository"
	db "github.com/JY8752/go-onion-architecture-sample/infrastructure"
	infrastructure "github.com/JY8752/go-onion-architecture-sample/infrastructure/repository"
	"github.com/stretchr/testify/assert"
)

var todoRep repository.TodoRepository

func TestMain(m *testing.M) {
	d := db.NewDBClient("file:infrastructure_test_db?mode=memory")
	todoRep = infrastructure.NewTodoRepository(d)

	code := m.Run()

	d.Client.Close() // Exitするずdeferが実行されないので
	os.Exit(code)
}

func TestCreate(t *testing.T) {
	// when
	userId := model.UserId(1)
	id, err := todoRep.Create(userId, "title", "description")
	if err != nil {
		t.Fatal(err)
	}

	// then
	todos, err := todoRep.List(userId)
	if err != nil {
		t.Fatal(err)
	}

	assert.Equal(t, 1, len(todos))
	assert.Equal(t, id, todos[0].Id)
	assert.Equal(t, model.Title("title"), todos[0].Title)
	assert.Equal(t, model.Description("description"), todos[0].Description)
}

もし、MySQLなどのDBのテストをする堎合、テスト実行時にコンテナの起動・停止をコヌド䞊で扱えるdockertestなどがおすすめです。

もし興味があれば以䞋の蚘事が参考になるかもしれたせん

https://zenn.dev/jy8752/articles/419ab77b2b6a61

serviceのテスト

ここではRepositoryの実装に䟝存したくないのでRepositoryはmockを䜿甚したす。今回はgomockを䜿甚したした。

todo_test.go
package service_test

import (
	"testing"

	"github.com/JY8752/go-onion-architecture-sample/application/service"
	"github.com/JY8752/go-onion-architecture-sample/domain/model"
	"github.com/JY8752/go-onion-architecture-sample/mocks/mock_repository"
	"github.com/golang/mock/gomock"
	"github.com/stretchr/testify/assert"
)

func TestCreate(t *testing.T) {
	// given
	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	m := mock_repository.NewMockTodoRepository(ctrl)

	m.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()).Return(model.TodoId(1), nil)

	ts := service.NewTodoService(m)

	// when
	result, err := ts.Create(1, model.Title("title"), model.Description("description"))
	if err != nil {
		t.Fatal(err)
	}

	// when
	assert.Equal(t, model.TodoId(1), result)
}

handlerのテスト

サヌビスのテスト同様、サヌビスの実装に䟝存したくないのでmockを䜿甚したす。

user_test.go
package handler_test

import (
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/JY8752/go-onion-architecture-sample/domain/model"
	mock_registory "github.com/JY8752/go-onion-architecture-sample/mocks/registory"
	mock_service "github.com/JY8752/go-onion-architecture-sample/mocks/service"
	"github.com/JY8752/go-onion-architecture-sample/userinterface/handler"
	"github.com/golang/mock/gomock"
	"github.com/labstack/echo/v4"
	"github.com/sebdah/goldie/v2"
	"github.com/stretchr/testify/assert"
)

const (
	goldenDir = "../../testdata/golden/"
)

func TestCreateUserHandler(t *testing.T) {
	// given
	e := echo.New()

	ctrl := gomock.NewController(t)
	defer ctrl.Finish()

	// ゚ンドポむントの登録
	service := mock_service.NewMockUserService(ctrl)
	service.EXPECT().Create("user1").Return(model.UserId(1), nil)

	registory := mock_registory.NewMockRegistory(ctrl)
	registory.EXPECT().UserService().Return(service)

	handler.CreateUserHandler(e, registory)

	// リク゚ストの䜜成
	body := `{"name": "user1"}`
	w := httptest.NewRecorder()
	r := httptest.NewRequest(echo.POST, "/users", strings.NewReader(body))
	r.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
	defer r.Body.Close()

	// when
	e.ServeHTTP(w, r)

	// then
	assert.Equal(t, 200, w.Code)
	g := goldie.New(t, goldie.WithFixtureDir(goldenDir))
	g.Assert(t, t.Name(), w.Body.Bytes())
}

handlerのテストはgolden testで実装したした。レスポンスのJSONの項目が増えおくるずアサヌションが倧倉なため期埅する情報をファむルで管理できるgolden testずしおテストを曞くこずで楜にテストを曞くこずができたした。

Goでgolden testを曞くためのモゞュヌルはいく぀かありたしたが今回はgoldieを䜿甚したした。

たずめ

䜕床も蚀うようですがクリヌンアヌキテクチャもオニオンアヌキテクチャも重芁なこずはレむダを分けるこずずそれぞれのレむダの䟝存関係の方向です。目的は関心ごずの分離であり倉曎に匷いシステムを䜜るこずです。

そのため、具䜓的なこれが正解ですずいったものはなくプロゞェクトや組織、䜿甚する蚀語によっおも䜜り方は倉わっおいくず思いたす。

抜象的な抂念なので完党に理解するこずは難しいため、たずは自分のスタむルで玍埗感のあるものをたずは䜜っおみるず理解に぀ながるかもしれたせん。

ずにかく、重芁だなず思ったこずはフレヌムワヌクやDBなど倖郚䟝存の郚分はい぀でも捚おられるように実装するこずです。たた、抂念を理解しなければ実装しおいおも腑に萜ちないず思いたすのでClean Architecuture本をただ読んだない方はぜひ読んでみるこずをおすすめしたす。

今回は以䞊です🐌

GitHubで線集を提案

Discussion