Dec 22, 2014

My reason to be excited for Go 1.4

More than a week ago, Go 1.4 was released, bringing new features and bug fixes to the programming language.

Many people consider the support for building shared libraries for Android the most important addition in this version. Others are excited about the new go generate tool. Some are happy that internal packages have landed, as they should help reduce duplication of code.

However, my favorite new feature of Go 1.4 is hidden deep in a section called "Minor changes to the [standard] library" in the release notes. A list item there states:

The testing package has a new facility to provide more control over running a set of tests. If the test code contains a function

func TestMain(m *testing.M)

that function will be called instead of running the tests directly. The M struct contains methods to access and run the tests.

To sum up the benefits of this feature in one sentence, it lets you do global set-up/tear-down for tests.

Why that excites me

Recently, I have been developing a full-blown MVC web application in Go. Having come from Python, where Django wraps you tightly with all sorts of helpers, I initially struggled with several areas of architecture of a web application.

One of those was database testing. Django provides a test runner that creates a fresh database every time the tests are run. This way, you can do away with mocking and run your tests on real data, thus improving their accuracy.

In Go, I don't use any framework for my projects, and even if I did, none seem to have their own sophisticated testing tools. Thus, I had to invent my own way to swap out the database while testing . After thinking the design over for weeks, the final result looked like this:

func TestSomeFeature(t *testing.T) {
    models.TestDBManager.Enter()
    defer models.TestDBManager.Exit()

    // Do the tests
}

Where Enter() is responsible for creating a new Postgres database, running the migrations, swapping out the usual database connection, while Exit() drops the database and restores the original one.

This worked, but was less than ideal. Every single test which utilized the DB had to go through this process and thus as the testing suite grow, the time it took to run tests followed linearly. Even if I wanted to only do this once, there was no single point of entry or exit I could hook on when go test ran.

The introduction of TestMain() made it possible to run these migrations only once. The code now looks like this:

func TestSomeFeature(t *testing.T) {
    defer models.TestDBManager.Reset()

    // Do the tests
}

func TestMain(m *testing.M) {
    models.TestDBManager.Enter()
    // os.Exit() does not respect defer statements
    ret := m.Run()
    models.TestDBManager.Exit()
    os.Exit(ret)
}

While each test must still clean up after itself, that only involves restoring the initial data, which is way faster than doing the schema migrations.

Package Without TestMain With TestMain
models 3.017s 0.548s
handlers 9.329s 2.971s
settings 0.008s 0.008s
cli 1.230s 0.528s

The table above illustrates the performance improvements. The time it takes to run tests has reduced significantly, considering the database-heavy parts.

This approach also reduces code duplication by 50%: we now only have one line for database management in each test instead of two.

Less is more

The addition of the TestMain function, as simple change as it is, improved my development process in important ways. This is why Go makes me happy: every 6 months, with a new version of the language, new features come along. Some are big, but most are little. Despite that, they all play an important part in making Go better, without sacrificing core values of the language. I am sure many others will benefit from the improvements brought by Go 1.4.