2

Even though there are a few posts on this, I haven't found one with much substance. So hopefully a few people will share opinions on this.

One thing holding me up from having a true TDD workflow is that I can't figure out a clean way to test things that have to connect to networked services like database.

For example:

type DB struct {
    conn *sql.DB
}

func NewDB(URL string) (*DB, err) {
    conn, err := sql.Open("postgres", URL)
    if err != nil {
        return nil, err
    }
}

I know I could pass the sql connection to NewDB, or directly to the struct and assign it to an interface that has all the methods I need, and that would be easily testable. But somewhere, I'm going to have to connect. The only way to test this that I've been able to find is...

var sqlOpen = sql.Open
func CreateDB() *DB {
    conn, err := sqlOpen("postgres", "url...")
    if err != nil {
         log.Fatal(err)
    }

    dataBase = DB{
        conn: conn
    }
}

Then in the test you swap out the sqlOpen function with something that returns a function with the same signature that will give an error for one test case and not give an error for another. But this feels like a hack, especially if you're doing this for several functions in the same file. Is there a better way? The codebase I'm working with has a lot of functions in packages and network connections. Because I'm struggling to test things in clean way, it's driving me away from TDD.

Dmitry Harnitski
  • 5,838
  • 1
  • 28
  • 43
A.Rowden
  • 31
  • 3

1 Answers1

1

Typical business application has A LOT of logic in queries. We significantly decrease testing coverage and leave room for regression errors if they are not tested. So, mocking DB repositories is not the best option. Instead, we can mock database itself and test how we work with it on SQL level.

Below are sample code using DATA-DOG/go-sqlmock, but there could be other libraries that mock sql databases.

First of all, we need to inject sql connection into our code. GO sql connection is a misleading name and it is actually connections pool, not just single DB connection. That is why, it is make sense to create single *sql.DB in your composition root and reuse in your code even if you do not write tests.

Sample below shows how to mock web service.

At the beginning, we need to create new handler with injected connection:

// New creates new handler
func New(db *sql.DB) http.Handler {
    return &handler{
        db:     db,
    }
}

Handler code:

type handler struct {
    db     *sql.DB
}

func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
  // some code that loads person name from database using id
}

Unit Test that code that mocks DB. It uses stretchr/testify for assertions :

func TestHandler(t *testing.T) {
    db, sqlMock, _ := sqlmock.New()
    rows := sqlmock.NewRows([]string{"name"}).AddRow("John")
    // regex is used to match query
    // assert that we execute SQL statement with parameter and return data
    sqlMock.ExpectQuery(`select name from person where id \= \?`).WithArgs(42).WillReturnRows(rows)
    defer db.Close()

    sut := mypackage.New(db)

    r, err := http.NewRequest(http.MethodGet, "https://example.com", nil)
    require.NoError(t, err, fmt.Sprintf("Failed to create request: %v", err))
    w := httptest.NewRecorder()

    sut.ServeHTTP(w, r)
    // make sure that all DB expectations were met 
    err = sqlMock.ExpectationsWereMet()
    assert.NoError(t, err)
    // other assertions that check DB data should be here 
    assert.Equal(t, http.StatusOK, w.Code)
}

Our test asserts simple SQL statement against DB. But with go-sqlmock it is possible to test all CRUD operations and database transactions.

Test above still has one weak point. We tested that our SQL statement is executed from code, but we did not test if it works against our real DB. That issue cannot be solved with unit tests. The only solution is integration test against real DB.

We are in better position now though. Out business logic is already tested in unit tests. We do not need to create lots of integration tests to cover different scenarios and parameters, instead we need to have just one test per query to verify SQL syntax and match to our DB schema.

Happy testing!

Dmitry Harnitski
  • 5,838
  • 1
  • 28
  • 43
  • Thank you. I agree with testing the SQL syntax and that's a good approach for SQL specifically. But what about other networked services like web-sockets, messaging queues, etc.? Is there a better way to test past the connection functions than making them global and swapping them out at runtime in the test? – A.Rowden Nov 16 '18 at 16:42
  • @A.Rowden You can mock calls to external web services -https://stackoverflow.com/a/53231951/1420332. It makes sense to reuse Http client at least for `oauth` to reuse tokens. – Dmitry Harnitski Nov 16 '18 at 17:21