Monkey patching with SQLMock

Firstly, a quick refresher: currently, the data package does not use DI, and therefore we cannot pass in the *sql.DB like we did in the previous example. The function currently looks as shown in the following code:

// Save will save the supplied person and return the ID of the newly 
// created person or an error.
// Errors returned are caused by the underlying database or our connection
// to it.
func Save(in *Person) (int, error) {
db, err := getDB()
if err != nil {
logging.L.Error("failed to get DB connection. err: %s", err)
return defaultPersonID, err
}

// perform DB insert
query := "INSERT INTO person (fullname, phone, currency, price) VALUES (?, ?, ?, ?)"
result, err := db.Exec(query, in.FullName, in.Phone, in.Currency, in.Price)
if err != nil {
logging.L.Error("failed to save person into DB. err: %s", err)
return defaultPersonID, err
}

// retrieve and return the ID of the person created
id, err := result.LastInsertId()
if err != nil {
logging.L.Error("failed to retrieve id of last saved person. err: %s", err)
return defaultPersonID, err
}
return int(id), nil
}

We could refactor to this, and perhaps in the future we might, but at the moment we have almost no tests on this code and refactoring without tests is a terrible idea. You might be thinking something similar to but if we write tests with monkey patching and then refactor to a different style of DI later, then we would have to refactor these tests, and you are right; this example is a little contrived. That said, there is nothing wrong with writing tests to provide you with a safety net or a high level of confidence now, and then deleting them later. It might feel like double work, but it's bound to be both less humiliating than introducing regression into a running system that people are relying on, and potentially less work that debugging that regression.

The first thing that jumps out is the SQL. We are going to need almost exactly the same string in our tests. So, to make it easier to maintain the code in the long term, we are going to convert that to a constant and move it to the top of the file. As the test is going to be quite similar to our previous example, let's first examine just the monkey patching. From the previous example, we have the following:

// define a mock db
testDb, dbMock, err := sqlmock.New()
defer testDb.Close()

require.NoError(t, err)

In these lines, we are creating a test instance of *sql.DB and a mock to control it. Before we can monkey patch our test instance of *sql.DB, we first need to create a backup of the original one so that we can restore it after the test is complete. To do this, we are going to use the defer keyword.

For those not familiar with it, defer is a function that is run just before the current function exits, that is, between executing the return statement and returning control to the caller of the current function. Another significant feature of defer is the fact that the arguments are evaluated immediately. The combination of these two features allows us to take a copy of the original sql.DB when defer is evaluated and not worry about how or when the current function exits, saving us from potentially a lot of copying and pasting of clean up code. This code looks as follows:

defer func(original sql.DB) {
// restore original DB (after test)
db = &original
}(*db)

// replace db for this test
db = testDb

With this done, the test looks as follows:

func TestSave_happyPath(t *testing.T) {
// define a mock db
testDb, dbMock, err := sqlmock.New()
defer testDb.Close()
require.NoError(t, err)

// configure the mock db
queryRegex := convertSQLToRegex(sqlInsert)
dbMock.ExpectExec(queryRegex).WillReturnResult(sqlmock.NewResult(2, 1))

// monkey patching starts here
defer func(original sql.DB) {
// restore original DB (after test)
db = &original
}(*db)

// replace db for this test
db = testDb
// end of monkey patch

// inputs
in := &Person{
FullName: "Jake Blues",
Phone: "01234567890",
Currency: "AUD",
Price: 123.45,
}

// call function
resultID, err := Save(in)

// validate result
require.NoError(t, err)
assert.Equal(t, 2, resultID)
assert.NoError(t, dbMock.ExpectationsWereMet())
}

Fantastic, we have our happy path test done. Unfortunately, we've only tested 7 out of 13 lines of our function; perhaps more importantly, we don't know whether our error handling code even works correctly.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.221.163.13