【发布时间】:2018-12-06 12:12:46
【问题描述】:
假设我们有这样一个方法:
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:
在断言之前为测试添加一个相对较小的 sleep 可以让我们确保 goroutine 将在 sleep 完成之前被调用:
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 将被同步调用,我们可以确定我们的 mock 方法在断言之前被调用。
这个解决方案的唯一问题是它覆盖了 golang 的基本结构,这是不正确的做法。
这些是我能找到的解决方案,但据我所知,没有一个是令人满意的。什么是最好的解决方案?
【问题讨论】:
-
少量睡眠通常就足够了,不会对测试执行时间产生重大影响。还有
runtime.Gosched(),它让给另一个goroutine,根本没有任何睡眠延迟。 -
IMO 在测试中使用睡眠进行同步是一种代码异味,将来经常会失败。创建某种可以在测试中观察到的同步副作用。
-
@JimB 我的代码(我的业务逻辑)不需要任何同步的副作用。我更不想仅仅因为一些测试问题而添加它们。
-
@Adrian 这是个好主意,但
Gosched不允许我选择选择哪个goroutine,对吗?我们正在并行运行我们的测试,我担心这会产生问题 -
也不睡觉。无论你使用什么,你都无法控制调度器的行为。
标签: unit-testing go goroutine