次のような方法があるとします。
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.exec
がgo
キーワードで呼び出されているため、テストでアサーションに到達したときに、それが呼び出されたかどうかを確認できないことです。
intr.exec
の引数にチャネルを追加すると、これを解決できる場合があります。テストでオブジェクトを受信するのを待つことができ、オブジェクトを受信した後、呼び出されていることを表明し続けることができます。このチャネルは、本番(非テスト)コードでは完全に使用されません。これは機能しますが、テスト以外のコードに不必要な複雑さが加わり、大きなコードベースが理解できなくなる可能性があります。
アサーションの前にテストに比較的小さなスリープを追加すると、スリープが終了する前にゴルーチンが呼び出されることがある程度保証される場合があります。
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)
}
}
これにより、非テストコードは現在のままになります。
問題は、ランダムな状況でテストが失敗する可能性があるため、テストが遅くなり、不安定になることです。
次のようなユーティリティ関数を作成します。
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の基本構造をオーバーライドしていることです。これは正しいことではありません。
これらは私が見つけることができた解決策ですが、私が見る限りでは満足のいくものではありません。最善の解決策は何ですか?
使う 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
}
このテストは、上記で提案した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)です。