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 跨测试复用,下一次检查会从不同位置继续取数,复现条件就被测试顺序改变了。

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 轮输入内没有找到反例。

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。
-
288 收藏
-
232 收藏
-
261 收藏
-
308 收藏
-
501 收藏
-
338 收藏
-
157 收藏
-
394 收藏
-
453 收藏
-
173 收藏
-
423 收藏
-
Golang · Go教程 | 11小时前 | 基准测试 · testing · 性能 · Go教程 · GOMAXPROCS RunParallel Go testing.B.SetParallelism 并行基准 IO吞吐404 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习