我有以下测试功能
func TestIntegrationAppsWithProductionSelf(t *testing.T) {
// here is where the apps array that will act as my test suite is being populated
myapps, err := RetrieveApps(fs)
for _, v := range apps {
v := v
t.Run("", func(t *testing.T) {
t.Parallel()
expectedOutput = `=` + v + `
`
cmpOpts.SingleApp = v
t.Logf("\t\tTesting %s\n", v)
buf, err := VarsCmp(output, cmpOpts)
if err != nil {
t.Fatalf("ERROR executing var comparison for %s: %s\n", v, err)
}
assert.Equal(t, expectedOutput, buf.String())
})
}
}
尽管我删除了
t.Parallel()
(甚至保留了子测试结构),测试仍然成功,但是测试失败了。失败(仅当合并了
t.Parallel()
时才发生这种情况)与以下事实有关:传递给assert的要比较的值不同步,即assert
方法比较了不应该比较的值)这是为什么?
我还对测试套件变量(
v := v
)进行了这种神秘的重新分配,我不理解)编辑:徘徊是否是this包中
assert
方法的用法,我做了以下替换,尽管最终结果是相同的, //assert.Equal(t, expectedOutput, buf.String())
if expectedOutput != buf.String() {
t.Errorf("Failed! Expected %s - Actual: %s\n", expectedOutput, buf.String())
}
最佳答案
让我们分析一下情况。
首先,让我们引用the docs on testing.T.Run
:
Run runs f as a subtest of t called name. It runs f in a separate goroutine <…>
(强调我的。)
因此,当您调用
t.Run("some_name", someFn)
时,测试套件正在运行该SomeFn
,就像您手动执行类似操作go someFn(t)
接下来,请注意,您没有将命名函数传递给对
t.Run
的调用,而是将其传递给所谓的函数文字。让我们引用the language spec on them:Function literals are closures: they may refer to variables defined in a surrounding function. Those variables are then shared between the surrounding function and the function literal, and they survive as long as they are accessible.
在您的情况下,这意味着编译器在编译函数文字的主体时,会使函数“覆盖”其主体所提及的任何变量,而这不是形式函数的参数之一;在您的情况下,唯一的函数参数是
t *testing.T
,因此创建的闭包会捕获所有其他访问的变量。在Go中,当函数文字关闭变量时,它会通过保留对该变量的引用来实现这一点-在规范中明确提及为(«然后,这些变量在周围的函数与函数文字<...之间共享 … >»,再次强调我的意思。)
现在,请注意Go中的循环在每次迭代时都重复使用迭代变量;也就是说,当你写
for _, v := range apps {
该变量
v
在循环的“外部”范围内创建一次,然后在循环的每次迭代中重新分配。回顾一下:同一变量的存储位于内存中的某个固定点,因此每次迭代都会为其分配一个新值。现在,由于函数文字通过保留对外部变量的引用来封闭外部变量(而不是将其定义的“时间”将其值复制到自身中),因此无需那种看上去很时髦的
v := v
“欺骗”每次调用时创建的每个函数文字循环中的t.Run
将引用与循环完全相同的迭代变量v
。v := v
构造函数声明了另一个名为v
的变量,该变量在循环主体中是本地的,同时为它分配了循环迭代变量v
的值。由于本地v
“shadows”循环迭代器的v
,此后声明的函数文字将覆盖该局部变量,因此,在每次迭代中创建的每个函数文字都将覆盖不同的,独立的变量v
。您可能会问为什么需要这样做?
之所以需要这样做是因为循环迭代变量和goroutines的相互作用存在一个微妙的问题,即detailed on the Go wiki:
当某人做某事时
for _, v := range apps {
go func() {
// use v
}()
}
创建关闭
v
的函数文字,然后使用go
语句运行-与运行循环的goroutine以及在len(apps)-1
其他迭代上启动的所有其他goroutine并行。这些运行函数文字的goroutine都引用相同的
v
,因此它们都在该变量上进行数据竞争:运行looop的goroutine对其进行写入,并且运行函数文字的goroutines从中读取数据-同时并没有任何同步。我希望,到目前为止,您应该可以看到难题的各个部分:在代码中
for _, v := range apps {
v := v
t.Run("", func(t *testing.T) {
expectedOutput = `=` + v + `
// ...
传递给
t.Run
的函数文字会关闭v
,expectedOutput
,cmpOpts.SingleApp
(可能还有其他),然后
t.Run()
使该函数文字在单独的goroutine中运行,如所记录的那样-在expectedOutput
和cmpOpts.SingleApp
以及其他不是v
(每次迭代的新变量)或t
(传递给函数文字)。您可以运行
go test -race -run=TestIntegrationAppsWithProductionSelf ./...
来查看参与的竞赛检测器使测试用例的代码崩溃。
关于go - 并行表驱动的go测试失败,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61100947/