Larry Price

And The Endless Cup Of Coffee

Better Testing in Go With Gocheck

| Comments

As a quick reminder, golang is a really fun programming language to use. It even includes testing out of the box! Unfortunately, this out-of-the-box testing framework isn’t all that great. It lacks the syntactic sugar of mature frameworks like rspec or gtest.

Of course, there are alternatives. I found an open-source library (licensed with Simplified BSD) called gocheck.

Gocheck, how do I love thee? Let me count the ways:

  • Test fixtures
  • Improved assertions
  • Improved test output
  • Sugar-coated syntax
  • Test skipping
  • Oh my

As usual, it’s time to guide you through a contrived example. Start by installing the package:

1
$ go get gopkg.in/check.v1

Let’s see, what should we make… how about a tip calculating library? We should start by testing, because we’re obsessed with TDD.

Test #1: Returns 0 for free meal

calculator_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package cheapskate

import (
  "testing"
  . "gopkg.in/check.v1"
)

// Hook up gocheck into the "go test" runner.
func Test(t *testing.T) { TestingT(t) }

type MySuite struct{}

var _ = Suite(&MySuite{})

func (s *MySuite) TestReturns0ForFreeMeal(c *C) {
  c.Assert(calculateTip(0.0), Equals, 0.0)
}

Not quite as obvious as the internal testing framework. First we hook up gocheck into the “go test” runner. Then we create a test suite; ours is empty for now and called MySuite. We call Suite to intialize the test runner with our custom suite. We then write our first test to assert that calculating the tip returns a value equal to 0. All tests must be prefixed with the word “Test”. Now I’ll write the implementation:

calculator.go
1
2
3
4
5
package cheapskate

func calculateTip(bill float64) float64 {
  return 0.0
}

Running the tests…

1
2
3
4
$ go test
OK: 1 passed
PASS
ok    _/home/lrp/Projects/2014/gocheck-quick  0.003s

Woohoo! All passed. What happens if we write a failing test?

calculator_test.go
1
2
3
4
...
func (s *MySuite) TestReturns15PercentByDefault(c *C) {
  c.Assert(calculateTip(100.0), Equals, 15.0)
}

Results:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[email protected]:~/Projects/2014/gocheck-quick$ go test

----------------------------------------------------------------------
FAIL: calculator_test.go:19: MySuite.TestReturns15PercentByDefault

calculator_test.go:20:
    c.Assert(calculateTip(100.0), Equals, 15.0)
... obtained float64 = 0
... expected float64 = 15

OOPS: 1 passed, 1 FAILED
--- FAIL: Test (0.00 seconds)
FAIL
exit status 1
FAIL  _/home/lrp/Projects/2014/gocheck-quick  0.003s

A nasty failure that one. I’ll fix it and continue:

calculator.go
1
2
3
4
5
package cheapskate

func calculateTip(bill float64) float64 {
  return .15 * bill
}

I want to create a Setup method for my entire suite. I’ll store some silly information there for information’s sake. The minBill and maxBill variables will only be set when I first load the suite.

calculator_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package cheapskate

import (
  "testing"
  . "gopkg.in/check.v1"
)

// Hook up gocheck into the "go test" runner.
func Test(t *testing.T) { TestingT(t) }

type MySuite struct{
  minBill float64
  maxBill float64
}

func (s *MySuite) SetUpSuite(c *C) {
  s.minBill = 0
  s.maxBill = 100
}

var _ = Suite(&MySuite{})

func (s *MySuite) TestReturns0ForFreeMeal(c *C) {
  c.Assert(calculateTip(s.minBill), Equals, 0.0)
}

func (s *MySuite) TestReturns15PercentByDefault(c *C) {
  c.Assert(calculateTip(s.maxBill), Equals, 15.0)
}

What if I wanted to set some information at the start of each test? I’ll log the current test number on the suite, updating it every time I run a test:

calculator_test.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package cheapskate

import (
  "testing"
  . "gopkg.in/check.v1"

  "fmt"
)

// Hook up gocheck into the "go test" runner.
func Test(t *testing.T) { TestingT(t) }

type MySuite struct{
  testNumber int
}

func (s *MySuite) SetUpTest(c *C) {
  fmt.Println(s.testNumber)
  s.testNumber += 1
}
...

Result:

1
2
3
4
5
6
$ go test
0
1
OK: 2 passed
PASS
ok    _/home/lrp/Projects/2014/gocheck-quick  0.004s

You can create tear down methods for suites and tests in the same manner, replacing the appropriate words above.

There’s loads of other cool stuff gocheck can do, I’ve barely scratched the surface with what little experience I’ve had using it. Like any testing framework, I’m sure it has its advantages and disadvantages, but it sure beats the pants off the off-the-shelf framework Google includes with golang.