我正在尝试在Go中为现有服务实现单元测试,该服务使用连接池结构和现有库中的连接结构(称为LibraryPool
和LibraryConnection
)来连接到外部服务。
要使用这些,主代码中的服务函数使用池的唯一全局实例,该实例具有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
}
在测试中,我将使用这些接口的模拟实现MockPool
和MockConnection
,并且全局pool
变量将使用MockPool
实例化。我将在TestMain()函数内部的pool
函数中实例化此全局setup()
。
问题在于,在新的主代码中,LibraryPool
无法正确实现poolInterface
,因为GetConnection()
返回了connInterface
而不是LibraryConnection
(即使LibraryConnection
是connInterface
的有效实现)。
进行这种测试的好方法是什么?顺便说一下,主要代码也很灵活。
答案 0 :(得分:1)
好吧,我将通过完全解释我如何看待这种设计来尝试回答。如果抱歉太多,请事先对不起。.
例如,假设我们想做一些简单的事情,例如将个人插入数据库。
打包人员将仅包含人员结构
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中,将所有内容捆绑在一起:
软件包主要
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中的域驱动设计,以及六角形拱门。
编辑:
此外,通过这种方式传递与服务的连接,该服务不会导入并使用全局数据库池。老实说,我不知道为什么它如此普遍,我想它有它的优点,并且对某些应用程序更好。但是总的来说,我认为让您的服务依赖某个接口,而实际上不知道发生了什么更好的做法。