当A的方法在Go中返回B时模拟对象A和B

时间:2018-12-13 20:15:14

标签: unit-testing go

我正在尝试在Go中为现有服务实现单元测试,该服务使用连接池结构和现有库中的连接结构(称为LibraryPoolLibraryConnection)来连接到外部服务。 要使用这些,主代码中的服务函数使用池的唯一全局实例,该实例具有GetConnection()方法,如下所示:

// Current Main Code
var pool LibraryPool // global, instantiated in main()

func someServiceFunction(w http.ResponseWriter, r *http.Request) {
  // read request
  // ...
  conn := pool.GetConnection()
  conn.Do("some command")
  // write response
  // ... 
}

func main() {
  pool := makePool() // builds and returns a LibraryPool
  // sets up endpoints that use the service functions as handlers
  // ...
}

我想在不连接外部服务的情况下对这些服务功能进行单元测试,因此我想模拟LibraryPool和LibraryConnection。为此,我正在考虑将主代码更改为以下内容:

// Tentative New Main Code
type poolInterface interface {
  GetConnection() connInterface
}

type connInterface interface {
  Do(command string)
}

var pool poolInterface

func someServiceFunction(w http.ResponseWriter, r *http.Request) {
  // read request
  // ...
  conn := pool.GetConnection()
  conn.Do("some command")
  // write response
  // ...
}

func main() {
  pool := makePool() // still builds a LibraryPool
}

在测试中,我将使用这些接口的模拟实现MockPoolMockConnection,并且全局pool变量将使用MockPool实例化。我将在TestMain()函数内部的pool函数中实例化此全局setup()

问题在于,在新的主代码中,LibraryPool无法正确实现poolInterface,因为GetConnection()返回了connInterface而不是LibraryConnection(即使LibraryConnectionconnInterface的有效实现)。

进行这种测试的好方法是什么?顺便说一下,主要代码也很灵活。

1 个答案:

答案 0 :(得分:1)

好吧,我将通过完全解释我如何看待这种设计来尝试回答。如果抱歉太多,请事先对不起。.

  • 实体/域
    • 应用程序的核心将包括实体struct,不会导入任何外层包,但可以(几乎)由每个包导入
  • 应用程序/用例
    • “服务”。将主要负责应用程序逻辑,不了解传输(http),将通过接口与DB“对话”。您可以在此处进行域验证,例如,如果找不到资源或文本太短。任何与业务逻辑有关的东西。
  • 运输
    • 将处理http请求,对请求进行解码,获取服务以完成工作,并对响应进行编码。如果请求中缺少必需的参数,或者用户未获得授权,或者有其他东西,您可以在此处返回401。
  • 基础设施
    • 数据库连接
    • 也许是一些http引擎和路由器之类的东西。
    • 完全与应用程序无关,不导入任何内部包,甚至不导入Pseron

例如,假设我们想做一些简单的事情,例如将个人插入数据库。

打包人员将仅包含人员结构

package person

type Person struct{
  name string
}

func New(name string) Person {
  return Person{
    name: name,
  {
}

关于数据库,假设您使用sql,我建议制作一个名为sql的程序包来处理存储库。 (如果您使用postgress,请使用'postgress包...)。

personRepo将获得dbConnection,它将在main中初始化并实现DBAndler。只有连接将直接与数据库“对话”,存储库的主要目标是成为数据库的网关,并使用应用程序术语进行对话。 (该连接与应用程序无关)

package sql

type DBAndler interface{
  exec(string, ...interface{}) (int64, error)
}

type personRepo struct{
  dbHandler DBHandler
}

func NewPersonRepo(dbHandler DBHandler) &personRepo {
  return &personRepo{
    dbHandler: dbHandler,
  }
}

func (p *personRepo) InsertPerson(p person.Person) (int64, error) {
  return p.dbHandler.Exec("command to insert person", p)
}

该服务将在intailtailzer中将该存储库作为依赖项(作为接口),并将与之交互以完成业务逻辑

package service

type PersonRepo interface{
  InsertPerson(person.Person) error
}

type service struct {
  repo PersonRepo
}

func New(repo PersonRepo) *service {
  return &service{
    repo: repo
  }
}

func (s *service) AddPerson(name string) (int64, error) {
  person := person.New(name)
  return s.repo.InsertPerson(person)
}

您的传输处理程序将使用该服务作为依赖项进行初始化,并且他将处理http请求。

package http

type Service interface{
  AddPerson(name string) (int64, error)
}

type handler struct{
  service Service
}

func NewHandler(s Service) *handler {
  return &handler{
    service: s,
  }
}

func (h *handler) HandleHTTP(w http.ResponseWriter, r *http.Request) {
  // read request
  // decode name

  id, err := h.service.AddPerson(name)

  // write response
  // ... 
}

然后在main.go中,将所有内容捆绑在一起:

  1. 初始化数据库连接
  2. 通过此连接初始化personRepo
  3. 通过存储库初始化服务
  4. 使用服务初始化运输

软件包主要

func main() {
  pool := makePool()
  conn := pool.GetConnection()

  // repo
  personRepo := sql.NewPersonRepo(conn)

  // service
  personService := service.New(personRepo)

  // handler
  personHandler := http.NewPersonHandler(personService)

  // Do the rest of the stuff, init the http engine/router by passing this handler.

}

请注意,每个包结构都用interface初始化,但返回了struct,并且接口是在使用它们的包中声明的,而不是在实现它们的包中声明的。

这使得对这些软件包进行单元测试变得容易。例如,如果您要测试服务,则无需担心http请求,只需使用一些实现了服务所依赖的接口(PersonRepo)的“模拟”结构,就可以了。

好吧,我希望它能对您有所帮助,乍一看似乎令人困惑,但是随着时间的推移,您会发​​现这看起来像是一段很大的代码,但是当您需要添加功能或切换功能时,它会有所帮助db driver等。我建议您阅读go中的域驱动设计,以及六角形拱门。

编辑:

此外,通过这种方式传递与服务的连接,该服务不会导入并使用全局数据库池。老实说,我不知道为什么它如此普遍,我想它有它的优点,并且对某些应用程序更好。但是总的来说,我认为让您的服务依赖某个接口,而实际上不知道发生了什么更好的做法。