【问题标题】:Mocking objects A and B when A's method returns B in Go当 A 的方法在 Go 中返回 B 时模拟对象 A 和 B
【发布时间】:2019-05-15 02:47:50
【问题描述】:

我正在尝试在 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,并且将使用MockPool 实例化全局pool 变量。我将在setup() 函数中实例化这个全局pool,在TestMain() 函数内部。

问题是在新的主代码中,LibraryPool 没有正确实现poolInterface,因为GetConnection() 返回一个connInterface 而不是LibraryConnection(即使@ 987654339@ 是connInterface 的有效实现。

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

【问题讨论】:

  • 只需在 LibraryPool 周围创建一个瘦包装器,该包装器将实现 poolInterface 接口。包装器的 GetConnection 只是委托给包含的 LibraryPool。 play.golang.com/p/TmRrtR_QS_u

标签: unit-testing go


【解决方案1】:

好吧,我将尝试通过完全解释我如何看待这种设计来回答。如果这太过分并且不切题,请提前抱歉..

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

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

包 person 只会包含 person 结构

package person

type Person struct{
  name string
}

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

关于 db,假设你使用 sql,我建议制作一个名为 sql 的包来处理 repo。 (如果您使用 postgress,请使用 'postgress package...)。

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

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)
}

服务将在初始化程序中将此存储库作为依赖项(作为接口)获取,并将与其交互以完成业务逻辑

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. 使用 repo 初始化服务
  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 中的域驱动设计,以及六角拱形。

编辑:

此外,这样您传递到服务的连接,服务导入和使用全局数据库池。老实说,我不知道它为什么如此普遍,我想它有它的优点并且对某些应用程序更好,但通常我认为让你的服务依赖于某个接口,而不实际知道发生了什么,是很多的更好的做法。

【讨论】:

  • 如果某个库有一个连接池,它的存在通常是有充分理由的(通常是因为单个连接对于并发使用是不安全的)。此外,池通常会在出现错误后自动处理重新连接。放弃这样的池对我来说似乎不切实际。
  • @Peter 同意,池化很有用,一些 sql 驱动程序会为您完成,但其他数据库需要不同的东西,我同意。无论如何,我的意思是您仍然可以使用全局池,只需从实现 DBHandler 的包中调用它...
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-13
  • 2013-05-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多