go - 并行表驱动的go测试失败

标签 go testing

我有以下测试功能


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将引用与循环完全相同的迭代变量vv := 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的函数文字会关闭vexpectedOutputcmpOpts.SingleApp(可能还有其他),
然后t.Run()使该函数文字在单独的goroutine中运行,如所记录的那样-在expectedOutputcmpOpts.SingleApp以及其他不是v(每次迭代的新变量)或t(传递给函数文字)。

您可以运行go test -race -run=TestIntegrationAppsWithProductionSelf ./...来查看参与的竞赛检测器使测试用例的代码崩溃。

关于go - 并行表驱动的go测试失败,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/61100947/

相关文章:

regex - 从捕获中排除符号并仅获取所需数据?

function - 更改函数中的全局 slice

go - 在 Go 中,在 http 处理程序中使用带有 pgx 的上下文的正确方法是什么?

go - Golang处理错误返回时删除重复的代码

ruby-on-rails - 无方法错误 : undefined method `env' for nil:NilClass

android - Robotium 和 Sherlock - NoClassDefFoundError

Golang 的 GIN 中的 JSON 响应作为乱码数据返回

testing - 你如何在 clojure 中使用它自己的命名空间之外的类型?

c# - 在类中插入值而不显式写入它们

testing - 如何测试与其他模块服务关联的 Controller ?嵌套