loading...

Using mock on Golang tests

rafaacioly profile image Rafael Acioly ・4 min read

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:

  1. return the data on the Find method along with the error
  2. compare the returned values

hints:

  • take a look at the method NewRows and the WillReturnRows.
  • 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.

Posted on Jan 2 '19 by:

rafaacioly profile

Rafael Acioly

@rafaacioly

backend developer; Go, Python and PHP

Discussion

markdown guide
 

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

}