Goのモック関数


147

私は小さな個人プロジェクトをコーディングすることで囲碁を学んでいます。小さいながら、厳密なユニットテストを行って、最初から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経由でページを取得せずに、つまりget_page(ページコンテンツのみを文字列として返すため、より簡単)またはhttp.Get()をモックすることで、downloader()をテストできるようにしたいと思います。

私はこのスレッドを見つけました:https : //groups.google.com/forum/#!topic / golang-nuts / 6AN1E2CJOxIこれは同様の問題についてのようです。Julian Phillipsが彼のライブラリである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は私のテスト問題の解決策ですか?それを機能させるにはどうすればよいですか?


Goユニットテストに飛び込んでいるので、振る舞い駆動型テストを行うための優れた方法をGoConveyで確認してください...そしてティーザー:ネイティブの「go test」テストでも機能する自動更新Web UIが登場します。
Matt

回答:


193

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

個人的には、私は使用していません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)
}

download()方法2:タイプのメソッドを作成し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()
}

4
どうもありがとう!私は2番目のもので行きました。(モックしたい他の関数もいくつかあったので、それらを構造体に割り当てる方が簡単でした)ところで。私は囲碁では少し好きです。特に、その同時実行機能は優れています。
GolDDranks 2013年

149
テストのためにメインコード/関数のシグネチャを変更する必要があるのはひどい唯一の発見ですか?
トーマス

41
@Thomasあなたが1人だけかどうかはわかりませんが、それが実際にテスト駆動開発の基本的な理由です。テスト可能なコードはよりモジュール化されています。この場合、Downloaderオブジェクトの 'get_page'動作はプラグイン可能になり、その実装を動的に変更できます。それが最初にひどく書かれた場合にのみ、メインコードを変更する必要があります。
weberc2

21
@トーマス私はあなたの2番目の文を理解していません。TDDはより良いコードを駆動します。テスト可能になるためにコードは変更されます(テスト可能なコードは、十分に検討されたインターフェースでモジュール化されているため)。ただし、主な目的は、より優れたコードを作成することです。自動化テストを行うことは、すばらしい副次的な利点です。機能コードが変更された後にテストを追加するだけであるという懸念がある場合でも、誰かがいつかそのコードを読んだり変更したりする可能性が高いため、変更することをお勧めします。
weberc2 14

6
@Thomasもちろん、進行中にテストを作成している場合、その難問に対処する必要はありません。
weberc2 14

24

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

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


8
必要に応じて反対票を投じます。これは、DIに関連するボイラープレートを回避するための小さなパッケージの許容パターンです。関数を含む変数は、エクスポートされないため、パッケージのスコープに対してのみ「グローバル」です。これは有効なオプションです、私は欠点を述べました、あなた自身の冒険を選択してください。
Jake

4
注意すべきことの1つは、この方法で定義された関数は再帰的ではないということです。
Ben Sandler

2
私はこのアプローチがその場所にあることを@Jakeに同意します。
m.kocikowski 2016

11

私は少し異なるアプローチを使用していますが、パブリック structメソッドはインターフェイスを実装しますが、それらのロジックは、これらのインターフェイスをパラメーターとして受け取るプライベート(非エクスポート)関数のラップに限定されます。これにより、実質的にすべての依存関係をモックする必要があり、しかもテストスイートの外部から使用するためのクリーンなAPIが必要になります。

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

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

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

  • SendMessageスラックウェブフックにHTTPリクエストを送信する方法
  • 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つのファイルに入れますが、GitHubで完全な例も確認することをお勧めします。ここにslack.goファイルとここにslack_test.goがあります。

そしてここですべて:)


これは実際には興味深いアプローチであり、テストファイル内のプライベートメソッドへのアクセスに関する一口は非常に役立ちます。C ++のpimplテクニックを思い出します。ただし、プライベート関数のテストは危険だと言っておくべきだと思います。プライベートメンバーは通常、実装の詳細と見なされ、パブリックインターフェイスよりも時間とともに変化する可能性が高くなります。ただし、パブリックインターフェイスのプライベートラッパーのみをテストする限り、問題はありません。
c1moore 2018年

ええ、一般的に言って私はあなたに同意します。この場合、プライベートメソッドの本体はパブリックボディとまったく同じなので、まったく同じことをテストします。2つの唯一の違いは、関数の引数です。これは、必要に応じて依存関係(モックされているかどうかにかかわらず)を挿入できるようにするためのトリックです。
Francesco Casula

ええ、同意します。私が言っているのは、これらのパブリックメソッドをラップするプライベートメソッドに制限する限り、行っても問題ないということです。実装の詳細であるプライベートメソッドのテストを開始しないでください。
c1moore 2018年

7

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

メイン

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では避けます。camelCaseをより効果的に使用する


