user10358843
user10358843

Reputation: 13

Mocking go methods

I'm writing a small POC in go for work, but I can't seem to figure out the mocking techniques. this is what I have so far...

connect.go

package db

import (
    "database/sql"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "strings"

    _ "github.com/lib/pq"
)

type config map[string]interface{}

type DbConn struct {
    db db
}

type db interface {
    getConnectionStringFromConfig(file string) (connStr string, err error)
}

func NewDbConn(d db) *DbConn {
    return &DbConn{db: d}
}

func getConnectionStringFromConfig(file string) (connStr string, err error) {
    var c config
    var bt []byte
    if bt, err = ioutil.ReadFile(file); err != nil {
        fmt.Printf("Error Reading config file: %v", err)
        return
    }
    fmt.Println("Finish reading file. Going to construct a connection string")
    if err = json.Unmarshal(bt, &c); err != nil {
        fmt.Printf("Error unmarshalling config file: %v", err)
        return
    }
    connStr = strings.TrimLeft(getConfigAsString(c), " ")
    return
}

func getConfigAsString(c config) (connStr string) {
    for k, v := range c {
        connStr += strings.Join([]string{" " + k, v.(string)}, "=")
    }
    return
}

// Connect database connection
func (dbConn DbConn) Connect() (conn *sql.DB, err error) {
    fmt.Println("Starting database connection...")
    connStr, err := getConnectionStringFromConfig("path/to/conf.json")
    if err != nil {
        return
    }
    conn, err = sql.Open("some_driver", connStr)
    return
}

connect_test.go

package db

import (
    "errors"
    "testing"
)

type dbConnMock struct {
    db dbMock
}

type dbMock interface {
    getConnectionStringFromConfig(file string) (connStr string, err error)
}

func (dbm dbConnMock) getConnectionStringFromConfig(file string) (connStr string, err error) {
    return "", errors.New("123")
}

// should not throw error when trying to open db connection
func TestDatabaseConnection(t *testing.T) {
    dbCon := &DbConn{}
    _, err := dbCon.Connect()
    if err != nil {
        t.Errorf("test failed. \n %d", err)
    }
}

func TestDatabaseConnectionFail(t *testing.T) {
    var dm dbMock
    dbCon := NewDbConn(dm)
    _, err := dbCon.Connect()
    if err == nil {
        t.Errorf("test failed. %d", err)
    }
}

As you can see, this is a simple database connection logic which I test and mock using an interface. I want to cover 100% of the code, so I have to mock certain methods. The code above, although it works, the second test fails, probably because I'm missing something in my attempts to mock it. Please help..

Upvotes: 1

Views: 1002

Answers (1)

Ullaakut
Ullaakut

Reputation: 3744

There are a few things you can do.


Simple way

If you want to keep it simple, what you could do is make your mock structure have fields for what it should return, and in each test case you set those fields to what your mock should return for that test case.

This way, you can make your mock succeed or fail in different ways.

Also, you don't need a dbMock interface, as dbConnMock implements the db interface and that's all you need.

Here is what your mock could look like:

type dbConnMock struct {
    FileCalled string

    connStr string
    err error
}

func (dbm dbConnMock) getConnectionStringFromConfig(file string) (connStr string, err error) {
    dbm.FileCalled = file
    return dbm.connStr, dbm.err
}

Now, you can verify that your method was called with the expected argument by using FileCalled and you can make it have the behavior you'd like to simulate.

If you also want to make sure that your method is only called once, you can also add a counter to see how many times it was called for example.


Using a mocking library

If you don't want to worry about writing that logic, a few libraries can do that for you, such as testify/mock for example.

Here is an example of how a simple mock would work using testify/mock:

type dbMock struct {
    mock.Mock
}

func (m *dbMock) getConnectionStringFromConfig(file string) (string, error) {
    args := m.Called(file)

    return args.String(0), args.Error(1)
}

func TestSomething(t *testing.T) {
    tests := []struct {
        description string

        connStr string
        err error

        expectedFileName string

        // add expected outputs and inputs of your tested function
    }{
        {
            description: "passing test",

            connStr: "valid connection string",
            err: nil,

            expectedFileName: "valid.json",
        },
        {
            description: "invalid file",

            connStr: "",
            err: errors.New("invalid file"),

            expectedFileName: "invalid.json",
        },
    }

    for _, test := range tests {
        t.Run(test.description, func(t *testing.T) {

            dbMock := &dbConnectionMock{}
            dbMock.
                On("getConnectionStringFromConfig", test.expectedFileName).
                Return(test.connStr, test.err).
                Once()

            thing := &Something{
                db: dbMock,
            }

            output, err := thing.doSomething()

            // assert that output and err are expected

            dbMock.AssertExpectations(t) // this will make sure that your mock is only used as expected in your test, depending on your `On` calls
        })
    }
}

This code ensures that your method is called once and with specific arguments, and will make it return what is specified in your test case.

Upvotes: 1

Related Questions