doumi7861
doumi7861
2017-08-18 20:05

依赖注入与测试

已采纳

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条回答

  • dongwei1855 dongwei1855 4年前

    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.

    点赞 评论 复制链接分享