go testing: support subtests in fuzzer

7kqas0il  于 4个月前  发布在  Go
关注(0)|答案(3)|浏览(50)

请考虑以下使用 testing.F 的测试:

func FuzzAtoi(f *testing.F) {
    for _, tc := range []struct {
        s        string
        expected int
    }{
        {s: "1", expected: 1},
        {s: "0", expected: 1},
    } {
        actual, err := strconv.Atoi(tc.s)
        if err != nil {
            f.Fatal(err)
        }
        if tc.expected != actual {
            f.Error(actual)
        }
    }
}

它会因为一条不友好的错误信息而失败:

=== RUN  FuzzAtoi
    atoi_test.go:77: 0
--- FAIL: FuzzAtoi (0.00s)
FAIL

通过在 f.Error 行中添加额外的上下文,可能会使其变得更好。但是对于 testing.T 来说,还有另一种方法可用,但 testing.F 不行 - 子测试:

func TestAtoi(t *testing.T) {
    for _, tc := range []struct {
        s        string
        expected int
    }{
        {s: "1", expected: 1},
        {s: "0", expected: 1},
    } {
        tc := tc
        t.Run(tc.s, func(t *testing.T) {
            actual, err := strconv.Atoi(tc.s)
            if err != nil {
                t.Fatal(err)
            }
            if tc.expected != actual {
                t.Error(actual)
            }
        })
    }
}

这会自动在错误信息中包含子测试名称,非常有帮助:

=== RUN   TestAtoi
=== RUN   TestAtoi/1
=== RUN   TestAtoi/0
    atoi_test.go:77: 0
--- FAIL: TestAtoi (0.00s)
    --- PASS: TestAtoi/1 (0.00s)
    --- FAIL: TestAtoi/0 (0.00s)
FAIL

最后,我希望能够这样做:

func FuzzAtoi(f *testing.F) {
    for _, tc := range []struct {
        s        string
        expected int
    }{
        {s: "1", expected: 1},
        {s: "0", expected: 1},
    } {
        tc := tc
        f.Run(tc.s, func(t *testing.T) { // note 1
            t.Parallel()

            actual, err := strconv.Atoi(tc.s)
            if err != nil {
                f.Fatal(err)
            }
            if tc.expected != actual {
                f.Error(actual)
            }

            f.Add(tc.s) // note 2
        })

        f.Add(tc.s) // note 3
    }

    f.Fuzz(func(t *testing.T, s string) {
        t.Parallel()

        // ...
    })
}

请注意,f.Run 接受一个接受 testing.T 而不是 testing.F 的函数。这允许并行运行子测试,并消除了关于模糊测试语料库的歧义 - 这样一来,整个 FuzzAtoi 函数只有一个。
f.Add 可以在两个地方被调用 - 在子测试内部和外部。因此,它应该具有线程安全性。
这个问题与 #46780 不同,因为它要求为 testing.F 支持子测试,而不是更复杂的子模糊目标。

8tntrjer

8tntrjer1#

对不起,我仍然不确定我理解这个。

  • 在你的最后一个例子中,你对每个输入调用了两次F.Add。为什么?如果给F.Run额外的名字在输出中是有用的,那么为什么不在两个地方都有用呢?
  • 为什么在调用F.Add之前测试种子输入值?F.Fuzz可以检查它们吗?我想你只会对种子输入值有预期值,但为什么不只是有一个单独的单元测试呢?
  • 我不确定我在这里看到并行性的优势。顶级模糊目标只需要为每个输入调用F.Add,然后是F.Fuzz。它可能不应该做太多计算,除了那个。传递给F.Fuzz的模糊函数可能会调用T.Parallel,这将并行运行种子输入。但是在模糊时没有效果;多个输入将在不同的进程中并行运行,无论是否调用了T.Parallel

我希望这是这样写的:

package atoi

import (
	"strconv"
	"testing"
)

var atoiTests = []struct {
	name, s  string
	expected int
}{
	{"zero", "0", 0},
	{"one", "1", 1},
}

func TestAtoi(t *testing.T) {
	for _, test := range atoiTests {
		test := test
		t.Run(test.name, func(t *testing.T) {
			t.Parallel()
			n, err := strconv.Atoi(test.s)
			if err != nil {
				t.Fatal(err)
			}
			if n != test.expected {
				t.Errorf("got %d; want %d", n, test.expected)
			}
		})
	}
}

func FuzzAtoi(f *testing.F) {
	for _, test := range atoiTests {
		f.Add(test.s)
	}
	f.Fuzz(func(t *testing.T, s string) {
		t.Parallel()
		n, err := strconv.Atoi(s)
		if err != nil {
			return
		}
		got := strconv.Itoa(n)
		if s != got {
			t.Fatalf("converting %q to integer and back, got %q", s, got)
		}
	})
}
lg40wkob

lg40wkob2#

在你的最后一个例子中,你对每个输入都调用了两次F.Add。为什么?
我在文本中尝试解释过这个问题:“f.Add可能在子测试的内部和外部都被调用。因此,它应该是线程安全的。”当然,它不应该这样,但我的想法是展示用户可以在任何地方选择它,而且它们都不会出错。
为什么要在调用F.Add之前测试种子输入值?F.Fuzz可以检查它们吗?我想你只会为种子输入值有预期值,但为什么不单独编写一个单元测试呢?
我想这就是问题的核心——我不想单独编写一个单元测试。我正在处理的实际包是一个数据编组器/解组器。正确数据的单元测试几乎与模糊测试(将字节解组到对象,重新编组该对象,比较字节)相同,所以我将它们合并在一起,就像这个例子试图展示的那样。模糊测试提案草案提到了这一点:“从长远来看,这种设计可能会开始取代现有的表测试,无缝地融入到现有的Go测试生态系统中。”也许我只是活在未来。
我不确定在这里看到并行性优势。
如果你把那部分当作一个单元测试,那么并行性优势就很明显了。
我希望这会被写成这样:
请注意,你的示例:

  • TestAtoiFuzzAtoi 中包含类似的代码(实际情况下共享代码更多);
  • 包含一个共享的包级变量。

我的建议允许编写一个单一的测试函数,既测试正常情况,又将它们提供给模糊测试器。然后唯一应该单独编写的测试是返回错误值的测试。

zc0qhyus

zc0qhyus3#

感谢您的解释,我认为我更好地理解了您的意思。
这将是一个相当大的设计变化,所以我想确保@katiehockman和@rolandshoemaker也对此有发言权。Katie休假几周,所以我们不能立即做出决定。
我的API设计直觉是,保持testing.F更简单、专注于模糊测试会更好,即使它可以适应做testing.T在非模糊测试中能做的大部分事情。当然,这里还是有一些重叠的。

相关问题