当A的方法在Go中返回B时模拟对象A和B [英] Mocking objects A and B when A's method returns B in Go

查看:44
本文介绍了当A的方法在Go中返回B时模拟对象A和B的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

 //当前主代码var pool LibraryPool//全局,在main()中实例化func someServiceFunction(w http.ResponseWriter,r * http.Request){//读取请求//...conn:= pool.GetConnection()conn.Do(某些命令")//写响应//...}func main(){pool:= makePool()//构建并返回一个LibraryPool//设置将服务功能用作处理程序的端点//...} 

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

 //暂定新的主代码输入poolInterface接口{GetConnection()conn接口}输入connInterface接口{执行(命令字符串)}var pool poolInterfacefunc someServiceFunction(w http.ResponseWriter,r * http.Request){//读取请求//...conn:= pool.GetConnection()conn.Do(某些命令")//写响应//...}func main(){pool:= makePool()//仍建立一个LibraryPool} 

在测试中,我将使用这些接口的模拟实现 MockPool MockConnection ,并且全局 pool 变量将使用 MockPool .我将在 setup()函数中的 TestMain()函数.

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

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

解决方案

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

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

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

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

 包装人员输入Person struct {名称字符串}func New(名称字符串)Person {返回人{名称:名称,{} 

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

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

 包sql类型DBAndler接口{exec(string,... interface {})(int64,错误)}输入personRepo struct {dbHandler DBHandler}func NewPersonRepo(dbHandler DBHandler)& personRepo {return& personRepo {dbHandler:dbHandler,}}func(p * personRepo)InsertPerson(p person.Person)(int64,error){返回p.dbHandler.Exec(插入人的命令",p)} 

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

 包装服务键入PersonRepo接口{InsertPerson(person.Person)错误}输入服务结构{回购PersonRepo}func New(repo PersonRepo)*服务{退货和服务{回购:回购}}func(s * service)AddPerson(name string)(int64,error){人:=人.新(姓名)返回s.repo.InsertPerson(person)} 

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

 程序包http键入服务接口{AddPerson(名称字符串)(int64,错误)}类型处理程序struct {服务内容}func NewHandler(s Service)*处理程序{退货和处理人{服务,}}func(h * handler)HandleHTTP(w http.ResponseWriter,r * http.Request){//读取请求//解码名称id,err:= h.service.AddPerson(name)//写响应//...} 

在main.go中,您将把所有东西绑在一起:

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

软件包主要

  func main(){池:= makePool()conn:= pool.GetConnection()//回购personRepo:= sql.NewPersonRepo(conn)//服务personService:= service.New(personRepo)//处理程序personH​​andler:= http.NewPersonH​​andler(personService)//完成其余的工作,通过传递此处理程序来初始化http引擎/路由器.} 

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

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

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

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

I'm trying to implement unit tests in Go for an existing service which uses a connection pool struct and a connection struct from an existing library (call these LibraryPool and LibraryConnection) to connect to an external service. To use these, the service functions in the main code uses a unique, global instance of the pool, which has a GetConnection() method, like this:

// 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
  // ...
}

I'd like to unit-test these service functions without connecting to the external service, and so I'd like to mock the LibraryPool and LibraryConnection. To allow for this, I was thinking of changing the main code to something like this:

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

In the tests, I would use mock implementations MockPool and MockConnection of these interfaces, and the global pool variable would be instantiated using MockPool. I would instantiate this global pool in a setup() function, inside of a TestMain() function.

The problem is that in the new main code, LibraryPool does not properly implement poolInterface, because GetConnection() returns a connInterface instead of a LibraryConnection (even though LibraryConnection is a valid implementation of connInterface).

What would be a good way to approach this kind of testing? The main code is flexible too, by the way.

解决方案

Well, I'll try to answer by completely explain how I see this design. Sorry in advance if this is too much and not to the point..

  • Entity / Domain
    • The core of the app, will include the entity struct, won't import ANY outer layer package, but can be imported by every package (almost)
  • Application / Use case
    • The "service". Will be responsible mainly for the app logic, won't know about the transport(http), will "talk" with the DB through interface. Here you can have the domain validation, for example if resource is not found, or text is too short. Anything related to business logic.
  • transport
    • Will handle the http request, decode the request, get the service to do his stuff, and encode the response. Here you can return 401 if there is a missing required param in the request, or the user is not authorized, or something...
  • infrastructure
    • DB connection
    • Maybe some http engine and router and stuff.
    • Totally app-agnostic, don't import any inner package, not even Pseron

For example, let's say we want to do something as simple as insert person to the db.

package person will only include the person struct

package person

type Person struct{
  name string
}

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

About the db, let's say you use sql, I recommend to make a package named sql to handle the repo. (if you use postgress, use 'postgress package...).

The personRepo will get the dbConnection which will be initialized in main and implement DBAndler. only the connection will "talk" with the db directly, the repository main goal is to be gateway to the db, and speak in application-terms. (the connection is app-agnostic)

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

The service will get this repository as a dependancy (as interface) in the initailzer, and will interact with it to accomplish the business logic

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

Your transport handler will be initialized with the service as a dependancy, and he will handle the http request.

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
  // ... 
}

And in main.go you will tie everything together:

  1. Initialize db connection
  2. Initialize personRepo with this connection
  3. Initialize service with the repo
  4. Initialize the transport with the service

package main

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.

}

Note that every package struct was initialized with an interface but returned a struct, and also the interfaces were declared in the package which used them, not in the package which implemented them.

This makes it easy to unit test these package. for example, if you want to test the service, you don't need to worry about the http request, just use some 'mock' struct that implements the interface that the service depend on (PersonRepo), and you good to go..

Well, I hope it helped you even a little bit, it may seem confusing at first, but in time you will see how this seems like a large piece of code, but it helps when you need to add functionality or switching the db driver and such.. I recommend you to read about domain driven design in go, and also hexagonal arch.

edit:

In addition, this way you pass the connection to the service, the service doesn't import and use the global DB pool. Honestly, I don't know why it is so common, I guess it has its advantages and it is better to some application, but generally I think that letting your service depend on some interface, without actually know what is going on, is much a better practice.

这篇关于当A的方法在Go中返回B时模拟对象A和B的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
相关文章
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