web-dev-qa-db-ja.com

Goのモック関数

小さな個人プロジェクトをコーディングすることでGoを学んでいます。たとえ小さいものであっても、Goの良い習慣を最初から学ぶために、厳密なユニットテストを行うことにしました。

些細な単体テストはすべてうまくいきましたが、今は依存関係に困惑しています。一部の関数呼び出しをモック呼び出しに置き換えたいと思います。これが私のコードのスニペットです。

func get_page(url string) string {
    get_dl_slot(url)
    defer free_dl_slot(url)

    resp, err := http.Get(url)
    if err != nil { return "" }
    defer resp.Body.Close()

    contents, err := ioutil.ReadAll(resp.Body)
    if err != nil { return "" }
    return string(contents)
}

func downloader() {
    dl_slots = make(chan bool, DL_SLOT_AMOUNT) // Init the download slot semaphore
    content := get_page(BASE_URL)
    links_regexp := regexp.MustCompile(LIST_LINK_REGEXP)
    matches := links_regexp.FindAllStringSubmatch(content, -1)
    for _, match := range matches{
        go serie_dl(match[1], match[2])
    }
}

Httpを介して実際にページを取得せずにdownloader()をテストできるようにしたい-つまり、get_page(ページコンテンツのみを文字列として返すため、より簡単に)またはhttp.Get()のいずれかをモックします。

私はこのスレッドを見つけました: https://groups.google.com/forum/#!topic/golang-nuts/6AN1E2CJOxI これは同様の問題についてのようです。ジュリアンフィリップスは、彼のライブラリであるWithmock( http://github.com/qur/withmock )をソリューションとして提示していますが、動作させることはできません。正直なところ、主に貨物カルトコードである私のテストコードの関連部分は次のとおりです。

import (
    "testing"
    "net/http" // mock
    "code.google.com/p/gomock"
)
...
func TestDownloader (t *testing.T) {
    ctrl := gomock.NewController()
    defer ctrl.Finish()
    http.MOCK().SetController(ctrl)
    http.EXPECT().Get(BASE_URL)
    downloader()
    // The rest to be written
}

テスト出力は次のとおりです。

ERROR: Failed to install '_et/http': exit status 1
output:
can't load package: package _et/http: found packages http (chunked.go) and main (main_mock.go) in /var/folders/z9/ql_yn5h550s6shtb9c5sggj40000gn/T/withmock570825607/path/src/_et/http

Withmockは私のテスト問題の解決策ですか?動作させるにはどうすればよいですか?

123
GolDDranks

良いテストを実践してくれてありがとう! :)

個人的に、私はgomockを使用しません(または、その点でのモックフレームワークです。Goでのモックは、それなしでは非常に簡単です)。パラメーターとしてdownloader()関数に依存関係を渡すか、またはdownloader()を型のメソッドにし、その型はget_page依存関係を保持できます。

方法1:get_page()のパラメーターとしてdownloader()を渡す

type PageGetter func(url string) string

func downloader(pageGetterFunc PageGetter) {
    // ...
    content := pageGetterFunc(BASE_URL)
    // ...
}

メイン:

func get_page(url string) string { /* ... */ }

func main() {
    downloader(get_page)
}

テスト:

func mock_get_page(url string) string {
    // mock your 'get_page()' function here
}

func TestDownloader(t *testing.T) {
    downloader(mock_get_page)
}

Method2:download()をタイプDownloaderのメソッドにする:

依存関係をパラメーターとして渡したくない場合は、get_page()を型のメンバーにし、download()をその型のメソッドにして、get_pageを使用することもできます。

type PageGetter func(url string) string

type Downloader struct {
    get_page PageGetter
}

func NewDownloader(pg PageGetter) *Downloader {
    return &Downloader{get_page: pg}
}

func (d *Downloader) download() {
    //...
    content := d.get_page(BASE_URL)
    //...
}

メイン:

func get_page(url string) string { /* ... */ }

func main() {
    d := NewDownloader(get_page)
    d.download()
}

テスト:

func mock_get_page(url string) string {
    // mock your 'get_page()' function here
}

func TestDownloader() {
    d := NewDownloader(mock_get_page)
    d.download()
}
163
weberc2

代わりに変数を使用するように関数定義を変更した場合:

var get_page = func(url string) string {
    ...
}

テストでオーバーライドできます:

func TestDownloader(t *testing.T) {
    get_page = func(url string) string {
        if url != "expected" {
            t.Fatal("good message")
        }
        return "something"
    }
    downloader()
}

ただし、オーバーライドする関数の機能をテストすると、他のテストが失敗する可能性があります!

Goの作成者は、Go標準ライブラリでこのパターンを使用して、テストフックをコードに挿入し、テストを容易にします。

https://golang.org/src/net/hook.go

https://golang.org/src/net/dial.go#L248

https://golang.org/src/net/dial_test.go#L701

15
Jake

