为了账号安全,请及时绑定邮箱和手机立即绑定

Go 中的并行表驱动测试惨遭失败

Go 中的并行表驱动测试惨遭失败

Go
慕雪6442864 2022-05-23 17:59:59
我有以下测试功能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方法比较了它不应该比较的值)这是为什么?v := v我还对我不理解的测试套件变量 ( ) 执行了这种神秘的重新分配)编辑:徘徊如果是使用这个assert包中的方法,我做了以下替换,但最终结果是一样的,    //assert.Equal(t, expectedOutput, buf.String())    if expectedOutput != buf.String() {        t.Errorf("Failed! Expected %s - Actual: %s\n", expectedOutput, buf.String())    }
查看完整描述

2 回答

?
犯罪嫌疑人X

TA贡献2080条经验 获得超4个赞

让我们剖析一下这个案例。


首先,让我们参考以下文档testing.T.Run:


Run 运行 f 作为名为 name 的 t 的子测试。 它在一个单独的 goroutine <...>中运行 f


(强调我的。)


因此,当您调用 时t.Run("some_name", someFn),SomeFn测试套件正在运行它,就好像您会手动执行类似的操作一样


go someFn(t)

接下来,让我们注意,您没有将命名函数传递给对 的调用t.Run,而是传递了一个所谓的函数字面量;让我们引用它们的语言规范:


函数字面量是闭包:它们可以引用在周围函数中定义的变量。然后,这些变量在周围的函数和函数字面量之间共享,只要它们可以访问,它们就会继续存在。


在您的情况下,这意味着当编译器编译函数文字的主体时,它会使函数“关闭”其主体提到的任何变量,而这不是正式的函数参数之一;在您的情况下,唯一的函数参数是t *testing.T,因此所有其他访问的变量都被创建的闭包捕获。


在 Go 中,当一个函数文字关闭一个变量时,它通过保留对该变量的引用来做到这一点——规范中明确提到(«这些变量然后在周围的函数和函数文字之间共享<...>» ,再次强调我的。)


现在请注意,Go 中的循环在每次迭代中重用迭代变量;也就是说,当你写


for _, v := range apps {

该变量v在循环的“外部”范围内创建一次,然后在循环的每次迭代中重新分配。回顾一下:相同的变量,其存储位于内存中的某个固定点,在每次迭代时都被分配一个新值。


现在,由于函数字面量通过保留对外部变量的引用来关闭外部变量——而不是在其定义的“时间”将它们的值复制到自身中——没有那种看起来时髦的v := v“技巧”,每个函数字面量在每次调用时创建t.Run在您的循环中将引用完全相同的循环迭代变量v。

该v := v构造声明了另一个名为的变量v,它是循环体的局部变量,同时为其分配了循环迭代变量的值v。由于本地v“阴影”循环迭代器的v,之后声明的函数文字将关闭该局部变量,因此在每次迭代中创建的每个函数文字都将关闭一个不同的单独变量v。


你可能会问,为什么需要这个?


这是必要的,因为循环迭代变量和 goroutine 的交互存在一个微妙的问题,Go wiki 对此进行了详细说明:当一个人执行类似的操作时


for _, v := range apps {

  go func() {

    // use v

  }()

}

创建一个函数字面量关闭v,然后使用语句运行它——与运行循环的 goroutine 以及在其他迭代中go启动的所有其他 goroutine 并行。 这些运行我们的函数字面量的 goroutine 都引用同一个变量,因此它们都在该变量上存在数据竞争:运行循环的 goroutine写入它,而运行函数字面量的 goroutines从它读取——并发且没有任何同步。len(apps)-1

v


我希望,现在您应该看到拼图的各个部分:在代码中


    for _, v := range apps {

        v := v

        t.Run("", func(t *testing.T) {

            expectedOutput = `=` + v + `

            // ...

传递给的函数文字t.Run结束v, expectedOutput, cmpOpts.SingleApp(并且可能是其他东西),然后t.Run()使该函数文字在单独的 goroutine 中运行,如记录的那样,在 and 上产生经典的数据竞争expectedOutput,cmpOpts.SingleApp以及其他任何不是v的(新鲜的每次迭代的变量)或t(传递给函数字面量的调用)。


您可能会跑去go test -race -run=TestIntegrationAppsWithProductionSelf ./...看到参与的竞赛检测器使您的测试用例的代码崩溃。


查看完整回答
反对 回复 2022-05-23
?
三国纷争

TA贡献1804条经验 获得超7个赞

我将发布实际有效的内容,但是(除非问题已关闭)我会接受实际详细说明的答案。


问题是用于存储的变量是在函数expectedOutput内部但在循环外部声明的(这现在反映在初始问题的代码片段中)。TestIntegrationAppsWithProductionSelffor


有效的是删除var expectedOutput string语句并在for循环中执行


    for _, v := range apps {

        v := v

        expectedOutput := `=` + v + `

`

        t.Run("", func(t *testing.T) {

            t.Parallel()

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

            if expectedOutput != buf.String() {

                t.Errorf("Failed! Expected %s - Actual: %s\n", expectedOutput, buf.String())

            }

        })

    }


查看完整回答
反对 回复 2022-05-23
  • 2 回答
  • 0 关注
  • 157 浏览
慕课专栏
更多

添加回答

举报

0/150
提交
取消
微信客服

购课补贴
联系客服咨询优惠详情

帮助反馈 APP下载

慕课网APP
您的移动学习伙伴

公众号

扫描二维码
关注慕课网微信公众号