在 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)。
假设我们有这样的方法:
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)。