【问题标题】:How to test function that creates and injects a dependency如何测试创建和注入依赖项的函数
【发布时间】:2019-02-13 19:43:30
【问题描述】:

我的问题是你如何决定在哪里注入依赖,以及如何测试第一次将依赖注入到函数中的函数?

例如,我正在重构一些 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
}

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

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

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

【问题讨论】:

  • 一个选项是:play.golang.org/p/_Ow3rM7IH_f
  • 我想知道您使用什么框架进行依赖注入?我们 Oracle 正在创建一个名为 Dargo (github.com/jwells131313/dargo) 的软件。它具有一些专门用于单元测试(Rank)的功能。如果您能看一下,我们将不胜感激,谢谢!

标签: unit-testing go dependency-injection


【解决方案1】:

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

在 Go 中执行此操作的规范方法是使用 New 函数,例如 object_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,可能会有所帮助。

【讨论】:

  • 假设我使用你的 NewFoo 函数在 main 或其他一些函数中返回一个 Foo 结构。即使我要使用您的 NewFoo() 函数,我将如何测试我从中调用 NewFoo 的函数(如果我想在 NewFoo 内部的一个结构上测试某些方法)?
  • 你可以使用它自己的 New 对每个结构进行单元测试。您可以通过模拟出任何子部分并保留其余部分来一起测试多个。如果您确实需要测试静态函数(如 main),您可以将新函数设为变量 NewFoo = func() *Foo { ... },然后您可以覆盖该变量以进行测试。当心 - 我只将此策略用作最后的手段。
【解决方案2】:

如果您使用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 进行单元测试的基础知识!希望对你有帮助

【讨论】:

    猜你喜欢
    • 2013-12-25
    • 2018-01-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-09-19
    • 1970-01-01
    相关资源
    最近更新 更多