structの処理を差し替えてテストする


現職に就いてからは初めての投稿。

転職に伴い以前は興味半分にしか触っていなかったGoについて本格的に開発で利用することになった。
言語の勉強も兼ねてツールを作って試行錯誤している中で最近やり方を調べながら進めたネタについてのメモで。

免責

まだGoを手探りで勉強中で作法とかあまりわかっていないので、その中でこうやるとうまくいったよという一例として閲覧いただければと思います。
(いいやり方かどうかはわかりません、むしろお作法的に良い方法あればコメ投げてください)

経緯

ファイルの変更を検知してあれやこれややるツールを作っていて、ファイルイベントの検知に fsnotify を利用している。

import "github.com/fsnotify/fsnotify"

func someFunc() error {
    var w, err = fsnotify.NewWatcher()
    if err != nil {
        return err
    }
    defer w.Close()
    if err = w.Add("/path/to/directory"); err != nil {
        return nil
    }
    for {
        select {
        ev := <-w.Events:
            // ファイルイベントを処理
        er := <-w.Errors:
            return er
        }
    }
}

これを使う時にファイルシステム関連で問題があると NewWatcher()w.Add が error を返すが、ここで エラーを返すケースのテストはどうやるんだろう? という疑問が挙がった。
全体からすると少ない範囲なのでカバレッジの欠けを気にしないでテストしないのも選択肢としてありだし、むしろ筆者のツールの場合はエラー分はログに出したり即時終了する方針なので過剰対応になりそう。
ただ勉強も兼ねて作っているのと調べた内容であまり実装を膨らませずに実現できたので、せっかくだから適用してみた。

対応方法

以下のステップで進める。

  • エラーを返す処理を直接呼び出さず、非公開の関数型変数に保持して呼び出す
  • 関数処理を差し替えてテストする

関数型メンバを定義して呼び出す

呼び出したい関数を一度関数型の変数に詰め込んで、変数を経由して呼び出すようにする。

package notify

var newWatcher = fsnotify.NewWatcher

func Hoge() {
    ...

    w, err := newWatcher() // 関数メンバを実行する
}

関数を差し替える

プライベート変数は同一パッケージ内からはアクセスできるので、同一パッケージ のテストソースで関数を差し替える処理を追加する。

export_test.go
package notify // <= ここは実装ソースのパッケージと合わせる

func NewWatcherError() func() {
    t := newWatcher
    newWatcher = func() (*fsnotify.Watcher, error) {
        return nil, errors.New("newWatcher error")
    }
    return func() {
        newWatcher = t // テスト後に戻す処理
    }
}
hoge_test.go(利用側)
package notify_test

func TestHoge(t *testing.T) {
    // newWatcher を差し替えてテスト終了時に元に戻す
    defer notify.NewWatcherError()()
}

インターフェイスを経由してモックする方法(ボツ案)

Goでmockを使ってテストする方法としては、ファイルシステムのテストtestify/mock などで紹介されているものがある。
これらはいずれも interface を経由して処理の差し替えを行う。
これらを採用しなかった理由としては以下の通り。
ただ今回ボツにしただけで interface 化が必ずしも悪い方法ではなく、むしろポリモーフィズムを使用する場面では必要に応じて使うことになるとは思う。

フィールドが利用できない

interface はフィールドを持つことができないので interface の差し替えで処理しようとするとフィールドをそのまま利用することができない。

// 差し替えて使用する用のインターフェイス
type nativeWatcher struct {
    Add(name string) error
}

// watcher1は Events や Errors のチャンネルを持つ
var watcher1 = fsnotify.NewWatcher()

// インターフェイスから使用しようとすると Events や Errors は参照できない
var watcher2 nativeWatcher = watcher1

インターフェイス側でフィールドをラップするレシーバー関数を用意すれば対処できるが、実装をあまり大きくしたくなかった。

structのレシーバー以外は別途インターフェイス化が必要になる

今回エラー検証したかった箇所に fsnotify.NewWatcher() が含まれていたが、この処理はレシーバーではないため interface 化できない。
ここをモックライブラリとか使うなら中継インターフェイスが必要になる。

// インターフェイスを用意して
type watcherGen interface {
    New() (*fsnotify.Watcher, error)
}

// 実装を用意して
type genImpl struct {
}
func (gen *genImpl) New() (*fsnotify.Watcher, error) {
    return fsnotify.NewWatcher()
}

// 実体を保持する
var gen watcherGen = &genImpl{}

1処理をモックするための追加としては少々重い。

定義へ移動が使えない

これはEclipse-JavaとかVisual Studioの過去バージョンにあった問題と同様で、開発環境として vscode を使っているが、Goで interface を利用すると処理の実体へジャンプができなくなる。
(多分JavaやC#と違ってインターフェイスの定義を紐付けるのは難しい気がする)

func TestXxx(t *testing.T) {
    w, err := gen.New() // <= 定義へ移動で interface へ飛ばされる
}

参考