如何测试创建和注入依赖项的功能

时间:2018-09-08 22:01:43

标签: unit-testing go dependency-injection

我的问题是如何确定在哪里注入依赖项,以及如何测试将依赖项首先注入到函数中的函数?

例如,我正在重构一些Go代码以使用依赖注入,目的是使代码更具可测试性。

这是我的重构代码:

type FooIface interface {
  FooFunc()
}

type Foo struct {}

func (f *Foo) FooFunc() {
  // Some function I would like to stub
}

func main() {
  OuterFunction()
}

func OuterFunction() {
  fooVar := &Foo{}
  InnerFunction(fooVar)
  // Other stuff
}

func InnerFunction(f *FooIface) {
  f.FooFunc()
  // Other stuff
}

我可以通过创建一个模拟的结构轻松地测试InnerFunction,该结构使用存根的FooFunc()实现FooIface,因此在这里一切都很好。 但是,如果我想测试OuterFunction()怎么办?那么既然注入的依赖项是在OuterFunction()中创建的,那我该如何从那里存根FooFunc()?

我是否必须通过在main中创建&Foo {}结构的创建,使其从OuterFunction()中移出一层,然后再将其注入到OuterFunction()中?

换句话说,您将如何测试创建和注入依赖项的功能

2 个答案:

答案 0 :(得分:0)

通常当人们谈论依赖注入时,它是在创建对象的上下文中(或在Go的案例结构中)。

在Go中执行此操作的规范方法是使用Newobject_name.New(...)之类的package_name.NewObjectName(...)函数。这些函数接受对象的依赖关系并输出对象的实例。

上面您在静态函数中执行了很多代码。可以将其转换为已创建并具有方法的对象吗?有时称为控制反转。

type Foo {
    bar Bar
    baz Baz
}

func NewFoo(bar Bar, baz Baz) *Foo {
    return &Foo{ bar: bar, baz: baz }
}

func (foo *Foo) X() {
    foo.bar.Y()
    foo.baz.Z()
}

此模型可以扩展到多个级别,因此测试更加容易。

foo := NewFoo(
    NewBar(...),
    NewBaz(...),
)

这是一个post,可能会有所帮助。

答案 1 :(得分:0)

如果您使用的是Dargo注入框架,则可以绑定具有更高等级的接口或结构版本,然后将这些等级用于您的代码中,而不是普通代码所绑定的内容。

假设您在常规代码中定义了一些服务,如下所示:

var globalLocator ioc.ServiceLocator

type AnExpensiveService interface {
    DoExpensiveThing(string) (string, error)
}

type NormalExpensiveServiceData struct {
}

func (nesd *NormalExpensiveServiceData) DoExpensiveThing(thingToDo string) (string, error) {
    time.Sleep(5 * time.Second)

    return "Normal", nil
}

type SomeOtherServiceData struct {
    ExpensiveService AnExpensiveService `inject:"AnExpensiveService"`
}

func init() {
    myLocator, err := ioc.CreateAndBind("TestingExampleLocator", func(binder ioc.Binder) error {
        binder.Bind("UserService", SomeOtherServiceData{})
        binder.Bind("AnExpensiveService", NormalExpensiveServiceData{})

        return nil
    })
    if err != nil {
        panic(err)
    }

    globalLocator = myLocator
}

func DoSomeUserCode() (string, error) {
    raw, err := globalLocator.GetDService("UserService")
    if err != nil {
        return "", err
    }

    userService, ok := raw.(*SomeOtherServiceData)
    if !ok {
        return "", fmt.Errorf("Unkonwn type")
    }

    return userService.ExpensiveService.DoExpensiveThing("foo")

}

现在,您不想在测试代码中调用昂贵的服务。在下面的测试代码中,昂贵的服务由具有较高排名的模拟服务代替。当测试调用用户代码时,将使用模拟代替常规的昂贵代码。这是测试代码:

type MockExpensiveService struct {
}

func (mock *MockExpensiveService) DoExpensiveThing(thingToDo string) (string, error) {
    return "Mock", nil
}

func putMocksIn() error {
    return ioc.BindIntoLocator(globalLocator, func(binder ioc.Binder) error {
        binder.Bind("AnExpensiveService", MockExpensiveService{}).Ranked(1)

        return nil
    })
}

func TestWithAMock(t *testing.T) {
    err := putMocksIn()
    if err != nil {
        t.Error(err.Error())
        return
    }

    result, err := DoSomeUserCode()
    if err != nil {
        t.Error(err.Error())
        return
    }

    if result != "Mock" {
        t.Errorf("Was expecting mock service but got %s", result)
        return
    }
}

调用DoUserCode时,将查找UserService,而不是获取常规实现,而是将其与模拟一起注入。

此后,测试仅验证注入的是模拟程序,而不是正常的代码。

这是Dargo进行单元测试的基础!希望对您有帮助