在 Golang 中进行单元测试时如何测试是否调用了 goroutine?

How to test if a goroutine has been called while unit testing in 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.exec 是用 go 关键字调用的,我们不能确定当我们在测试中到达我们的断言时,它是否已经被调用。

可能的解决方案 1:

intr.exec 的参数添加一个通道可以解决这个问题:我们可以等待在测试中从它接收任何对象,并且在从它接收到一个对象之后我们可以继续断言它正在被调用。该通道将完全不用于生产(非测试)代码。 这会起作用,但它会为非测试代码增加不必要的复杂性,并可能使大型代码库难以理解。

可能的解决方案 2:

在断言之前的测试中添加一个相对较小的睡眠可以让我们确信 goroutine 将在睡眠完成之前被调用:

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 的基本结构,这是不正确的做法。


这些是我能找到的解决方案,但据我所知,没有一个是令人满意的。什么是最佳解决方案?

在 mock

中使用 sync.WaitGroup

您可以扩展 mockInterface 以允许它等待其他 goroutine 完成

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
}

首先我会使用模拟生成器,即 github。com/gojuno/minimock 而不是写自嘲:

minimock -f example.go -i MyInterface -o my_interface_mock_test.go

然后你的测试看起来像这样(顺便说一下,测试存根也是用 github.com/hexdigest/gounit 生成的)

func Test_method(t *testing.T) {
    type args struct {
        intr MyInterface
    }
    tests := []struct {
        name string
        args func(t minimock.Tester) args
    }{
        {
            name: "check if exec is called",
            args: func(t minimock.Tester) args {
                return args{
                    intr: NewMyInterfaceMock(t).execMock.Return(),
                }
            },
        },
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            mc := minimock.NewController(t)
            defer mc.Wait(time.Second)

            tArgs := tt.args(mc)

            method(tArgs.intr)
        })
    }
}

本次测试

defer mc.Wait(time.Second)

等待调用所有模拟方法。

此测试不会像上面提出的 sync.WaitGroup 解决方案那样永远挂起。如果没有调用 mock.exec:

,它会挂起一秒钟(在这个特定的例子中)
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)。