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

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.