概要
golangでデータベースを使ってみようと思ったとき、調べてみるとmain()
の中でsql.Open()
をしている記事が多いです
実際に利用するときは、テストも必要となってくるので、ちょっと考えて実装する必要があります
今回、クリーンアーキテクチャっぽくDB部分をDIして、sqlmockを使ったテストを実装しました。
その結果、テスト書きやすーいってなりました
クリーンアーキテクチャ
簡単にいうと、アプリケーションを4つのレイヤーに分類して実装するアーキテクチャ。
- Frameworks & Drivers (フレームワークやデータベース、インフラストラクチャ)
- Interface Adapters (コントローラ、プレゼンテーション)
- Application Business Rules (ビジネスロジック)
- Enterprise Business Rules (エンティティ)
上のレイヤーから下のレイヤーに向かっては依存してもいいが、下のレイヤーから上のレイヤーに依存してはダメ
下のレイヤーから上のレイヤーを参照する場合は、インターフェースを利用する
参考
Goで書くClean Architecture API - Qiita
構成
.
├── entity # 一番内側の層(エンティティ)
│ └── entity.go
├── infrastructure # 一番外側の層(インフラストラクチャ)
│ ├── database # DBの実装
│ │ ├── database.go
│ │ └── database_test.go
│ └── infrastructure.go
├── interface # 外から2番目の層
│ ├── adapter # アダプタ
│ │ └── adapter.go
│ └── controller # コントローラ
│ └── controller.go
├── main.go
└── usecase # 外から3番目の層(ビジネスロジック)
└── usecase.go
実装
コードベースで説明していきます。
実際のコードはこちら
https://github.com/hirano00o/godbsample
このコードは、単純に名前と年齢をDBに書き込んで、読み出すサンプルとなります。
まずは、クリーンアーキテクチャに沿ってDB実装から追います。
// interfaceに注入するための構造体
type Server struct {
Conn *sql.DB
}
type Config struct {
User string
Password string
Host string
Port string
DBName string
}
// NewDBでMySQLへのコネクションをinterfaceであるadapter.DBに詰めて返す
func NewDB(cnf Config) adapter.DB { // adapter.DBはInterfaceAdapters層で定義したinterface
dbconf := mysql.Config{
User: cnf.User,
Passwd: cnf.Password,
Net: "tcp",
Addr: cnf.Host + ":" + cnf.Port,
DBName: cnf.DBName,
AllowNativePasswords: true,
}
db, err := sql.Open("mysql", dbconf.FormatDSN())
if err != nil {
log.Fatal(err)
}
handler := new(Server)
handler.Conn = db
// Server{mysql.Conn}をadapter.DB interfaceに注入
return handler
}
func (s *Server) Get(name string) ([][]interface{}, error) {
// select処理
}
adapter.DB
はどこで定義されているか、Get()
はどこで呼ばれているかというと...
// ↓adapter.DB
// このinterfaceにmysql.Connを注入する。中の関数は上のdatabase.goに実装がある。
type DB interface {
Set(map[string]string) error
Get(string) ([][]interface{}, error)
}
type UserAdapter struct {
DB // 上記のinterface
}
// usecase層から呼ばれる
func (a *UserAdapter) Find(u entity.User) ([]entity.User, error) {
// レシーバのinterfaceからdatabaseの実装を呼んでいる
users, err := a.Get(u.Name)
// 〜処理〜
}
adapterの関数は、usecaseにinterfaceが作成されて呼ばれている。
type UserUsecase struct {
Adp UserAdapter
}
// 内側の層から外側の層を参照するためにinterfaceを実装している
type UserAdapter interface {
Store(entity.User) error
Find(entity.User) ([]entity.User, error)
}
// controllerから呼ばれる
func (u *UserUsecase) ReadUser(name string) {
user := new(entity.User)
user.Name = name
// レシーバが持つアダプタのinterfaceを呼ぶ->Interface Adapters層の実装につながる
users, err := u.Adp.Find(*user)
// 〜処理〜
}
外から内側へは依存して良いため、usecaseの関数は、controllerから直接呼ばれる。
type UserController struct {
Interactor usecase.UserUsecase
}
// 各層の構造体を通して、DBの実装にコネクションを渡せるようにする
// 初期化時に実行する
func NewUserController(db adapter.DB) *UserController {
return &UserController{
Interactor: usecase.UserUsecase{
Adp: &adapter.UserAdapter{
DB: db,
},
},
}
}
func (u *UserController) Read(name string) {
// レシーバから依存しているusecase層の関数を呼ぶ
u.Interactor.ReadUser(name)
}
同様にinfrastructureからcontrollerを直接呼んでいる。
// main()から呼ぶ
func Route() {
conf := database.Config{
User: os.Getenv("DB_USER"),
Password: os.Getenv("DB_PASSWORD"),
Host: os.Getenv("DB_HOST"),
Port: os.Getenv("DB_PORT"),
DBName: os.Getenv("DB_NAME"),
}
// DBコネクション、コントローラを作成
c := controller.NewUserController(database.NewDB(conf))
users := []user{
user{"Alice", 10},
// 〜他のユーザ〜
}
// 依存しているコントローラの関数を呼ぶ
// TODO http handlerにして、プレゼンテーション層につなげるようにしたい
for _, u := range users {
c.Write(u.name, u.age)
}
for _, u := range users {
c.Read(u.name)
}
}
DBの単体テスト
sqlmock自体は、下記にexampleがあります。
DATA-DOG/go-sqlmock: Sql mock driver for golang to test database interactions
ざっくり書き方をコード中にコメントで説明していきます。
// まずはINSERT OKの場合
func TestOKSet(t *testing.T) {
db, mock, err := sqlmock.New() // 新規mockを作成
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
defer db.Close()
// DBの実装と同じ順序で、期待する操作をmockします
// 今回はトランザクションをBeginしてPreparedStatementを作成して、
// Execしてコミットしているので、下記の順序となる
mock.ExpectBegin()
mock.ExpectPrepare("INSERT INTO USERS"). // INSERTはテーブル名まで書く
ExpectExec(). // PreparedStatementを実行
WithArgs("Bob", 10). // INSERTする値、"Bob"、10歳
WillReturnResult( // 返り値はどうなるか
sqlmock.NewResult(1, 1) // 主キーの自動生成IDと実行して影響を受ける行数を指定
)
mock.ExpectCommit() // Commit
s := new(Server)
// mockで作成したDBを入れる
s.Conn = db
m := map[string]string{
"Name": "Bob",
"Age": "10",
}
if err := s.Set(m); err != nil {
t.Errorf("error was not expected while insert stats: %s", err)
}
// 上で作成したmockの期待は満たせているか確認
if err := mock.ExpectationsWereMet(); err != nil {
t.Errorf("there were unfulfilled expectations: %s", err)
}
}
// INSERT NGの場合(PreparedStatement失敗パターン)
func TestNGSetStmt(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
defer db.Close()
// 基本的には同じ
// PreparedStatementで失敗させたいので、Beginまで書く
mock.ExpectBegin()
s := new(Server)
s.Conn = db
err = s.Set(nil)
if err == nil {
t.Errorf("was expecting an error, but there was none")
}
// PreparedStatement以外で失敗していたらエラー
if strings.Contains(err.Error(), "Prepare") == false {
t.Errorf("was not expecting an error, but there was Prepare")
}
}
// SELECT OKのパターン
func TestOKGet(t *testing.T) {
// 〜mockの新規作成(上と同じ)〜
// 期待するクエリを書く
// QuoteMetaを利用すると簡単に書けるらしい
mock.ExpectQuery(
regexp.QuoteMeta(`SELECT NAME, AGE FROM USERS WHERE NAME = ?`),
).
WithArgs("Bob"). // NAME = ?に入れる値
WillReturnRows(sqlmock.NewRows([]string{"ID", "NAME", "AGE"}).
AddRow(1, "Bob", 15), // 返り値
)
s := new(Server)
s.Conn = db
res, err := s.Get("Bob")
if err != nil {
t.Fatalf("an error '%s' was not expected", err)
}
if len(res) != 1 {
t.Errorf("was not expecting count %d, but there was count 1", len(res))
}
// 〜いろいろ確認〜
}
// SELECT NGパターン...?
func TestNGGetConn(t *testing.T) {
// DBにアクセスしたかったけど、コネクションがないパターン
_, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
mock.ExpectQuery(
regexp.QuoteMeta(`SELECT NAME, AGE FROM USERS WHERE NAME = ?`),
).
WithArgs("Bob").
WillReturnRows(sqlmock.NewRows([]string{"ID", "NAME", "AGE"}).
AddRow(1, "Bob", 15),
)
s := new(Server)
_, err = s.Get("Bob")
if err != nil {
t.Fatalf("an error '%s' was not expected", err)
}
}
まとめ
ポイントとしては下記かなと
- SELECT等を実行する関数は、テストしやすいように小さくする
- コネクションは、レシーバか引数で渡せるようにする
- 今回は、DBの実装を間接的に呼ぶ層の構造体に、各層の構造体を通してDBのコネクションを渡した
- sqlmockは楽
今回使ったコードはこちらです
https://github.com/hirano00o/godbsample
やpull req頂けるとやる気モリモリになります!
golang weeklyでinterfaceの説明とDBのテストしている記事見つけたので共有しておきます
Interfaces Explained
TODO
- log出力じゃなくて、http handlerにしたい
- 他の層のテスト
- redisでの実装
- コード中のexportする関数等のコメント追加