publicstructメソッドがinterfacesを実装するわずかに異なるアプローチを使用していますが、そのロジックはラッピングprivate(エクスポートされていない)関数。これらのinterfacesをパラメーターとして取ります。これにより、実質的にすべての依存関係をモックする必要がありますが、テストスイートの外部から使用できるクリーンなAPIが必要になります。

これを理解するには、テストケースのエクスポートされていないメソッドにアクセスできることを理解することが不可欠です(つまり、_test.goファイル内から)、代わりにテストするラッピングの内側にロジックのないエクスポートされたものをテストします。

要約すると:エクスポートされた関数をテストする代わりに、エクスポートされていない関数をテストします!

例を作りましょう。 2つのメソッドを持つSlack API構造体があるとします。

  • slack webhookにHTTPリクエストを送信するSendMessageメソッド
  • 文字列のスライスを指定したSendDataSynchronouslyメソッドはそれらを反復処理し、反復ごとにSendMessageを呼び出します

したがって、SendDataSynchronouslyをモックする必要があるたびに、HTTPリクエストを作成せずにSendMessageをテストするには、そうですか?

package main

import (
    "fmt"
)

// URI interface
type URI interface {
    GetURL() string
}

// MessageSender interface
type MessageSender interface {
    SendMessage(message string) error
}

// This one is the "object" that our users will call to use this package functionalities
type API struct {
    baseURL  string
    endpoint string
}

// Here we make API implement implicitly the URI interface
func (api *API) GetURL() string {
    return api.baseURL + api.endpoint
}

// Here we make API implement implicitly the MessageSender interface
// Again we're just WRAPPING the sendMessage function here, nothing fancy 
func (api *API) SendMessage(message string) error {
    return sendMessage(api, message)
}

// We want to test this method but it calls SendMessage which makes a real HTTP request!
// Again we're just WRAPPING the sendDataSynchronously function here, nothing fancy
func (api *API) SendDataSynchronously(data []string) error {
    return sendDataSynchronously(api, data)
}

// this would make a real HTTP request
func sendMessage(uri URI, message string) error {
    fmt.Println("This function won't get called because we will mock it")
    return nil
}

// this is the function we want to test :)
func sendDataSynchronously(sender MessageSender, data []string) error {
    for _, text := range data {
        err := sender.SendMessage(text)

        if err != nil {
            return err
        }
    }

    return nil
}

// TEST CASE BELOW

// Here's our mock which just contains some variables that will be filled for running assertions on them later on
type mockedSender struct {
    err      error
    messages []string
}

// We make our mock implement the MessageSender interface so we can test sendDataSynchronously
func (sender *mockedSender) SendMessage(message string) error {
    // let's store all received messages for later assertions
    sender.messages = append(sender.messages, message)

    return sender.err // return error for later assertions
}

func TestSendsAllMessagesSynchronously() {
    mockedMessages := make([]string, 0)
    sender := mockedSender{nil, mockedMessages}

    messagesToSend := []string{"one", "two", "three"}
    err := sendDataSynchronously(&sender, messagesToSend)

    if err == nil {
        fmt.Println("All good here we expect the error to be nil:", err)
    }

    expectedMessages := fmt.Sprintf("%v", messagesToSend)
    actualMessages := fmt.Sprintf("%v", sender.messages)

    if expectedMessages == actualMessages {
        fmt.Println("Actual messages are as expected:", actualMessages)
    }
}

func main() {
    TestSendsAllMessagesSynchronously()
}

このアプローチで気に入っているのは、エクスポートされていないメソッドを見ると、依存関係が明確にわかることです。同時に、エクスポートするAPIは非常にクリーンで、渡すパラメーターが少なくなります。ここでの依存関係は、これらすべてのインターフェイス自体を実装している親レシーバーにすぎないためです。しかし、すべての機能は潜在的にその一部(1つ、場合によっては2つのインターフェイス)のみに依存しているため、リファクタリングが非常に簡単になります。関数のシグネチャを見るだけで、コードが実際にどのように結合されているかを見ることができて嬉しいです。臭いのあるコードに対して強力なツールになると思います。

物事を簡単にするために、すべてを1つのファイルに入れて playground here でコードを実行できるようにしますが、GitHubの完全な例を確認することをお勧めします。ここに slackがあります。 go ファイル、ここでは slack_test.go

そして ここ 全体:)

9

私は次のようなことをします

メイン

var getPage = get_page
func get_page (...

func downloader() {
    dl_slots = make(chan bool, DL_SLOT_AMOUNT) // Init the download slot semaphore
    content := getPage(BASE_URL)
    links_regexp := regexp.MustCompile(LIST_LINK_REGEXP)
    matches := links_regexp.FindAllStringSubmatch(content, -1)
    for _, match := range matches{
        go serie_dl(match[1], match[2])
    }
}

テスト

func TestDownloader (t *testing.T) {
    origGetPage := getPage
    getPage = mock_get_page
    defer func() {getPage = origGatePage}()
    // The rest to be written
}

// define mock_get_page and rest of the codes
func mock_get_page (....

そしてgolangでは_を避けます。 キャメルケースを使用する

6
Fallen