Golang from Python

Testing and the Database

Writing Go database code took several tries before I settled on something that worked well for tests and HTTP handlers. The root decision was that all database functions should require a *sql.DB. Everything else falls into place — concise tests, separation between the application and test databases, and flexible HTTP request handlers.

The application *sql.DB is created in main(), and associated with each HTTP request context with some request middleware, which I'll talk about more in another post. I spent some time with a global *sql.DB, but it made testing HTTP handlers uncomfortable. Instead, some middleware associates the database connection with the *http.Request, and I retrieve it when needed:

func someHandler(w http.ResponseWriter, r *http.Request) {
    user, err := db.GetUserByGuid(GetDb(r), guid)
    // ...do other stuff
}

Tests work in the exact opposite way, by declaring var tdb *sql.DB globally in a _test.go file, so it isn't compiled in to my regular builds. Instead of GetDb(r), I pass tdb.

Cleaning Up After

Or, "Life Without tear_down." My usual test (if it exercises the database) runs some set-up, calls functions, and cleans up any data created in the process. Go cleans up resources by deferring a cleanup function; because cleaning up database data is such a common task, my first cleanup helper was a tearDown function for deleting test database rows. Using that, a test for a function that inserts a new row into the user table would look something like this:

func TestNewUser(t *testing.T) {
    username := randStr()
    defer tearDown("user", "Username = $1", username)

    user, err := CreateUser(username)
    // ...assertions follow
}

I make a point of deferring the tearDown before the actual insert. It lets me handle panics that happen between inserting data and returning from a function — which will be useful later. Here's how I implemented tearDown:

func tearDown(table, where string, params ...interface{}) {
    _, err := tdb.Exec(
        fmt.Sprintf("DELETE FROM \"%s\" WHERE %s", table, where),
        params...)
    if err != nil {
        // Usually I'd return the error, but using panic means less code in
        // my tests, which is more important.
        panic(fmt.Sprintf("Problem tearing down %s data: %v", table, err))
    }
}

More Rows, Fewer Lines

This works for tests that insert a row or two, but as you start using more supporting data in your tests, it makes sense to bundle up common object creation helpers in their own functions:

func TestSomethingElse(t *testing.T) {
    user, cleanup := newTestUser()
    defer cleanup()
    // ...test follows
}

You may have noticed, after all the fuss I made about queueing the cleanup before inserting the rows, there's no way to do that when you're returning a cleanup function. That means newTestUser has to handle cleaning up if there are any problems. Just remember, "fewer lines" refers to your tests, not your helpers:

func newTestUser() (User, func()) {
    username := randStr()
    cleanup := func() {
        tearDown("user", "Username = $1", username)
    }

    doCleanup := true
    defer func() {
        if doCleanup {
            cleanup()
        }
    }()

    user, err := CreateUser(username)
    if err != nil {
        panic(err)
    }

    doCleanup = false
    return user, cleanup
}

Writing test functions like this really starts to click when your helpers start calling each other — if any part of the test function fails, they all collapse back down, leaving a clean database.