web-dev-qa-db-ja.com

Golangでのユニットテスト中にゴルーチンが呼び出されたかどうかをテストするにはどうすればよいですか?

次のような方法があるとします。

func method(intr MyInterface) {
    go intr.exec()
} 

ユニットテストmethodでは、inter.execが一度だけ呼び出されたことを表明したいと思います。そのため、テストで別のモック構造体を使用してモックを作成できます。これにより、呼び出されたかどうかを確認する機能が提供されます。

type mockInterface struct{
    CallCount int
}

func (m *mockInterface) exec() {
    m.CallCount += 1
}

そしてユニットテストでは:

func TestMethod(t *testing.T) {
    var mock mockInterface{}
    method(mock)
    if mock.CallCount != 1 {
        t.Errorf("Expected exec to be called only once but it ran %d times", mock.CallCount)
    }
}

ここで問題となるのは、intr.execgoキーワードで呼び出されているため、テストでアサーションに到達したときに、それが呼び出されたかどうかを確認できないことです。

考えられる解決策1:

intr.execの引数にチャネルを追加すると、これを解決できる場合があります。テストでオブジェクトを受信するのを待つことができ、オブジェクトを受信した後、呼び出されていることを表明し続けることができます。このチャネルは、本番(非テスト)コードでは完全に使用されません。これは機能しますが、テスト以外のコードに不必要な複雑さが加わり、大きなコードベースが理解できなくなる可能性があります。

考えられる解決策2:

アサーションの前にテストに比較的小さなスリープを追加すると、スリープが終了する前にゴルーチンが呼び出されることがある程度保証される場合があります。

func TestMethod(t *testing.T) {
    var mock mockInterface{}
    method(mock)

    time.sleep(100 * time.Millisecond)

    if mock.CallCount != 1 {
        t.Errorf("Expected exec to be called only once but it ran %d times", mock.CallCount)
    }
}

これにより、非テストコードは現在のままになります。
問題は、ランダムな状況でテストが失敗する可能性があるため、テストが遅くなり、不安定になることです。

考えられる解決策3:

次のようなユーティリティ関数を作成します。

var Go = func(function func()) {
    go function()
} 

そして、methodを次のように書き直します。

func method(intr MyInterface) {
    Go(intr.exec())
} 

テストでは、Goを次のように変更できます。

var Go = func(function func()) {
    function()
} 

したがって、テストを実行しているときは、intr.execが同期的に呼び出され、アサーションの前にモックメソッドが呼び出されることを確認できます。
このソリューションの唯一の問題は、golangの基本構造をオーバーライドしていることです。これは正しいことではありません。


これらは私が見つけることができた解決策ですが、私が見る限りでは満足のいくものではありません。最善の解決策は何ですか?

5
sazary

使う sync.WaitGroupモック内

mockInterfaceを拡張して、他のゴルーチンが終了するのを待つことができます

type mockInterface struct{
    wg sync.WaitGroup // create a wait group, this will allow you to block later
    CallCount int
}

func (m *mockInterface) exec() {
    m.wg.Done() // record the fact that you've got a call to exec
    m.CallCount += 1
}

func (m *mockInterface) currentCount() int {
    m.wg.Wait() // wait for all the call to happen. This will block until wg.Done() is called.
    return m.CallCount
}

テストでは、次のことができます。

mock := &mockInterface{}
mock.wg.Add(1) // set up the fact that you want it to block until Done is called once.

method(mock)

if mock.currentCount() != 1 {  // this line with block
    // trimmed
}
9
Zak

このテストは、上記で提案したsync.WaitGroupソリューションのように永遠にハングすることはありません。 mock.execの呼び出しがない場合、(この特定の例では)1秒間ハングします。

package main

import (
    "testing"
    "time"
)

type mockInterface struct {
    closeCh chan struct{}
}

func (m *mockInterface) exec() {
    close(closeCh)
}

func TestMethod(t *testing.T) {
    mock := mockInterface{
        closeCh: make(chan struct{}),
    }

    method(mock)

    select {
    case <-closeCh:
    case <-time.After(time.Second):
        t.Fatalf("expected call to mock.exec method")
    }
}

これは基本的に、上記の私の答えのmc.Wait(time.Second)です。

2
Maxim