登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go教程

Go testing/quick 怎么固定随机源并限制属性测试次数

来源:17golang原创

时间:2026-10-13 12:03:05 256浏览 收藏

使用 testing/quick 做属性测试时,最容易遇到两个实际问题:同一段测试每次生成的输入不一样,失败难以复现;默认检查次数又不够明确,调试阶段可能白白消耗较长时间。解决办法集中在 quick.Config 的两个字段:给 Rand 传入由固定种子创建的 *rand.Rand,再用 MaxCount 设定本次检查的最大迭代次数。

官方文档:https://pkg.go.dev/testing/quick#Config

要点速览
  • Rand 非空时,quick.Check 使用它生成属性测试输入;用同一个种子重新创建它,才能得到可复现的输入序列。
  • MaxCount 非零时直接作为最大迭代次数;为零时才会继续看 MaxCountScale 或默认的 -quickchecks 值。
  • 固定随机源只解决输入序列可重复,不等于自动保存失败样本;失败时应读取 *quick.CheckError 的 Count 和 In。

先看没有配置时的两个变量

quick.Check 接收一个返回 bool 的函数,并反复生成参数调用它。配置为空或字段保持零值时,随机输入来自包内部的默认逻辑,最大次数也来自默认配置。这样的写法适合快速试探,但不适合定位“第几轮出现问题”或比较一次改动前后的成本。

可以把一次属性检查拆成两个可观察指标:输入序列和调用上限。前者由 Rand 决定,后者由 MaxCount 或其回退规则决定。两者互相独立:换种子会改变输入,不会改变上限;调大次数会增加尝试机会,也不会让随机源自动变得可复现。

最小配置:同一个种子得到可复现输入

为 Rand 创建独立的伪随机源,并把它放入 quick.Config。下面的属性函数只检查一个整数的绝对值不会为负,示例重点在配置关系,不把代码执行结果当成文章证据。

package quickdemo

import (
    "math/rand"
    "testing/quick"
)

func checkAbsProperty() error {
    // 固定种子后,每次重新创建的 Rand 都从同一位置开始取数。
    source := rand.New(rand.NewSource(20261012))

    // MaxCount 控制最多尝试多少组自动生成的输入。
    config := &quick.Config{
        Rand:      source,
        MaxCount:  40,
    }

    // quick.Check 要求属性函数返回一个 bool。
    property := func(x int) bool {
        // 这里只演示输入和次数配置,不引入失败样本持久化。
        if x == -int(^uint(0)>>1)-1 {
            return true
        }
        return abs(x) >= 0
    }

    return quick.Check(property, config)
}

