在Go中,模拟通常意味着实现具有测试版本的接口,该测试版本允许从测试中控制运行时行为。它也可以指模拟函数和方法,我们将探索如何实现它。示例中使用的Patch和Restore函数可以在https://play.golang.org/p/oLF1XnRX3C 找到。

包含大量分支条件或深度嵌套逻辑的代码可能很难测试,最后测试往往更加效果很差。这是因为开发人员需要在其测试中跟踪很多模拟对象,返回值和状态。

实践

建立 mock.go:

package mocking

// DoStuffer 是一个简单的接口
type DoStuffer interface {
    DoStuff(input string) error
}

建立 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)
    }
}

建立 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
}

建立 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
}

建立 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)
            }
        })
    }
}

运行go test:

PASS
ok github.com/agtorre/go-cookbook/chapter8/mocking 0.006s

说明

无论是使用errors.New,fmt.Errorf还是自定义错误,最重要的是不应该在代码中不处理错误。这些定义错误的不同方法提供了很大的灵活性。例如,你可以在结构中添加额外的函数,以进一步检查错误并将接口转换为调用函数中的错误类型,以获得一些额外的功能。

接口本身非常简单,唯一的要求是返回一个有效的字符串。(在测试中明显将其复杂化了)这样的测试保证对某些要求严格的应用程序同样可用。

最后编辑: kuteng  文档更新时间: 2021-01-03 15:03   作者:kuteng