コアテクノロジープラットフォーム部・プラットフォームチームの金丸です。
主にgolangを使用したAPI開発を担当しております。
さて現在、一部のプロジェクトでDB等の外部リソースにアクセスするテストを行う際、弊社ではdockertestを使用しています。
コンテナを立ち上げる事で外部リソースへのテストを行う事ができ、便利なのは良いのですが
現状全てのunitテストで使用されているため、上位packageのunit test内でもDBの設定が必要な状態になっています。
そのためmockで置き換える事ができるサービスロケーターパターンを使用した実装へ変更する事にします。
- DB
- プロジェクトを作成する
- DBにアクセスするpackageを作る
- DBにアクセスするpackageを動かしてみる
- DBにアクセスするpackageを呼ぶpackageを作る
- DBにアクセスするpackageをmock化して呼ぶpackageのテストを作る
DB
mysql> desc users; +-------+------------------+------+-----+---------+-------+ | Field | Type | Null | Key | Default | Extra | +-------+------------------+------+-----+---------+-------+ | id | int(10) unsigned | NO | PRI | NULL | | | name | varchar(255) | NO | | NULL | | +-------+------------------+------+-----+---------+-------+ 2 rows in set (0.03 sec) mysql> select * from users; +----+------+ | id | name | +----+------+ | 1 | test | +----+------+ 1 row in set (0.00 sec)
通常時の確認のためDBを準備します。
idとnameだけのシンプルなテーブルで、テストデータを1レコードだけ投入してあります。
プロジェクトを作成する
~go/src/github.com/hogehoge/locator-go ┣ repositories ┃ ┣ db.go ┃ ┗ db_test.go ┗services ┣ services.go ┗ services_test.go
プロジェクトを作成します。repositories/db.goがDBとアクセスするpackageで、services.service.goが前述のpackageを呼び出します。
DBにアクセスするpackageを作る
db.go
package repositories import ( "context" "database/sql" ) // Repositoriesで実装しているインターフェイス type Repositories interface { Search(ctx context.Context, id uint64) (string, error) NotUseFunc() } // Repositories内のメソッドを使うための構造体 type Repository struct { DB *sql.DB } // 新規Repositoryオブジェクト作成 func NewRepository(db *sql.DB) Repository { return Repository{ DB: db, } } // DBから検索するメソッド func (r Repository) Search(ctx context.Context, id uint64) (name string, err error) { err = r.DB.QueryRow("SELECT name FROM students WHERE id = ? LIMIT 1", id).Scan(&name) return } // mockで置き換えないメソッド func (r Repository) NotUseFunc() { return }
やってる事はシンプルなSQLを実行しているだけです。
この時実装しているメソッドはRepositoryをレシーバとして設定してあり、これらはRepositoriesインターフェイスを満たしています。
DBにアクセスするpackageを動かしてみる
db_test.go
package repositories import ( "context" "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" "testing" ) func TestRepositories_Search(t *testing.T) { var ( cnn *sql.DB err error name string ) cnn, err = sql.Open("mysql", "user:password@tcp(host:port)/db_name") if err != nil { panic(err) } name, err = NewRepository(cnn).Search(context.TODO(), 1) if err != nil { panic(err) } fmt.Printf("name = %s\n", name) }
=== RUN TestRepositories_Search name = test --- PASS: TestRepositories_Search (0.02s)
簡易テストですので標準出力でログを出しています。
DBの値が取れている事が確認できます。
DBにアクセスするpackageを呼ぶpackageを作る
services.go
package services import ( "context" "github.com/hogehoge/locator-go/repositories" ) // Servicesで実装しているインターフェイス type Services interface { SearchService(ctx context.Context, id uint64) (name string, err error) } // Services内のメソッドを使うための構造体 type Service struct { Repository repositories.Repositories } // 新規Serviceオブジェクト作成 func NewService(rep repositories.Repositories) Service { return Service{ Repository: rep, } } // Repository.Search()を呼ぶメソッド func (s Service) SearchService(ctx context.Context, id uint64) (string, error) { return s.Repository.Search(ctx, id) }
DBアクセス時と同様にpackageを作成します。
この時レシーバにrepositories.Repositoriesインターフェイス型の変数を実装しておき、メソッド内部ではそれを使用してSearch()を呼ぶ様にしています。
DBにアクセスするpackageをmock化して呼ぶpackageのテストを作る
services_test.go
package services import ( "context" "fmt" "github.com/hogehoge/locator-go/repositories" "testing" ) // 置き換えるmockオブジェクト type mockRepository struct { repositories.Repositories // インターフェイス埋め込み rName string // result用name変数 rErr error // result用err変数 } // overrideするメソッド func (m mockRepository) Search(ctx context.Context, id uint64) (string, error) { return m.rName, m.rErr } func TestService_SearchService(t *testing.T) { mock := mockRepository{ rName: "mockName", } // mockで置き換える service := NewService(mock) // mockが渡されているためoverrideしたメソッドが呼ばれる name, err := service.SearchService(context.TODO(), 1) if err != nil { panic(err) } if name != mock.rName { panic("name is not equal") } fmt.Printf("name = %s\n", name) }
- 置き換えるmockオブジェクトを作成します
- この時インターフェイスを埋め込んでおく事で、テストに関係のないメソッドを実装しなくて良くなります
- mock化するメソッドを作成する
- mockオブジェクトをレシーバとするメソッドを作成します。インターフェイス通りに実装しましょう
- 結果を入れる変数を返す様にすればテスト毎に望む結果が返せて便利です
- テストを実装する
- Service.Repositoryオブジェクトにmockを入れます
- mockが入ったService.RepositoryをNewServiceに受け渡します
- これ以降、テスト内ではmock内のメソッドが呼ばれるようになります
=== RUN TestService_SearchService name = mockName --- PASS: TestService_SearchService (0.00s)
これでRepositoriesを呼んでいるパッケージはテスト時にmockに置き換えられる様になりました。
ビューなどを実装する場合は、さらにServicesを同じ要領で置き換えてください。
これで実際にDBに繋げてテストする部分はrepositoriesだけになります。
もっと良いやり方や上手い実装があればウチに入社してこっそりマージリクエストください。