Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 表示失败

测试过程

  1. 为函数参数生成随机值
  2. 调用函数 f
  3. 如果 f 返回 false,返回 *CheckError
  4. 重复直到达到 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 必须有相同的签名
  • 返回值必须可以比较

测试过程

  1. 为函数参数生成随机值
  2. 同时调用 f 和 g
  3. 比较返回值
  4. 如果不同,返回 *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)
}

注意事项

限制

  1. 包已冻结

    • 不再接受新功能
    • 考虑使用第三方属性测试库
  2. 结构体要求

    • 所有字段必须导出
    • 否则无法生成随机值
  3. 性能考虑

    • 默认运行 100 次测试
    • 复杂测试可能较慢
  4. 随机性

    • 测试失败可能难以重现
    • 使用固定种子重现问题

使用建议

  1. 属性选择

    • 选择明确的数学属性
    • 避免过于复杂的属性
  2. 测试范围

    • 限制输入范围避免溢出
    • 对大输入使用较小的 MaxCount
  3. 错误处理

    • 检查 Check 返回的错误
    • 使用 CheckError 获取失败输入
  4. 可重现性

    • 使用固定随机种子
    • 记录失败时的输入

快速参考

函数速查表

函数功能返回值
Check(f, config)查找使 f 返回 false 的输入*CheckError
CheckEqual(f, g, config)查找使 f 和 g 返回不同结果的输入*CheckEqualError
Value(t, rand)生成类型 t 的随机值reflect.Value

类型速查表

类型功能
Config测试配置选项
CheckErrorCheck 发现的错误
CheckEqualErrorCheckEqual 发现的错误
Generator自定义生成器接口
SetupError设置错误

Config 字段

字段默认值说明
MaxCount100 或 8最大迭代次数
MaxCountScale0比例因子
Randnil随机数源
Valuesnil自定义值生成函数

常见模式

// 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
Mapmap[K]V
数组[N]T
指针*T
结构体struct{…}
通道chan T
函数func(…)

总结

testing/quick 包是 Go 标准库中用于属性测试的工具包。

核心优势

  • ✅ 自动生成测试数据
  • ✅ 发现边界条件和意外输入
  • ✅ 支持自定义生成器
  • ✅ 比较函数等价性
  • ✅ 可配置测试参数

重要限制

  • ⚠️ 包已冻结,不接受新功能
  • ⚠️ 结构体字段必须全部导出
  • ⚠️ 测试失败可能难以重现

主要用途

  • 属性测试(Property-based Testing)
  • 函数等价性验证
  • 边界条件发现
  • 随机数据生成
  • 黑盒测试

使用建议

  1. 定义清晰、明确的属性
  2. 使用自定义生成器控制输入范围
  3. 限制测试范围避免溢出
  4. 使用 CheckEqual 比较不同实现
  5. 配置固定种子重现问题

典型用法

// 属性测试
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)),
}

替代方案

  • gopter - 功能更丰富的属性测试库
  • rapid - 现代属性测试库