func abs(x int) int {
    // 对最小负整数的处理属于业务边界,示例不把它当作测试结论。
    if x 

这里的关键不是种子数字本身,而是每次需要复现时都重新执行 rand.New(rand.NewSource(20261012))。如果把已经消耗过一部分随机数的 Rand 跨测试复用,下一次检查会从不同位置继续取数,复现条件就被测试顺序改变了。

Go testing quick 中 quick.Check、Config、Rand、seed 和 MaxCount 的静态关系说明图
图1:Config、固定随机源和迭代上限的静态关系图,不是运行截图。

Rand 固定的是输入序列,不是测试结果

Config.Rand 的类型是 *math/rand.Rand。当它为 nil 时,当前 Go 标准库源码会为本次检查创建一个新的伪随机源;显式传入自己的源,则由调用方负责决定种子和生命周期。因此,固定种子能让自动生成值的顺序稳定下来,但不能替属性函数修复逻辑错误。

复现失败时,建议把“种子”和“失败轮次”放在同一条记录中。种子回答“怎样重放输入序列”,CheckError.Count 回答“哪一轮首先失败”,CheckError.In 则提供该轮的输入。它们是三种不同信息,不要只保存其中一个。

package quickdemo

import (
    "errors"
    "fmt"
    "math/rand"
    "testing/quick"
)

func runWithSeed(seed int64) error {
    // 每次重放都从同一个 seed 新建源,避免受到前一次调用的消耗影响。
    source := rand.New(rand.NewSource(seed))
    config := &quick.Config{Rand: source, MaxCount: 40}

    // 属性函数只负责返回真假;失败输入由 CheckError 提供。
    property := func(x int) bool {
        return x == 0 || x/2*2 == x
    }

    err := quick.Check(property, config)
    if err == nil {
        // nil 只能说明本次上限内没有找到反例,不代表所有输入都正确。
        return nil
    }

    var checkErr *quick.CheckError
    if errors.As(err, &checkErr) {
        return fmt.Errorf("seed=%d count=%d input=%v: %w", seed, checkErr.Count, checkErr.In, err)
    }
    // SetupError 等配置问题不应被伪装成属性失败。
    return err
}

示例中的 seed、count 和 input 是诊断信息的组织方式。实际项目可以把它们写入测试日志或错误信息,但不要把一次失败输入直接当作永久规则:改变属性函数、Go 版本或自定义生成逻辑后,同一个种子可能不再代表相同的输入过程。

MaxCount 到底限制了什么

MaxCount 限制的是 quick.Check 调用属性函数的最大轮数,而不是每个输入的大小,也不是失败样本的数量。源码中的检查循环以 i 为条件,每轮先生成参数,再调用属性函数;如果函数返回 false,会立即返回 CheckError,因此实际调用次数可能小于上限。

例如把 MaxCount 设为 40,它表达的是“最多检查 40 组输入”。如果第 7 轮就失败,错误中的 Count 会指向第 7 轮;如果 40 轮都返回 true,检查结束并返回 nil。后者只能说明在当前生成规则和这 40 轮输入内没有找到反例。

Go testing quick 中 MaxCount、输入生成、属性函数和 CheckError Count 的静态边界图
图2:最大迭代次数、属性函数与失败轮次之间的静态边界图,不是运行截图。

MaxCount 和 MaxCountScale 怎么选

这两个字段有明确的优先级。只要 MaxCount 非零,就直接采用它;只有当 MaxCount == 0 时,才会检查 MaxCountScale。缩放值也为零时,使用默认最大次数;当前标准库源码把默认值设为 100,同时允许通过 -quickchecks 标志调整默认值。

配置有效规则适合场景
MaxCount: 20直接最多 20 轮快速定位、单测级别的稳定成本
MaxCount: 0, MaxCountScale: 0.5按默认次数的一半计算统一降低一组检查的默认成本
两个字段都为零使用默认最大次数只想快速接入,不需要显式控制预算
两个字段都非零MaxCount 优先避免把缩放规则误当成硬上限

工程上可以先用较小的 MaxCount 调试属性函数,再在持续集成中提高次数。提高次数并不会替代更好的输入生成策略;如果属性函数覆盖不到关键边界,盲目把 100 轮改成 10000 轮只会扩大运行成本。

用简单指标观察测试成本

这个主题适合用三个指标做前后对比:每次检查的最大轮数、首次失败轮次、整组测试耗时。不要把“没有找到反例”写成性能结果,也不要把示例中的配置数字写成已经在某台机器上测得的基准。

  • 复现性:固定种子后,记录同一测试的 seed 和 CheckError.Count,比较失败是否能回到同一输入边界。
  • 成本:在测试框架或 CI 日志中观察总耗时,分别比较 MaxCount=20、MaxCount=100 等配置。
  • 覆盖:不要只看轮数,还要检查生成值是否覆盖空切片、负数、零值和结构体边界;次数多不代表输入空间合理。

如果测试需要精确保存并回放某个失败输入,应在属性测试之外设计自己的样本记录机制。本文只讨论 Config.Rand 和 MaxCount,它们负责控制生成过程和检查预算,不提供失败样本文件格式。

常见误区和一份速查表

第一,不要把全局随机源直接塞进多个并行测试。共享状态会让测试顺序、并发调度和种子复现互相影响;每个需要稳定复现的检查应独立创建 *rand.Rand。第二,不要把 MaxCount 理解为“至少执行多少次”,它是上限,遇到第一个反例会提前结束。第三,MaxCountScale 不是和 MaxCount 相乘的附加系数,前者只在后者为零时生效。

想解决的问题应该设置不要误解为
失败输入需要稳定重放重新用固定 seed 创建 Rand自动保存失败样本
调试时减少运行成本设置较小的 MaxCount保证覆盖所有边界
统一调整默认检查预算设置 MaxCountScale,并保持 MaxCount 为零和非零 MaxCount 叠加
定位哪一轮失败读取 *quick.CheckError 的 Count 与 In只看错误字符串就完成持久化

常见问题

把 Rand 设置为 nil 会使用固定的默认种子吗?

不会把它当成固定种子使用。Rand 为空时,testing/quick 会创建默认伪随机源;要稳定复现,应显式传入由固定种子创建的 *rand.Rand。

MaxCount 设置为 0 是不是不检查?

不是。零值表示回退到 MaxCountScale 或默认最大次数;它不是“跳过检查”。如果确实不希望执行,应在测试层面控制是否调用 quick.Check,不要依赖这个字段表达跳过语义。

MaxCountScale 设置为 0.5 一定得到 50 次吗?

它是按当前默认最大次数计算的缩放值,默认值通常为 100,但也可能受 -quickchecks 调整;此外最终结果会转换为整数。需要明确硬上限时,直接设置非零的 MaxCount 更清楚。

同一个 Rand 可以给多个 quick.Check 使用吗?

技术上可以传入,但后一个检查会接着消耗前一个检查留下的随机状态,复现会依赖执行顺序。需要隔离和稳定日志时,为每个检查用同一种子分别创建新的 Rand。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>