mock goプログラムの新しい方法


今まで、goの中でmockはとても難しいと思っていました.動的言語やVM上を走る言語とは異なり、goは開発時にmockに予備空間を介入させなければならない.そうしないと、テスト時にドアを開けて入ることができない.開発するときに頭が痛くなることが多くて、テスト性をもう一度考えなければならないのは、ちょっと難しいですね.またサードパーティライブラリは必ずしもmockに空間を残すとは限らず、このような状況に遭遇すると目を丸くして迂回するしかありません.多くの場合、副作用のある関数をmockできず、ターゲットパスを上書きできないことがあります.肝心なパスがテストできない以上、いっそテストを書かない.その結果、プロジェクトの多くのgoコードは実際にテストで上書きされていません.
しかし、最近ライブラリを見つけました.https://github.com/bouk/monkey冒頭の悩みとさよならできるようになった?狭い範囲で体験してみると、やはり使いやすい感じがします.
簡単に言えば、monkeyライブラリは、メモリアドレスを変更することによって、ターゲット関数の実際の実行アドレスを置き換え、任意の関数のmockを実現する(ほぼ)ことができる.ターゲット関数を指定し、匿名関数を定義して置き換えることができます.置換されたレコードはグローバル・テーブルに存在し、不要な場合は元のターゲット関数を再復元できます.メモリアドレスを変更するブラックテクノロジーを採用しているため、著者らはテスト環境以外の場所では絶対に使用しないことを提案している.現在、x 86アーキテクチャのLinuxとMacしかサポートされていませんが、Windowsはテストしたことがありませんか?いずれにしても、LinuxとMacをサポートすれば、開発機とCI環境をカバーするのに十分です.monkeyライブラリは非常に簡単で、サンプルコードを直接説明しながら説明しました.
package main

import (
    "fmt"
    "github.com/bouk/monkey"
    "os"
    "os/exec"
    "reflect"
    "testing"
)

//   call
func call(cmd string) (int, string) {
    bytes, err := exec.Command("sh", "-c", cmd).CombinedOutput()
    output := string(bytes)
    if err != nil {
        return 1, reportExecFailed(output)
    }
    return 0, output
}

//  , mock !
func reportExecFailed(msg string) string {
    os.Exit(1) //  
    return msg
}

func TestExecSussess(t *testing.T) {
    //   patch  
    //   UnpatchAll   teardown  
    //   go   testing  
    defer monkey.UnpatchAll()
    // mock   exec.Command   *exec.Cmd   CombinedOutput  
    monkey.PatchInstanceMethod(
        reflect.TypeOf((*exec.Cmd)(nil)),
        "CombinedOutput", func(_ *exec.Cmd) ([]byte, error) {
            return []byte("results"), nil
        },
    )
    // mock   reportExecFailed  
    monkey.Patch(reportExecFailed, func(msg string) string {
        return msg
    })

    rc, output := call("any")
    if rc != 0 {
        t.Fail()
    }
    if output != "results" {
        t.Fail()
    }
}

func TestExecFailed(t *testing.T) {
    defer monkey.UnpatchAll()
    //   mock  , 
    monkey.PatchInstanceMethod(
        reflect.TypeOf((*exec.Cmd)(nil)),
        "CombinedOutput", func(_ *exec.Cmd) ([]byte, error) {
            return []byte(""), fmt.Errorf("sth bad happened")
        },
    )
    monkey.Patch(reportExecFailed, func(msg string) string {
        return msg
    })

    rc, output := call("any")
    if rc != 1 {
        t.Fail()
    }
    if output != "" {
        t.Fail()
    }
}
go test xx_test.goを実行し、上記のコードを実行できます.
テストでは、位置Aにはmockドロップ関数が必要で、位置Bには元の関数を呼び出す必要があります.この場合、monkeyライブラリが提供するPatchGuard構造体を使用する必要がある.公式ドキュメントの例ですが、ここでは少し調整します.
package main

import (
    "fmt"
    "github.com/bouk/monkey"
    "strings"
)

func main() {
    var guard *monkey.PatchGuard
    guard = monkey.Patch(fmt.Println, func(a ...interface{}) (n int, err error) {
        s := make([]interface{}, len(a))
        for i, v := range a {
            s[i] = strings.Replace(fmt.Sprint(v), "hell", "*bleep*", -1)
        }
        //  
        // guard.Unpatch()
        // defer guard.Restore()
        // return fmt.Println(s...)
        guard.Unpatch()
        n, err = fmt.Println(s...)
        guard.Restore()
        return
    })
    fmt.Println("what the hell?") // what the *bleep*?
    fmt.Println("what the hell?") // what the *bleep*?
}

上のコードの鍵は、元の関数を呼び出す前にUnpatchを1回呼び出し、mockの前に復元することです.その後、元の関数を呼び出した後、Restoreを1回呼び出し、mockを再ダイヤルします.残りは、入力パラメータに基づいて、現在位置Aまで運転されているのか、位置Bまで運転されているのかを判断することにほかならない.