1
あなたのためにこれを行うことができるパッケージを開発することは可能ですか?私は次のようなものを考えています:p := patch(mockGetPage, getPage); defer p.done()。私は新しいので、unsafeライブラリを使用してこれを実行しようとしましたが、一般的なケースでは実行できないようです。
15年

@Fallenこれは、私の回答から1年以上経って書かれた私の回答とほぼ同じです。
ジェイク

1
1.唯一の類似点は、グローバル変数の方法です。@Jake 2.複雑よりも単純の方が優れています。weberc2
堕ちた

1
@fallen私はあなたの例がより単純であるとは思わない。引数を渡すことは、グローバル状態を変更することほど複雑ではありませんが、グローバル状態に依存すると、他の方法では存在しない多くの問題が発生します。たとえば、テストを並列化する場合は、競合状態に対処する必要があります。
weberc2

それはほとんど同じですが、:)ではありません。この回答では、varに関数を割り当てる方法と、これによりテストに別の実装を割り当てる方法がわかります。テストしている関数の引数を変更できないので、これは私にとって素晴らしい解決策です。代わりの方法はレシーバーをモック構造体で使用することですが、どちらがよりシンプルかはまだわかりません。
alexbt

0

警告:これにより、実行可能ファイルのサイズが少し大きくなり、実行時のパフォーマンスが少し低下する可能性があります。IMO、これはgolangにマクロや関数デコレーターなどの機能がある場合に適しています。

APIを変更せずに関数をモックしたい場合、最も簡単な方法は実装を少し変更することです:

func getPage(url string) string {
  if GetPageMock != nil {
    return GetPageMock()
  }

  // getPage real implementation goes here!
}

func downloader() {
  if GetPageMock != nil {
    return GetPageMock()
  }

  // getPage real implementation goes here!
}

var GetPageMock func(url string) string = nil
var DownloaderMock func() = nil

このようにして、ある機能を他の機能から実際に模倣することができます。より便利にするために、そのようなあざける定型文を提供できます。

// download.go
func getPage(url string) string {
  if m.GetPageMock != nil {
    return m.GetPageMock()
  }

  // getPage real implementation goes here!
}

func downloader() {
  if m.GetPageMock != nil {
    return m.GetPageMock()
  }

  // getPage real implementation goes here!
}

type MockHandler struct {
  GetPage func(url string) string
  Downloader func()
}

var m *MockHandler = new(MockHandler)

func Mock(handler *MockHandler) {
  m = handler
}

テストファイル:

// download_test.go
func GetPageMock(url string) string {
  // ...
}

func TestDownloader(t *testing.T) {
  Mock(&MockHandler{
    GetPage: GetPageMock,
  })

  // Test implementation goes here!

  Mock(new(MockHandler)) // Reset mocked functions
}

-2

ユニットテストがこの質問のドメインであると考えると、https://github.com/bouk/monkeyを使用することを強くお勧めします。このパッケージを使用すると、元のソースコードを変更せずに模擬テストを行うことができます。他の回答と比較すると、より「非侵入型」です。

メイン

type AA struct {
 //...
}
func (a *AA) OriginalFunc() {
//...
}

模擬試験

var a *AA

func NewFunc(a *AA) {
 //...
}

monkey.PatchMethod(reflect.TypeOf(a), "OriginalFunc", NewFunc)

悪い面は:

-Dave.Cにより指摘された、このメソッドは安全ではありません。したがって、単体テスト以外では使用しないでください。

-非慣用的な囲碁です。

良い面は:

++は邪魔にならない。メインコードを変更せずに物事を行うようにします。トーマスが言ったように。

++最小限のコードでパッケージの動作を変更する(サードパーティによって提供される場合があります)。


1
これを行わないでください。これは完全に安全ではなく、さまざまなGo内部を破壊する可能性があります。言うまでもなく、それはリモートで慣用的なGoでもありません。
Dave C

1
@DaveC私はGolangについてのあなたの経験を尊重しますが、あなたの意見を疑います。1.安全はソフトウェア開発のすべてを意味するのではなく、機能が豊富で便利なことを行います。2.慣用的なGolangはGolangではなく、その一部です。1つのプロジェクトがオープンソースである場合、他の人がそれを悪用するのはよくあることです。コミュニティはそれを少なくとも抑制しないで奨励すべきです。
フランクワン

2
この言語はGoと呼ばれます。安全でないということは、ガベージコレクションのようなGoランタイムを破壊する可能性があることを意味します。
Dave C

1
私にとって、安全ではないことは単体テストにとってはクールです。ユニットテストが行​​われるたびに、より多くの 'インターフェイス'を持つコードをリファクタリングする必要がある場合。安全ではない方法で解決する方が私に適しています。
フランクワン

1
@DaveC私はこれがひどい考えであることに完全に同意します(私の回答が投票されて受け入れられた回答です)。ただし、修正されれば幸いです。
weberc2
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.