golang实现故障注入测试的插件库failpoint的使用
Golang实现故障注入测试的插件库failpoint的使用
failpoint是一个用于Golang的故障注入测试库,它允许你在代码中添加可控的错误注入点。以下是使用failpoint的完整指南和示例。
快速开始(使用failpoint-ctl)
- 构建failpoint-ctl工具
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-ctl
- 在程序中注入故障点
package main
import "github.com/pingcap/failpoint"
func main() {
failpoint.Inject("testPanic", func() {
panic("failpoint triggerd")
})
}
-
使用failpoint-ctl enable转换代码
-
使用go build构建程序
-
通过环境变量启用故障点
GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
注意:GO_FAILPOINTS
不适用于InjectCall
类型的标记。
- 如果使用go run运行测试,不要忘记添加生成的binding__failpoint_binding__.go
GO_FAILPOINTS="main/testPanic=return(true)" go run your-program.go binding__failpoint_binding__.go
快速开始(使用failpoint-toolexec)
- 构建failpoint-toolexec工具
git clone https://github.com/pingcap/failpoint.git
cd failpoint
make
ls bin/failpoint-toolexec
- 在程序中注入故障点
package main
import "github.com/pingcap/failpoint"
func main() {
failpoint.Inject("testPanic", func() {
panic("failpoint triggerd")
})
}
- 使用单独的构建缓存并构建
GOCACHE=/tmp/failpoint-cache go build -toolexec path/to/failpoint-toolexec
- 通过环境变量启用故障点
GO_FAILPOINTS="main/testPanic=return(true)" ./your-program
- 也可以使用go run或go test
GOCACHE=/tmp/failpoint-cache GO_FAILPOINTS="main/testPanic=return(true)" go run -toolexec path/to/failpoint-toolexec your-program.go
设计原则
- 在有效的Golang代码中定义故障点,而不是注释或其他方式
- 故障点不会带来额外开销
- 故障点代码不会出现在最终二进制文件中
- 故障点例程是可读写的,并且应该由编译器检查
- 支持带有context.Context的并行测试
关键概念
故障点
故障点是一段代码片段,只有在相应的故障点激活时才会执行。
var outerVar = "declare in outer scope"
failpoint.Inject("failpoint-name-for-demo", func(val failpoint.Value) {
fmt.Println("unit-test", val, outerVar)
})
标记函数
- 它是一个空函数
- 用于提示重写器用相等性语句重写
- 引入编译器检查,如果故障点代码无效,在常规模式下无法编译
支持的标记函数列表
func Inject(fpname string, fpblock func(val Value)) {}
func InjectContext(fpname string, ctx context.Context, fpblock func(val Value)) {}
func InjectCall(fpname string, args ...any) {}
func Break(label ...string) {}
func Goto(label string) {}
func Continue(label ...string) {}
func Fallthrough() {}
func Return(results ...interface{}) {}
func Label(label string) {}
支持的故障点环境变量
故障点可以通过导出以下模式的环境变量启用:
[<percent>%][<count>*]<type>[(args...)][-><more terms>]
参数指定要采取的操作,可以是以下之一:
- off: 不采取任何操作(不触发故障点代码)
- return: 使用指定参数触发故障点
- sleep: 休眠指定的毫秒数
- panic: 引发panic
- break: 执行gdb并进入调试器
- print: 打印注入变量的故障点路径
- pause: 暂停直到故障点被禁用
如何向程序中注入故障点
基本用法
failpoint.Inject("failpoint-name", func(val failpoint.Value) {
failpoint.Return("unit-test", val)
})
转换后的代码:
if val, _err_ := failpoint.Eval(_curpkg_("failpoint-name")); _err_ == nil {
return "unit-test", val
}
忽略返回值
failpoint.Inject("failpoint-name", func(_ failpoint.Value) {
fmt.Println("unit-test")
})
或
failpoint.Inject("failpoint-name", func() {
fmt.Println("unit-test")
})
使用context.Context
failpoint.InjectContext(ctx, "failpoint-name", func(val failpoint.Value) {
fmt.Println("unit-test", val)
})
转换后的代码:
if val, _err_ := failpoint.EvalContext(ctx, _curpkg_("failpoint-name")); _err_ == nil {
fmt.Println("unit-test", val)
}
控制故障点
func (s *dmlSuite) TestCRUDParallel() {
sctx := failpoint.WithHook(context.Backgroud(), func(ctx context.Context, fpname string) bool {
return ctx.Value(fpname) != nil // 通过ctx key确定
})
insertFailpoints = map[string]struct{} {
"insert-record-fp": {},
"insert-index-fp": {},
"on-duplicate-fp": {},
}
ictx := failpoint.WithHook(context.Backgroud(), func(ctx context.Context, fpname string) bool {
_, found := insertFailpoints[fpname] // 只启用某些故障点
return found
})
deleteFailpoints = map[string]struct{} {
"tikv-is-busy-fp": {},
"fetch-tso-timeout": {},
}
dctx := failpoint.WithHook(context.Backgroud(), func(ctx context.Context, fpname string) bool {
_, found := deleteFailpoints[fpname] // 只禁用某些故障点
return !found
})
// 其他DML并行测试用例
s.RunParallel(buildSelectTests(sctx))
s.RunParallel(buildInsertTests(ictx))
s.RunParallel(buildDeleteTests(dctx))
}
循环中的故障点
failpoint.Label("outer")
for i := 0; i < 100; i++ {
inner:
for j := 0; j < 1000; j++ {
switch rand.Intn(j) + i {
case j / 5:
failpoint.Break()
case j / 7:
failpoint.Continue("outer")
case j / 9:
failpoint.Fallthrough()
case j / 10:
failpoint.Goto("outer")
default:
failpoint.Inject("failpoint-name", func(val failpoint.Value) {
fmt.Println("unit-test", val.(int))
if val == j/11 {
failpoint.Break("inner")
} else {
failpoint.Goto("outer")
}
})
}
}
}
故障点命名最佳实践
所有故障点在同一个包中共享相同的命名空间,因此需要小心避免名称冲突。以下是一些推荐的命名规则:
- 在当前子包中保持名称唯一
- 为故障点使用自解释的名称
可以通过环境变量启用故障点:
GO_FAILPOINTS="github.com/pingcap/tidb/ddl/renameTableErr=return(100);github.com/pingcap/tidb/planner/core/illegalPushDown=return(true);github.com/pingcap/pd/server/schedulers/balanceLeaderFailed=return(true)"
实现细节
- 定义一组标记函数
- 解析导入并修剪不导入故障点的源文件
- 遍历AST查找标记函数调用
- 标记函数调用将被重写为IF语句,该语句调用
failpoint.Eval
来确定故障点是否处于活动状态,并在故障点启用时执行故障点代码
示例Demo
以下是一个完整的故障点使用示例:
package main
import (
"fmt"
"github.com/pingcap/failpoint"
)
func main() {
// 简单的故障点注入
failpoint.Inject("simple-failpoint", func() {
fmt.Println("Simple failpoint triggered")
})
// 带返回值的故障点
failpoint.Inject("return-failpoint", func(val failpoint.Value) {
fmt.Printf("Return failpoint triggered with value: %v\n", val)
})
// 在循环中使用故障点
failpoint.Label("outer-loop")
for i := 0; i < 5; i++ {
failpoint.Inject("loop-failpoint", func(val failpoint.Value) {
fmt.Printf("Loop failpoint triggered at iteration %d with value: %v\n", i, val)
if i == 3 {
failpoint.Break("outer-loop")
}
})
}
// 使用context控制故障点
ctx := failpoint.WithHook(context.Background(), func(ctx context.Context, fpname string) bool {
return fpname == "context-aware-failpoint"
})
failpoint.InjectContext(ctx, "context-aware-failpoint", func() {
fmt.Println("Context-aware failpoint triggered")
})
fmt.Println("Program completed successfully")
}
要启用这些故障点,可以设置环境变量:
GO_FAILPOINTS="main/simple-failpoint=return;main/return-failpoint=return(42);main/loop-failpoint=50%return(loop-value)" ./your-program
这个示例展示了:
- 基本故障点注入
- 带返回值的故障点
- 循环中的故障点控制
- 使用context控制故障点激活
通过这种方式,你可以在测试中模拟各种错误条件,验证程序的健壮性和错误处理能力。
更多关于golang实现故障注入测试的插件库failpoint的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html
更多关于golang实现故障注入测试的插件库failpoint的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html
Golang故障注入测试:failpoint库使用指南
failpoint是Golang中一个强大的故障注入测试库,允许开发者在代码中注入各种故障场景(如延迟、错误、panic等)来测试系统的容错能力。下面详细介绍其使用方法。
安装failpoint
go get github.com/pingcap/failpoint
基本使用
1. 定义failpoint
在代码中插入failpoint标记:
import "github.com/pingcap/failpoint"
func DoSomething() error {
// 注入一个错误failpoint
failpoint.Inject("failpoint-name", func() error {
return errors.New("injected error")
})
// 正常业务逻辑
return nil
}
2. 启用failpoint
在测试中启用failpoint:
func TestDoSomething(t *testing.T) {
// 启用failpoint
defer failpoint.Enable("failpoint-name", "return(true)")()
err := DoSomething()
if err == nil {
t.Error("Expected error but got nil")
}
}
常用注入类型
1. 返回错误
failpoint.Inject("db-error", func() error {
return fmt.Errorf("mock db error")
})
2. 延迟执行
failpoint.Inject("slow-request", func() error {
time.Sleep(5 * time.Second)
return nil
})
3. 随机行为
failpoint.Inject("random-error", func() error {
if rand.Intn(100) < 30 { // 30%概率返回错误
return errors.New("random error")
}
return nil
})
4. Panic注入
failpoint.Inject("panic-memory", func() error {
panic("out of memory")
})
高级用法
1. 条件触发
failpoint.Inject("error-when-x-gt-5", func(val int) error {
if val > 5 {
return errors.New("value too large")
}
return nil
})(x)
2. 动态修改行为
func TestDynamicFailpoint(t *testing.T) {
// 通过环境变量控制failpoint
os.Setenv("GO_FAILPOINTS", "failpoint-name=return(true)")
defer os.Unsetenv("GO_FAILPOINTS")
defer failpoint.Enable("failpoint-name", "return(true)")()
// 测试逻辑...
}
3. 链式failpoint
failpoint.Inject("fp1", func() error {
return failpoint.Inject("fp2", func() error {
return errors.New("chained error")
})
})
完整示例
package main
import (
"errors"
"testing"
"time"
"github.com/pingcap/failpoint"
)
func ProcessRequest() error {
// 模拟数据库操作
failpoint.Inject("db-timeout", func() error {
time.Sleep(2 * time.Second)
return errors.New("db timeout")
})
// 模拟业务逻辑
failpoint.Inject("business-error", func() error {
return errors.New("invalid parameter")
})
// 正常流程
return nil
}
func TestProcessRequest(t *testing.T) {
// 测试超时场景
t.Run("db timeout", func(t *testing.T) {
defer failpoint.Enable("db-timeout", "return(true)")()
err := ProcessRequest()
if err == nil || err.Error() != "db timeout" {
t.Errorf("Expected db timeout error, got %v", err)
}
})
// 测试业务错误场景
t.Run("business error", func(t *testing.T) {
defer failpoint.Enable("business-error", "return(true)")()
err := ProcessRequest()
if err == nil || err.Error() != "invalid parameter" {
t.Errorf("Expected business error, got %v", err)
}
})
// 测试正常流程
t.Run("normal flow", func(t *testing.T) {
err := ProcessRequest()
if err != nil {
t.Errorf("Expected nil error, got %v", err)
}
})
}
最佳实践
- 命名规范:为failpoint使用有意义的名称,如
service-timeout
、db-error
等 - 清理机制:确保测试后禁用所有failpoint,避免影响其他测试
- 文档记录:为每个failpoint添加注释说明其用途和触发条件
- 谨慎生产:确保生产代码中不会意外启用failpoint
- 组合测试:结合多个failpoint测试复杂故障场景
通过failpoint,您可以系统性地验证应用程序在各种故障条件下的行为,提高系统的健壮性和可靠性。