## 使用标准库进行模拟 在Go中,模拟通常意味着实现具有测试版本的接口,该测试版本允许从测试中控制运行时行为。它也可以指模拟函数和方法,我们将探索如何实现它。示例中使用的Patch和Restore函数可以在https://play.golang.org/p/oLF1XnRX3C 找到。 包含大量分支条件或深度嵌套逻辑的代码可能很难测试,最后测试往往更加效果很差。这是因为开发人员需要在其测试中跟踪很多模拟对象,返回值和状态。 ### 实践 1. 建立 mock.go: ``` package mocking // DoStuffer 是一个简单的接口 type DoStuffer interface { DoStuff(input string) error } ``` 2. 建立 patch.go: ``` package mocking import "reflect" // Restorer是一个可用于恢复先前状态的函数 type Restorer func() // Restore存储了之前的状态 func (r Restorer) Restore() { r() } // Patch将给定目标指向的值设置为给定值,并返回一个函数以将其恢复为原始值。 该值必须可分配给目标的元素类型。 func Patch(dest, value interface{}) Restorer { destv := reflect.ValueOf(dest).Elem() oldv := reflect.New(destv.Type()).Elem() oldv.Set(destv) valuev := reflect.ValueOf(value) if !valuev.IsValid() { // 对于目标类型不可用的情况,这种解决方式并不优雅 valuev = reflect.Zero(destv.Type()) } destv.Set(valuev) return func() { destv.Set(oldv) } } ``` 3. 建立 exec.go: ``` package mocking import "errors" var ThrowError = func() error { return errors.New("always fails") } func DoSomeStuff(d DoStuffer) error { if err := d.DoStuff("test"); err != nil { return err } if err := ThrowError(); err != nil { return err } return nil } ``` 4. 建立 mock_test.go: ``` package mocking type MockDoStuffer struct { // 使用闭包模拟 MockDoStuff func(input string) error } func (m *MockDoStuffer) DoStuff(input string) error { if m.MockDoStuff != nil { return m.MockDoStuff(input) } // 如果我们不模拟输入,就返回一个常见的情况 return nil } ``` 5. 建立 exec_test.go: ``` package mocking import ( "errors" "testing" ) func TestThrowError(t *testing.T) { tests := []struct { name string wantErr bool }{ {"base-case", true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if err := ThrowError(); (err != nil) != tt.wantErr { t.Errorf("DoSomeStuff() error = %v, wantErr %v", err, tt.wantErr) } }) } } func TestDoSomeStuff(t *testing.T) { tests := []struct { name string DoStuff error ThrowError error wantErr bool }{ {"base-case", nil, nil, false}, {"DoStuff error", errors.New("failed"), nil, true}, {"ThrowError error", nil, errors.New("failed"), true}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // 使用模拟结构来模拟接口 d := MockDoStuffer{} d.MockDoStuff = func(string) error { return tt.DoStuff } // 模拟声明为变量的函数对func A()不起作用,必须是var A = func() defer Patch(&ThrowError, func() error { return tt.ThrowError }).Restore() if err := DoSomeStuff(&d); (err != nil) != tt.wantErr { t.Errorf("DoSomeStuff() error = %v, wantErr %v", err, tt.wantErr) } }) } } ``` 6. 运行go test: ``` PASS ok github.com/agtorre/go-cookbook/chapter8/mocking 0.006s ``` ### 说明 无论是使用errors.New,fmt.Errorf还是自定义错误,最重要的是不应该在代码中不处理错误。这些定义错误的不同方法提供了很大的灵活性。例如,你可以在结构中添加额外的函数,以进一步检查错误并将接口转换为调用函数中的错误类型,以获得一些额外的功能。 接口本身非常简单,唯一的要求是返回一个有效的字符串。(在测试中明显将其复杂化了)这样的测试保证对某些要求严格的应用程序同样可用。 * * * * 学识浅薄,错误在所难免。欢迎在群中就本书提出修改意见,以飨后来者,长风拜谢。 Golang中国(211938256) beego实战(258969317) Go实践(386056972)