testing/quick 包详解
概述
testing/quick 包实现了用于黑盒测试的实用函数。它通过自动生成随机测试数据来帮助发现代码中的 bug。
核心功能:
- 属性测试(Property-based Testing)
- 随机数据生成
- 函数等价性测试
- 自定义生成器支持
- 测试配置控制
重要说明:
- ✅ Go 版本:所有 Go 版本都支持
- ⚠️ 已冻结:该包已冻结,不接受新功能
- ✅ 测试用途:用于发现边界条件和意外输入
- ⚠️ 结构体要求:生成任意结构体值时,所有字段必须导出
包导入
import "testing/quick"
类型详解(按 A-Z 分类)
C
CheckEqualError
type CheckEqualError struct {
CheckError
Out1 []interface{}
Out2 []interface{}
}
功能: CheckEqual 发现错误时的结果。
字段:
CheckError- 基础错误信息Out1 []interface{}- 第一个函数的输出Out2 []interface{}- 第二个函数的输出
方法:
Error() string- 实现 error 接口
示例:
package main
import (
"fmt"
"testing/quick"
)
func add1(x int) int {
return x + 1
}
func add2(x int) int {
return x + 2 // 不同的实现
}
func main() {
err := quick.CheckEqual(add1, add2, nil)
if err != nil {
if ce, ok := err.(*quick.CheckEqualError); ok {
fmt.Printf("输入:%v\n", ce.In)
fmt.Printf("函数 1 输出:%v\n", ce.Out1)
fmt.Printf("函数 2 输出:%v\n", ce.Out2)
}
}
}
CheckError
type CheckError struct {
Count int
In []interface{}
}
功能: Check 发现错误时的结果。
字段:
Count int- 成功运行的测试次数In []interface{}- 导致失败的输入
方法:
Error() string- 实现 error 接口
示例:
package main
import (
"fmt"
"testing/quick"
)
func main() {
// 一个会失败的测试
f := func(x int) bool {
return x > 0 // 对负数失败
}
err := quick.Check(f, nil)
if err != nil {
if ce, ok := err.(*quick.CheckError); ok {
fmt.Printf("运行了 %d 次测试\n", ce.Count)
fmt.Printf("失败输入:%v\n", ce.In)
}
}
}
C
Config
type Config struct {
// MaxCount 设置最大迭代次数。如果为零,使用 MaxCountScale
MaxCount int
// MaxCountScale 是默认最大值的非负比例因子
// 如果为零,默认值不变
MaxCountScale float64
// 如果非 nil,rand 是随机数源
// 否则使用默认的伪随机源
Rand *rand.Rand
// 如果非 nil,Values 函数生成任意 reflect.Value 切片
// 这些值与被测函数的参数一致
// 否则使用顶层 Values 函数生成它们
Values func([]reflect.Value, *rand.Rand)
}
功能: 包含运行测试的选项。
字段说明:
MaxCount:
- 最大迭代次数
- 默认值:100(小类型)或 8(大类型)
- 如果设置为 0,使用 MaxCountScale
MaxCountScale:
- 比例因子
- 实际 MaxCount = 默认值 × MaxCountScale
- 如果为 0,使用默认值
Rand:
- 随机数源
- 如果为 nil,使用默认随机源
- 可用于重现测试
Values:
- 自定义值生成函数
- 用于生成特定类型的测试数据
示例:
package main
import (
"math/rand"
"testing/quick"
)
func main() {
// 自定义配置
config := &quick.Config{
MaxCount: 1000, // 运行 1000 次测试
MaxCountScale: 2.0, // 默认值的 2 倍
Rand: rand.New(rand.NewSource(42)), // 可重现
}
f := func(x int) bool {
return x == x // 总是 true
}
quick.Check(f, config)
}
G
Generator
type Generator interface {
// Generate 使用 size 作为大小提示返回类型的随机实例
Generate(rand *rand.Rand, size int) reflect.Value
}
功能: 可以生成自身类型随机值的接口。
方法:
Generate(rand *rand.Rand, size int) reflect.Value- 生成随机值
参数:
rand *rand.Rand- 随机数源size int- 大小提示(用于控制生成数据的复杂度)
返回值:
reflect.Value- 生成的随机值
示例:
package main
import (
"math/rand"
"reflect"
"testing/quick"
)
// 自定义类型
type PositiveInt int
// 实现 Generator 接口
func (PositiveInt) Generate(rand *rand.Rand, size int) reflect.Value {
// 只生成正数
return reflect.ValueOf(PositiveInt(rand.Intn(size) + 1))
}
func main() {
// 测试只接受正数的函数
f := func(x PositiveInt) bool {
return x > 0
}
quick.Check(f, nil)
}
S
SetupError
type SetupError string
功能: 使用 check 方式错误时的结果,与被测函数无关。
方法:
Error() string- 实现 error 接口
触发场景:
- 函数签名不正确
- 参数类型不支持
- 配置错误
示例:
package main
import (
"fmt"
"testing/quick"
)
func main() {
// 不是函数,会返回 SetupError
err := quick.Check("not a function", nil)
if err != nil {
if se, ok := err.(quick.SetupError); ok {
fmt.Printf("设置错误:%s\n", se)
}
}
}
函数详解(按 A-Z 分类)
C
Check
func Check(f interface{}, config *Config) error
功能: 查找使函数 f 返回 false 的输入。
参数:
f interface{}- 返回 bool 的函数config *Config- 测试配置(nil 使用默认配置)
返回值:
error- 如果找到失败输入,返回 *CheckError
函数要求:
- 必须返回 bool
- 参数可以是任意类型
- 返回 true 表示测试通过,false 表示失败
测试过程:
- 为函数参数生成随机值
- 调用函数 f
- 如果 f 返回 false,返回 *CheckError
- 重复直到达到 MaxCount
示例:
package main
import (
"fmt"
"testing/quick"
)
func main() {
// 测试加法交换律
f := func(a, b int) bool {
return a+b == b+a
}
if err := quick.Check(f, nil); err != nil {
fmt.Println("测试失败:", err)
} else {
fmt.Println("测试通过")
}
// 测试会失败的情况
f2 := func(x int) bool {
return x > 0 // 对负数失败
}
if err := quick.Check(f2, nil); err != nil {
fmt.Println("测试失败:", err)
}
}
运行结果:
测试通过
测试失败:#1: (0) failed on input: -1
CheckEqual
func CheckEqual(f, g interface{}, config *Config) error
功能: 查找使函数 f 和 g 返回不同结果的输入。
参数:
f interface{}- 第一个函数g interface{}- 第二个函数config *Config- 测试配置
返回值:
error- 如果找到不同输出,返回 *CheckEqualError
函数要求:
- f 和 g 必须有相同的签名
- 返回值必须可以比较
测试过程:
- 为函数参数生成随机值
- 同时调用 f 和 g
- 比较返回值
- 如果不同,返回 *CheckEqualError
示例:
package main
import (
"fmt"
"testing/quick"
)
// 两个等价的函数
func add1(a, b int) int {
return a + b
}
func add2(a, b int) int {
return b + a
}
// 两个不等价的函数
func mul1(a, b int) int {
return a * b
}
func mul2(a, b int) int {
return a * b + 1 // 不同
}
func main() {
// 测试等价的函数
if err := quick.CheckEqual(add1, add2, nil); err != nil {
fmt.Println("add1 和 add2 不等价:", err)
} else {
fmt.Println("add1 和 add2 等价")
}
// 测试不等价的函数
if err := quick.CheckEqual(mul1, mul2, nil); err != nil {
fmt.Println("mul1 和 mul2 不等价:", err)
}
}
运行结果:
add1 和 add2 等价
mul1 和 mul2 不等价:#1: (0, 0) gave different results: 0 vs 1
V
Value
func Value(t reflect.Type, rand *rand.Rand) (value reflect.Value, ok bool)
功能: 返回给定类型的任意值。
参数:
t reflect.Type- 目标类型rand *rand.Rand- 随机数源(nil 使用默认源)
返回值:
value reflect.Value- 生成的随机值ok bool- 是否成功生成
支持的类型:
- 基本类型:bool、int、uint、float、string 等
- 复合类型:slice、map、array、struct、pointer、channel、function
- 实现 Generator 接口的类型
注意:
- 结构体的所有字段必须导出
- 如果类型实现 Generator 接口,使用该接口生成
示例:
package main
import (
"fmt"
"reflect"
"testing/quick"
)
func main() {
// 生成 int
v, ok := quick.Value(reflect.TypeOf(0), nil)
if ok {
fmt.Printf("int: %v\n", v.Int())
}
// 生成 string
v, ok = quick.Value(reflect.TypeOf(""), nil)
if ok {
fmt.Printf("string: %q\n", v.String())
}
// 生成 slice
v, ok = quick.Value(reflect.TypeOf([]int{}), nil)
if ok {
fmt.Printf("slice: %v\n", v.Interface())
}
// 生成 struct
type Person struct {
Name string
Age int
}
v, ok = quick.Value(reflect.TypeOf(Person{}), nil)
if ok {
fmt.Printf("struct: %v\n", v.Interface())
}
}
典型示例
示例 1:基本属性测试
package main
import (
"fmt"
"testing/quick"
)
// 测试函数
func reverse(s string) string {
runes := []rune(s)
for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
runes[i], runes[j] = runes[j], runes[i]
}
return string(runes)
}
func main() {
// 属性:反转两次等于原字符串
f := func(s string) bool {
return reverse(reverse(s)) == s
}
if err := quick.Check(f, nil); err != nil {
fmt.Println("测试失败:", err)
} else {
fmt.Println("测试通过:反转两次等于原字符串")
}
}
示例 2:数学属性测试
package main
import (
"fmt"
"testing/quick"
)
func abs(x int) int {
if x < 0 {
return -x
}
return x
}
func main() {
// 属性 1:绝对值总是非负
f1 := func(x int) bool {
return abs(x) >= 0
}
quick.Check(f1, nil)
// 属性 2:abs(abs(x)) == abs(x)
f2 := func(x int) bool {
a := abs(x)
return abs(a) == a
}
quick.Check(f2, nil)
// 属性 3:abs(x) == abs(-x)
f3 := func(x int) bool {
return abs(x) == abs(-x)
}
if err := quick.Check(f3, nil); err != nil {
fmt.Println("测试失败:", err)
} else {
fmt.Println("测试通过:abs(x) == abs(-x)")
}
}
示例 3:比较两个实现
package main
import (
"fmt"
"testing/quick"
)
// 递归实现
func fibRecursive(n int) int {
if n <= 1 {
return n
}
return fibRecursive(n-1) + fibRecursive(n-2)
}
// 迭代实现
func fibIterative(n int) int {
if n <= 1 {
return n
}
a, b := 0, 1
for i := 2; i <= n; i++ {
a, b = b, a+b
}
return b
}
func main() {
// 只测试小数字(递归实现慢)
config := &quick.Config{MaxCount: 50}
f := func(n int8) bool {
if n < 0 {
n = -n
}
if n > 20 {
n = 20 // 限制范围
}
return fibRecursive(int(n)) == fibIterative(int(n))
}
if err := quick.Check(f, config); err != nil {
fmt.Println("测试失败:", err)
} else {
fmt.Println("测试通过:两种实现等价")
}
}
示例 4:自定义生成器
package main
import (
"fmt"
"math/rand"
"reflect"
"testing/quick"
)
// 只生成偶数
type EvenInt int
func (EvenInt) Generate(rand *rand.Rand, size int) reflect.Value {
return reflect.ValueOf(EvenInt(rand.Intn(size/2+1) * 2))
}
// 只生成正数
type PositiveInt int
func (PositiveInt) Generate(rand *rand.Rand, size int) reflect.Value {
return reflect.ValueOf(PositiveInt(rand.Intn(size) + 1))
}
func main() {
// 测试偶数属性
f1 := func(x EvenInt) bool {
return int(x)%2 == 0
}
quick.Check(f1, nil)
// 测试正数属性
f2 := func(x PositiveInt) bool {
return int(x) > 0
}
quick.Check(f2, nil)
fmt.Println("自定义生成器测试通过")
}
示例 5:切片操作测试
package main
import (
"fmt"
"testing/quick"
)
func appendInt(s []int, x int) []int {
return append(s, x)
}
func main() {
// 属性:append 后长度加 1
f1 := func(s []int, x int) bool {
result := appendInt(s, x)
return len(result) == len(s)+1
}
quick.Check(f1, nil)
// 属性:append 的元素在最后
f2 := func(s []int, x int) bool {
result := appendInt(s, x)
return result[len(result)-1] == x
}
quick.Check(f2, nil)
// 属性:原元素不变
f3 := func(s []int, x int) bool {
result := appendInt(s, x)
for i := 0; i < len(s); i++ {
if result[i] != s[i] {
return false
}
}
return true
}
if err := quick.Check(f3, nil); err != nil {
fmt.Println("测试失败:", err)
} else {
fmt.Println("测试通过:append 保持原元素")
}
}
示例 6:Map 操作测试
package main
import (
"fmt"
"testing/quick"
)
func copyMap(m map[string]int) map[string]int {
result := make(map[string]int)
for k, v := range m {
result[k] = v
}
return result
}
func main() {
// 属性:复制的 map 长度相同
f1 := func(m map[string]int) bool {
return len(copyMap(m)) == len(m)
}
quick.Check(f1, nil)
// 属性:复制的 map 包含相同的键值对
f2 := func(m map[string]int) bool {
copy := copyMap(m)
for k, v := range m {
if copy[k] != v {
return false
}
}
return true
}
if err := quick.Check(f2, nil); err != nil {
fmt.Println("测试失败:", err)
} else {
fmt.Println("测试通过:map 复制正确")
}
}
示例 7:使用配置
package main
import (
"fmt"
"math/rand"
"testing/quick"
)
func main() {
// 自定义配置
config := &quick.Config{
MaxCount: 1000, // 运行 1000 次
MaxCountScale: 1.0, // 不缩放
Rand: rand.New(rand.NewSource(42)), // 固定种子
}
// 测试排序属性
f := func(a, b, c int) bool {
nums := []int{a, b, c}
// 简单排序
for i := 0; i < len(nums)-1; i++ {
for j := i+1; j < len(nums); j++ {
if nums[i] > nums[j] {
nums[i], nums[j] = nums[j], nums[i]
}
}
}
// 验证有序
for i := 0; i < len(nums)-1; i++ {
if nums[i] > nums[i+1] {
return false
}
}
return true
}
if err := quick.Check(f, config); err != nil {
fmt.Println("测试失败:", err)
} else {
fmt.Println("测试通过:排序正确")
}
}
示例 8:结构体测试
package main
import (
"fmt"
"testing/quick"
)
type Point struct {
X int
Y int
}
func distance(p1, p2 Point) int {
dx := p1.X - p2.X
dy := p1.Y - p2.Y
return dx*dx + dy*dy
}
func main() {
// 属性:距离是对称的
f1 := func(p1, p2 Point) bool {
return distance(p1, p2) == distance(p2, p1)
}
quick.Check(f1, nil)
// 属性:到自身的距离为 0
f2 := func(p Point) bool {
return distance(p, p) == 0
}
quick.Check(f2, nil)
fmt.Println("测试通过:距离函数属性正确")
}
最佳实践
1. 定义清晰的属性
// ✅ 推荐:清晰的属性
func TestReverse(t *testing.T) {
// 属性:反转两次等于原字符串
f := func(s string) bool {
return reverse(reverse(s)) == s
}
quick.Check(f, nil)
}
// ❌ 不推荐:模糊的属性
func TestReverse(t *testing.T) {
f := func(s string) bool {
r := reverse(s)
return len(r) == len(s) // 太弱
}
quick.Check(f, nil)
}
2. 使用自定义生成器
// ✅ 推荐:自定义生成器
type PositiveInt int
func (PositiveInt) Generate(rand *rand.Rand, size int) reflect.Value {
return reflect.ValueOf(PositiveInt(rand.Intn(size) + 1))
}
// ❌ 不推荐:在测试中过滤
f := func(x int) bool {
if x <= 0 {
return true // 跳过
}
// 测试...
}
3. 限制测试范围
// ✅ 推荐:限制范围
f := func(n int8) bool {
if n > 100 {
n = 100
}
// 测试...
}
// ❌ 不推荐:可能导致溢出或超时
f := func(n int) bool {
// 使用 n,可能很大
}
4. 使用 CheckEqual 比较实现
// ✅ 推荐:比较两个实现
quick.CheckEqual(slowImplementation, fastImplementation, nil)
// ❌ 不推荐:手动比较
f := func(input Input) bool {
return slow(input) == fast(input)
}
quick.Check(f, nil)
5. 配置可重现的测试
// ✅ 推荐:固定随机种子
config := &quick.Config{
Rand: rand.New(rand.NewSource(42)),
}
quick.Check(f, config)
// ❌ 不推荐:无法重现失败
quick.Check(f, nil)
与其他包配合
与 testing 包配合
package mypackage
import (
"testing"
"testing/quick"
)
func TestAddition(t *testing.T) {
f := func(a, b int) bool {
return a+b == b+a
}
if err := quick.Check(f, nil); err != nil {
t.Error(err)
}
}
与 math/rand 配合
package main
import (
"math/rand"
"testing/quick"
)
func main() {
// 使用自定义随机源
config := &quick.Config{
Rand: rand.New(rand.NewSource(time.Now().UnixNano())),
}
f := func(x int) bool {
return x == x
}
quick.Check(f, config)
}
注意事项
限制
-
包已冻结:
- 不再接受新功能
- 考虑使用第三方属性测试库
-
结构体要求:
- 所有字段必须导出
- 否则无法生成随机值
-
性能考虑:
- 默认运行 100 次测试
- 复杂测试可能较慢
-
随机性:
- 测试失败可能难以重现
- 使用固定种子重现问题
使用建议
-
属性选择:
- 选择明确的数学属性
- 避免过于复杂的属性
-
测试范围:
- 限制输入范围避免溢出
- 对大输入使用较小的 MaxCount
-
错误处理:
- 检查 Check 返回的错误
- 使用 CheckError 获取失败输入
-
可重现性:
- 使用固定随机种子
- 记录失败时的输入
快速参考
函数速查表
| 函数 | 功能 | 返回值 |
|---|---|---|
Check(f, config) | 查找使 f 返回 false 的输入 | *CheckError |
CheckEqual(f, g, config) | 查找使 f 和 g 返回不同结果的输入 | *CheckEqualError |
Value(t, rand) | 生成类型 t 的随机值 | reflect.Value |
类型速查表
| 类型 | 功能 |
|---|---|
Config | 测试配置选项 |
CheckError | Check 发现的错误 |
CheckEqualError | CheckEqual 发现的错误 |
Generator | 自定义生成器接口 |
SetupError | 设置错误 |
Config 字段
| 字段 | 默认值 | 说明 |
|---|---|---|
MaxCount | 100 或 8 | 最大迭代次数 |
MaxCountScale | 0 | 比例因子 |
Rand | nil | 随机数源 |
Values | nil | 自定义值生成函数 |
常见模式
// 1. 基本属性测试
f := func(x int) bool {
return property(x)
}
quick.Check(f, nil)
// 2. 比较两个实现
quick.CheckEqual(impl1, impl2, nil)
// 3. 自定义配置
config := &quick.Config{
MaxCount: 1000,
Rand: rand.New(rand.NewSource(42)),
}
quick.Check(f, config)
// 4. 自定义生成器
type MyType int
func (MyType) Generate(rand *rand.Rand, size int) reflect.Value {
// 生成逻辑
}
// 5. 结构体测试
type Point struct {
X, Y int
}
f := func(p Point) bool {
// 测试属性
}
quick.Check(f, nil)
支持的类型
| 类型类别 | 示例 |
|---|---|
| 布尔 | bool |
| 整数 | int, int8, int16, int32, int64 |
| 无符号 | uint, uint8, uint16, uint32, uint64, uintptr |
| 浮点 | float32, float64 |
| 复数 | complex64, complex128 |
| 字符串 | string |
| 切片 | []T |
| Map | map[K]V |
| 数组 | [N]T |
| 指针 | *T |
| 结构体 | struct{…} |
| 通道 | chan T |
| 函数 | func(…) |
总结
testing/quick 包是 Go 标准库中用于属性测试的工具包。
核心优势:
- ✅ 自动生成测试数据
- ✅ 发现边界条件和意外输入
- ✅ 支持自定义生成器
- ✅ 比较函数等价性
- ✅ 可配置测试参数
重要限制:
- ⚠️ 包已冻结,不接受新功能
- ⚠️ 结构体字段必须全部导出
- ⚠️ 测试失败可能难以重现
主要用途:
- 属性测试(Property-based Testing)
- 函数等价性验证
- 边界条件发现
- 随机数据生成
- 黑盒测试
使用建议:
- 定义清晰、明确的属性
- 使用自定义生成器控制输入范围
- 限制测试范围避免溢出
- 使用 CheckEqual 比较不同实现
- 配置固定种子重现问题
典型用法:
// 属性测试
f := func(x int) bool {
return property(x)
}
if err := quick.Check(f, nil); err != nil {
t.Error(err)
}
// 等价性测试
quick.CheckEqual(impl1, impl2, nil)
// 自定义配置
config := &quick.Config{
MaxCount: 1000,
Rand: rand.New(rand.NewSource(42)),
}
替代方案: