I was studying golang these days and needed to write some tests.
I'm new at golang and i was not familiar with testing and stuffs like that so it was a little "hard" to me to get into it, this post is to make this easy for you as I wish it had been easy for me, i'll cover the basic principles to write tests with sql mock using the main golang structure (interfaces) to keep it simple.
I'm assuming that you're already familiar with golang
Golang interfaces
If you come from a language like java
, PHP
or any other OOP language you must be familiar with interfaces
, in short an interface is a contract
used to guarantee that a specific class implements determined method/attributes.
Go
has interfaces too and is very simple to write it:
type repository interface {}
I'll use as example the Error
interface that are built-in in go
, as we know errors
in go
are a type or like some people say a first class citizen.
we can return a error, create a new type of error, pass error as parameter, store a error in a variable and so on...checkout the error interface
as you can see the Error
interface only requires a single method called Error that returns a string, so if we do;
type MyCustomError struct {
message string
}
func (e MyCustomError) Error() string {
return e.message
}
we can use MyCustomError
as an error type.
func myMethod() error {
return MyCustomError{message: "my custom error"}
}
you see that our myMethod
declares that the return of this method is a built-in error type and the method return a MyCustomError
, this is possible because MyCustomError
implements all methods required by the Error
interface this way we can say that MyCustomError
is a error type.
This is the main principle to write mocks in go
, we create a custom struct that implementes the needed methods that provide all methods of a specific resource.
We'll not write an entire struct for sql.DB
, thanks to God DATA-DOG
we already have the package to do this job
Hands on
In the follow examples i'll let some
TODOS
in the code so you can see what is missing, i let you a challenge at the end, good luck.
Ok, know that we know a little about interfaces/structs let's dive in mocks
, first create a new project with these files: database.go
and test_database.go
the database.go
file:
package database
import "database/sql"
type Repository interface {
Find(id int) error
}
type repository struct {
// the db field is of type `DB` from the `database` package
// that provides the method to interact with our real database
db *sql.DB
}
func NewRepository(db *sql.DB) Repository {
return &repository{db: db}
}
// Find retrieves a single record from database
// based on id column
func (r repository) Find(id int) error {}
Notes:
The NewRepository
method declares that will return a Repository
interface and accept the sql.DB
struct as parameter, this interface is only for learning propose, we do not have to write it in this example.
Let's start with the Find
method, in your database_test
file write the follow code:
package database
import (
"gopkg.in/DATA-DOG/go-sqlmock.v1"
"testing"
)
func TestFindDatabaseRecord(t *testing.T) {
// the db satisfy the sql.DB struct
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
defer db.Close()
// our sql.DB must exec the follow query
mock.ExpectQuery("SELECT \\* FROM my_table").
// the number 3 must be on the query like "where id = 3"
WithArgs(3)
// TODO: define the values that need to be returned from the database
myDB := NewRepository(db) // passes the mock to our code
// run the code with the database mock
if err = myDB.Find(3); err != nil {
t.Errorf("something went wrong: %s", err.Error())
}
}
Resume:
The code above create a mock of sql.DB
struct and passes it as parameter to our code, the mock says that the query SELECT * FROM my_table
must be executed with the argument 3
, the DATA-DOG
package provides a lot of methods like ExpectExec
, WillReturnResult
and ExpectRollback
, the package is smart enough to know when you run a specific method and will compare your expectations if needed.
the Find
method:
func (r repository) Find(id int) error {
// r is the reference to our struct and db is the mock that we passed
stmt, err := r.db.Prepare("SELECT * from my_table WHERE id = $1")
if err != nil {
return err
}
defer stmt.Close()
// change the placeholder $1 to use the id parameter
_, err = stmt.Exec(id) // should be result, err = stmt.Exec(id)
if err != nil {
return err
}
// TODO: we need to fill a struct with the database result
// and return it with nil
return nil // should be mystruct, nil
}
You have noted that the Find
method doesn't return any database information, the examples of this code is very generic with the purpose of you implement it, can you handle this?
What you have to todo from here:
- return the data on the
Find
method along with the error - compare the returned values
hints:
- take a look at the method
NewRows
and theWillReturnRows
. - create a struct to hold the database values and return this struct on
Find
method
when are finished share with me what you have done! :)
Thank you for reading till here, if you have any advice, questions or suggestion i'll be glad to talk about it.
Top comments (3)
We have not made any database connection here. db.open. Do we need that or mock will do for us.
We need to add the connection_string to interact with db
That's the point Dinesh, this post show how to mock the database connection so your tests don't depend of a external software to run consequently running faster, there is no need to create a "connection_string" because the real connection never happens.
I am writing mock test for the first time, how it will show us the correct result if it is not connecting to the db. Can you provide me one complete example where I can run the test and check. In my case, I have a select query and I want to do the mock test. I have used gorm for the connection.
If you want I can share the code with you
Here is the mock code which I am trying but facing issue :
product_test.go
func TestFindPublication(t *testing.T) {
db, mock, err := sqlmock.New()
if err != nil {
t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
}
defer db.Close()
myDb := mock.ExpectQuery("SELECT * FROM product_data").
// the number 3 must be on the query like "where id = 3"
WithArgs("ST")
myDB := repo.NewProductRepository()
result,err := myDB.FindProductFromPublication("ST")
if err != nil {
t.Errorf("something went wrong: %s", err.Error())
}
assert.Equal(t, myDb,result)
}
product.go :
// ProductRepository Interface implemented
type ProductRepository interface {
FindProductFromPublication(pub string) ([]model.Product, error)
}
// ProductRepositoryImpl repository to product table
type ProductRepositoryImpl struct {
conn *gorm.DB
}
// NewProductRepository construct product repository
func NewProductRepository() ProductRepository {
return ProductRepositoryImpl{}
}
// FindProductFromPublication based on pub
func (productListRepository ProductRepositoryImpl) FindProductFromPublication(pub string) ([]model.Product, error) {
err := productListRepository.connect()
if err != nil {
return nil, err
}
var products []model.Product
err = productListRepository.conn.Where("publication_id = ?", pub).Find(&products).Error
defer productListRepository.close()
return products, err
}
func (productListRepository *ProductRepositoryImpl) connect() error {
conn, err := database.Connect()
if err != nil {
return err
}
productListRepository.conn = conn
return nil
}
func (productListRepository ProductRepositoryImpl) close() {
err := productListRepository.conn.Close()
if err != nil {
log.Fatalf("unable to close the database %s", err)
}
}