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

时间:2018-06-27 14:33:14

标签: unit-testing go goroutine

假设我们有一个这样的方法:

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


这些是我可以找到的解决方案,但就我所知,还不能令人满意。什么是最佳解决方案?

3 个答案:

答案 0 :(得分:4)

在模拟中使用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
}

答案 1 :(得分:-1)

首先,我将使用模拟生成器,即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)

等待所有模拟方法被调用。

答案 2 :(得分:-1)

该测试不会像上面建议的sync.WaitGroup解决方案那样永远挂起。如果没有对嘲笑的调用,它将挂起一秒钟(在此示例中):

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)。