2019年11月20日水曜日

golang で struct が持つ既存の関数をスタブにしてテストする方法を考える

概要

過去に紹介した rspec-mock のように既存の関数のオーバライド的な感じでモックを作成したかったのですが gomock では厳しいことが判明しました
とりあえず gomock などは考えずに golang でどうやって実現できるのかを考えてみました

環境

  • macOS 10.15
  • golang 1.12.9

テストしたいコード

package main

import (
    "fmt"
)

type A struct {
    b B
}

type B struct {
    key string
}

func (b B) Method(value string) {
    fmt.Printf("%s => %s\n", b.key, value)
}

func main() {
    b := B{key: "hello"}
    a := A{b: b}
    a.b.Method("world")
}

この a.b.Method をテストするにあたって Method をスタブしたいと思います

DI を使った仕組みに書き換える

B 構造体を DI (dependency injection) を使った仕組みに書き換えます

package main

import (
    "fmt"
)

type A struct {
    b B
}

type B struct {
    key string
    Method func(key string, value string)
}

func main() {
    m := func (key string, value string) {
        fmt.Printf("%s => %s\n", key, value)
    }
    b := B{
        key: "hello",
        Method: m,
    }
    a := A{b: b}
    a.b.Method(a.b.key, "world")
}

B 構造体のメンバに関数を持たせることで関数の実装をあとから注入する方法です
こうすることでテスト時には main とは異なる挙動をさせることができるため a.b.Method のスタブが書きやすくなります

package main

import (
    "fmt"
    "testing"
)

func TestMethod(t *testing.T) {
    // stub
    m := func(key string, value string) {
        fmt.Printf("%s => %s\n", key, value)
        fmt.Println("test func")
    }
    b := B{
        key: "hello",
        Method: m,
    }
    a := A{b: b}
    a.b.Method(a.b.key, "world")
}

ただこの場合は冒頭に紹介した B 構造体の仕組みから大きく変える必要があるため既存のロジックにも影響する可能性があります

既存のメソッドのオーバライドはできないか

新しい構造体を作成して B 構造体を組み込んで Method をオーバライドする的なことはできますが B 構造体が持つ Method 自体を変更することは golang ではできなさそうです

Ruby でいうところの「クラスの展開」や「特異メソッド」のように既存のクラスに対する実装の変更などはできないようです (そもそも golang にはクラスの概念はありませんが)

BInterface を使う

これも既存のロジックを書き換えてしまいますが A 構造体が持っていた B 構造体を B インタフェースに変えることであたかも B 構造体のように振る舞う fakeB 構造体をフィールドのセットします
こうすることで関数の振る舞いを上書きしているように見せかけます

package main

import (
    "fmt"
)

type A struct {
    b BInterface
}

type BInterface interface {
    Method(value string)
}

type B struct {
    key string
}

func (b B) Method(value string) {
    fmt.Printf("%s => %s\n", b.key, value)
}

type fakeB struct {
    key string
}

func (b fakeB) Method(value string) {
    fmt.Printf("%s => %s !!\n", b.key, value)
    fmt.Println("by fake")
}

func main() {
    b := B{
        key: "hello",
    }
    fb := fakeB{
        key: "hello",
    }
    a := A{b: b}
    a.b = fb
    a.b.Method("world")
}

一度 B 構造体をセットしているにも関わらずあとからセットしている fakeB 構造体で上書きされているのがわかります
A 構造体を書き換えるのでかなりリスクがある感じはしますが一応これでも対応できました

最後に

golang で構造体が持つ関数をスタブする方法を考えてみました
一番良さそうなのは DI を使う方法かなと思います
自由度も高いのでいろいろなところで使える技だと思います

0 件のコメント:

コメントを投稿