DIを用いたDB(MySQL)の単体テストに #sqlmock を使い、DIのテストしやすさを試す #golang #初心者

# 概要
golangでデータベースを使ってみようと思ったとき、調べてみると`main()`の中で`sql.Open()`をしている記事が多いです:ghost:
実際に利用するときは、テストも必要となってくるので、ちょっと考えて実装する必要があります:thinking:
今回、クリーンアーキテクチャっぽくDB部分をDIして、sqlmockを使ったテストを実装しました。
その結果、テスト書きやすーいってなりました:innocent:

# クリーンアーキテクチャ
簡単にいうと、アプリケーションを４つのレイヤーに分類して実装するアーキテクチャ。

- Frameworks & Drivers (フレームワークやデータベース、インフラストラクチャ)
- Interface Adapters (コントローラ、プレゼンテーション)
- Application Business Rules (ビジネスロジック)
- Enterprise Business Rules (エンティティ)

上のレイヤーから下のレイヤーに向かっては依存してもいいが、下のレイヤーから上のレイヤーに依存してはダメ:skull:
下のレイヤーから上のレイヤーを参照する場合は、インターフェースを利用する:thumbsup:

#### 参考
[Goで書くClean Architecture API - Qiita](https://qiita.com/muroon/items/8add8da911341312176d)

# 構成

```
.
├── entity                    # 一番内側の層(エンティティ)
│   └── entity.go
├── infrastructure            # 一番外側の層(インフラストラクチャ)
│   ├── database              # DBの実装
│   │   ├── database.go
│   │   └── database_test.go
│   └── infrastructure.go
├── interface                 # 外から２番目の層
│   ├── adapter               # アダプタ
│   │   └── adapter.go
│   └── controller            # コントローラ
│       └── controller.go
├── main.go
└── usecase                   # 外から3番目の層(ビジネスロジック)
    └── usecase.go
```
# 実装
コードベースで説明していきます。
:point_down::point_down::point_down::point_down:実際のコードはこちら:point_down::point_down::point_down::point_down:
https://github.com/hirano00o/godbsample
:point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2::point_up_2:

このコードは、単純に名前と年齢をDBに書き込んで、読み出すサンプルとなります。

まずは、クリーンアーキテクチャに沿ってDB実装から追います。

```golang:infrastructure/database/database.go
// 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()`はどこで呼ばれているかというと...

```golang:interface/adapter/adapter.go
// ↓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が作成されて呼ばれている。

```golang:usecase/usecase.go
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から直接呼ばれる。

```golang:interface/controller/controller.go
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を直接呼んでいる。

```golang:infrastructure/infrastructure.go
// 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](https://github.com/DATA-DOG/go-sqlmock)
ざっくり書き方をコード中にコメントで説明していきます。

```golang:infrastructure/database/database_test.go
// まずは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"、１０歳
            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は楽

今回使ったコードはこちらです
:point_right: https://github.com/hirano00o/godbsample
:star2: やpull req頂けるとやる気モリモリになります！:muscle:

[golang weekly](https://golangweekly.com/)でinterfaceの説明とDBのテストしている記事見つけたので共有しておきます
[Interfaces Explained](https://www.alexedwards.net/blog/interfaces-explained)

#### TODO

- log出力じゃなくて、http handlerにしたい
- 他の層のテスト
- redisでの実装
- コード中のexportする関数等のコメント追加

# 参考
[golangでMySQLのTransactionを実装してみる - Qiita](https://qiita.com/fiemon/items/eb38c8d681ed1ae05925)