依赖注入和测试

huangapple go评论113阅读模式
英文:

Dependency Injection & Testing

问题

我正在开发一个小型的Go应用程序,基本上是各种密码存储(Ansible Vault、Hashicorp Vault、Chef Vault等)的包装器。我的想法是:在我的各种配置脚本中,我可以使用我的Go包装器来获取密码,如果我们决定在后台切换密码存储,所有接口都不需要在我的项目中进行更新。

我正在尝试为这个应用程序设置适当的测试,并在此过程中,我试图找出最佳的方法来注入我的依赖关系。

例如,假设项目名为secrets。其中一个实现是ansible。Ansible实现需要自己的parser并且需要打开自己的connection到ansible vault来检索数据。

所以我可能有以下代码:

package secrets

type PasswordStore interface {
    GetKey(key string) (string, error)
}

func New(backend string, config map[string]interface{}) (PasswordStore, error) {
    switch backend {
    case "ansible":
        return ansible.New(config)
    default:
        return nil, fmt.Errorf("Password store '%s' not supported.", backend)
    }
}


package ansible


type Connection interface {
    open() (string, error)
}

type Ansible struct {
    connection Connection
    contents   map[string]string
}

func New(c map[string]interface{}) (*Ansible, error) {
    conn, err := NewConnection(c["ansible_path"].(string))
    if err != nil {
        return nil, err
    }

    // 打开连接,解析等...

    a := &Ansible{
        connection: conn,
        contents:   parsedData,
    }

    return a, nil
}

这样看起来很好,因为secrets包不需要了解ansible包的依赖关系(connection),工厂只需使用一些配置数据创建新的实例。然而,如果我需要模拟Ansible接收到的connection,似乎没有很好的方法可以做到这一点(除非该配置映射有一个名为mock的连接选项)。

另一种选择是放弃工厂,只需从secrets包中组装所有依赖项,例如:

package secrets

type PasswordStore interface {
    GetKey(key string) (string, error)
}

func New(backend string, config map[string]interface{}) (PasswordStore, error) {
    switch backend {
    case "ansible":
        return ansible.New(AnsibleConnection{}, config)
    default:
        return nil, fmt.Errorf("Password store '%s' not supported.", backend)
    }
}

package ansible


// 在此文件中与之前相同,但注入了依赖项...

func New(connect Connection, c map[string]interface{}) (*Ansible, error) {
    conn, err := connect.NewConnection(c["ansible_path"].(string))
    if err != nil {
        return nil, err
    }

    // 打开连接,解析等...

    a := &Ansible{
        connection: conn,
        contents:   parsedData,
    }

    return a, nil
}

现在依赖项被注入了,但似乎secrets需要了解每个实现的每个依赖项。

是否有一种更合理的方式来构建这个结构,使得secrets知道得更少?或者顶层包是否通常负责协调一切?

英文:

I'm working on a small Go application that's basically a wrapper for various password stores (Ansible Vault, Hashicorp Vault, Chef Vault, etc). The idea is: In my various provisioning scripts, I can use my Go wrapper to grab secrets and if we decide to switch password stores behind the scenes, all of the interfaces don't need to be updated across my projects.

I'm trying to setup proper tests for this application, and in doing so, am trying to figure out the best way to to inject my dependencies.

For example, lets say the project is called secrets. And one of my implementations is ansible. And the ansible implementation needs its own parser and needs to open its own connection to the ansible vault, to retrieve the data.

So I might have the following:

package secrets

type PasswordStore interface {
	GetKey(key string) (string, error)
}

func New(backend string, config map[string]interface{}) (PasswordStore, error) {
	switch backend {
	case "ansible":
		return ansible.New(config)
	default:
		return nil, fmt.Errorf("Password store '%s' not supported.", backend)
	}
}


package ansible


type Connection interface {
	open() (string, error)
}

type Ansible struct {
	connection Connection
	contents   map[string]string
}

func New(c map[string]interface{}) (*Ansible, error) {
	conn, err := NewConnection(c["ansible_path"].(string))
	if err != nil {
		return nil, err
	}

    // open connection, parse, etc...	

	a := &Ansible{
		connection: conn,
		contents:   parsedData,
	}

	return a, nil
}

So this seems nice because the secrets package doesn't need knowledge of the ansible package dependencies (connection), and the factory just new's up the instance with some config data. However, if I need to mock the connection that Ansible receives, there doesn't seem to be a good way to do this (unless that config map had a connection option called mock)

The other option is to abandon the factory, and just assemble all the dependencies from the secrets package, like:

package secrets

type PasswordStore interface {
    GetKey(key string) (string, error)
}

func New(backend string, config map[string]interface{}) (PasswordStore, error) {
    switch backend {
    case "ansible":
        return ansible.New(AnsibleConnection{}, config)
    default:
        return nil, fmt.Errorf("Password store '%s' not supported.", backend)
    }
}

package ansible


// same as before in this file, but with injected dependency ...

func New(connect Connection, c map[string]interface{}) (*Ansible, error) {
    conn, err := connect.NewConnection(c["ansible_path"].(string))
    if err != nil {
        return nil, err
    }

    // open connection, parse, etc...   

    a := &Ansible{
        connection: conn,
        contents:   parsedData,
    }

    return a, nil
}

Now the dependency is injected, but it seems like secrets needs to have knowledge of every dependency for every implementation.

Is there a more logical way to structure this so that secrets knows less? Or is it typical for the top level package to be orchestrating everything?

答案1

得分: 3

决定后端是什么的是什么?这应该能帮助你指导。我在一个项目中做过类似的事情,支持多个数据库,我所做的基本上是:

  • config 包读取配置文件,确定使用的后端是什么
  • store 包提供通用接口,并有一个函数接受配置,并返回一个实现
  • server 包只引用接口
  • main 包读取配置,将其传递给 store 中的工厂函数,然后在创建服务器时将结果注入到服务器中

所以当我创建我的服务器(实际上使用数据存储)时,我将配置传递给 store 中的工厂函数,该函数返回一个接口,然后将其注入到服务器中。唯一需要了解不同具体实现的是暴露接口和工厂的同一个包;serverconfigmain 包将其视为黑盒。

英文:

What decides what the backend is? That should help guide you. I've done something similar with support for multiple databases on a project, and what I did was basically:

  • config package reads in config file, which determines what backend is being used
  • store package offers the generic interface and has a function that takes a config, and returns an implementation
  • server package references only the interface
  • main package reads the config, passes it to the factory function in store, then injects the result into the server on creation

So when I create my server (which actually uses the data store), I pass the config to the factory function in store, which returns an interface, and then inject that into the server. The only thing that has to know about the different concrete implementations is the same package that exposes the interface and factory; the server, config, and main packages see it as a black box.

huangapple
  • 本文由 发表于 2017年8月19日 04:05:34
  • 转载请务必保留本文链接:https://go.coder-hub.com/45764315.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定