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..
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:
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.