【OSSから学ぶ】Golang製ライブラリDATA-DOG/go-sqlmockを読んだ


はじめに

OSSのコードには自分が今まで学んできた原理原則やデザインパターンが多く散りばめられていました。
今回はこちらのOSSのコードを読んで学んだ面白い実装を備忘録として残しておきます。

学んだコードの概要

DATA-DOG/go-sqlmockは、SQLのテストをサポートしてくれるGo製のライブラリです。
その中でも今回は、SQLの実測値と期待値を比較する箇所の実装に焦点を絞って書いていきます。

まずdbのモックを作成する際にsqlmock.New()で作成できます。
引数なしで、New()した時は、SQLの実測値と期待値の比較に正規表現を使用したものをデフォルトで使用するようになっています。

DATA-DOG/go-sqlmockが提供するQueryMatcherEqualをオプションで使用したら実測値と期待値の比較は文字列として完全一致しているかを比較します。
※空白は削除された状態で比較されます。
実装例は以下のようになります。

db, mock, err := sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))

また、自身でカスタマイズしたSQLの比較処理を使用することができます。
その際とても拡張性に優れていていいなと思ったのが、記事に残そうとしたきっかけです。

今回対象のソースコードと補足説明

不要な箇所は割愛して抜粋しています。
ソースコードと解説を交えて説明していきます。

driver.go

 New()で生成する際に可変長引数でoptionを受け取ります。

driver.go
func New(options ...func(*sqlmock) error) (*sql.DB, Sqlmock, error) {
    return smock.open(options)
}

sqlmock.go

 for文で引数で受け取ったoptionsは関数なので、*sqlmockを引数に実行しています。
あとで中身の説明はしますが、c.queryMatcherを設定しているイメージです。
なので、optionsに何も指定がない時は、c.queryMatcher = QueryMatcherRegexpとあるように、
デフォルトのSQLの比較処理を指定しています。

sqlmock.go
func (c *sqlmock) open(options []func(*sqlmock) error) (*sql.DB, Sqlmock, error) {
    for _, option := range options {
        err := option(c)
        if err != nil {
            return db, c, err
        }
    }
    if c.queryMatcher == nil {
        c.queryMatcher = QueryMatcherRegexp
    }
    return db, c, db.Ping()
}

sqlmockの構造体の中身です。

sqlmock.go
type sqlmock struct {
    ordered      bool
    dsn          string
    opened       int
    drv          *mockDriver
    converter    driver.ValueConverter
    queryMatcher QueryMatcher // 比較用インタフェースを埋め込み

    expected []expectation
}

options.go

上記のNew()で受け取る引数です。以下のように

 sqlmock.New(sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual))

また引数で受け取ったqueryMatchers *sqlmockにsetしているのがわかります。
s *sqlmockはsqlmock.goのoption(c)で受け取ります。

options.go
func QueryMatcherOption(queryMatcher QueryMatcher) func(*sqlmock) error {
    return func(s *sqlmock) error {
        s.queryMatcher = queryMatcher
        return nil
    }
}

query.go

少し長いのでソースコード上にコメントを書いておきました。
比較の仕方を自分でカスタムしたいときは、QueryMatcherFunc型を実装した変数を作成し、
QueryMatcherOptionの引数に渡してあげるだけで使えます。

query.go
type QueryMatcher interface {
    Match(expectedSQL, actualSQL string) error
}

// 関数を返す型:func(expectedSQL, actualSQL string) error
type QueryMatcherFunc func(expectedSQL, actualSQL string) error

// QueryMatcherの実装。関数を実行しているだけ
func (f QueryMatcherFunc) Match(expectedSQL, actualSQL string) error {
    return f(expectedSQL, actualSQL)
}

/*
QueryMatcherFunc型を返す実装/QueryMatcherRegexp/QueryMatcherEqual
*/
// デフォルトで使う正規表現の比較
var QueryMatcherRegexp QueryMatcher = QueryMatcherFunc(func(expectedSQL, actualSQL string) error {
    expect := stripQuery(expectedSQL)
    actual := stripQuery(actualSQL)
    re, err := regexp.Compile(expect)
    // 比較処理・・・
    return nil
})

// 空白を削除した文字列の一致で比較。sqlmock.QueryMatcherOption(sqlmock.QueryMatcherEqual)と書くことで使う。
var QueryMatcherEqual QueryMatcher = QueryMatcherFunc(func(expectedSQL, actualSQL string) error {
    expect := stripQuery(expectedSQL)
    actual := stripQuery(actualSQL)
    // 比較処理・・・
    return nil
})

open-closedの原則

golangのソースにopen-closedの原則が多くみらる原則の1つだと感じました。
インタフェースを用いることで、既存のソースに手を加える事がなく、拡張できるのでとても使い勝手がいいです。

詳細は以下の記事を参考
Go言語で再考するオープンクローズドの原則

最後に

理解するまで時間がかかりましたが、分かってくると拡張性に優れていて素晴らしいコードだなーと思えるようになりました。
実際に自分が実装する時もOSSから学んだことを活かして実装していきたいです。