technology from back to front

Testing go programs with gocheck

The Go programming language comes with a simple built in test framework testing – this is usable and functional but is lacking in features that you might find in other languages test frameworks. A more fully featured testing framework called gocheck has been developed by Gustavo Niemeyer, this blog post walks through developing an extremely simple go program using gocheck.

The code we will try and produce is a package for editing two dimensional images such as icons. To start with lets write a minimal implementation of the library and a test. We will use a struct that holds a two dimensional array of integers and the size of the image and add a New function so that we can hide the underlying array from consumers of the library.

package image

type Image struct {
  M, N int
  content [][]int
}

func New(M, N int) *Image {
  c := make([][]int, M, M)
  for i := 0; i < M; i++ {
    c[i] = make([]int, N, N)
  }
  return &Image{N, M, c}
}

For the first test I would really like not to be able to create images with negative sizes and would like the library to panic if that happens. gocheck allows panics to be tested for so this is our first basic test.

package image

import (
  . "launchpad.net/gocheck"
  "testing"
  "os"
)

// This plumbs gocheck into testing
func Test(t *testing.T) {
  TestingT(t)
}

// This is a fixure used by a suite of tests
type S struct{}
var _ = Suite(&S{})

// This is a test
func (s *S) TestNew(c *C) {
  c.Assert(func(){New(-1, -2)}, Panics, os.NewError("Bang"))
}

Now I run the test!

make test
gotest
rm -f _test/test.a
8g  -o _gotest_.8 image.go  image_test.go
rm -f _test/test.a
gopack grc _test/test.a _gotest_.8

----------------------------------------------------------------------
PANIC: image_test.go:16: test.S.TestNew

... Panic: runtime error: makeslice: len out of range (PC=0x8056ABA)

/home/tim/Tools/go/src/pkg/runtime/proc.c:1041
  in runtime.panic
/home/tim/Tools/go/src/pkg/runtime/runtime.c:116
  in runtime.panicstring
/home/tim/Tools/go/src/pkg/runtime/slice.c:22
  in runtime.makeslice
image.go:9
  in test.New
image_test.go:17
  in test.S.TestNew
OOPS: 0 passed, 1 PANICKED
--- FAIL: image.Test (0.01 seconds)
FAIL
gotest: "./8.out" failed: exit status 1
make: *** [test] Error 2

It has failed, a panic occurred but not the panic I expected, so lets modify the New function to produce a specific panic if negative sizes are passed in.

const newError = "An image dimension cannot be negative (M = %d  N = %d)"

func New(M, N int) *Image {
  if M < 0 || N < 0 {
    panic(fmt.Errorf(newError, M, N))
  }

  c := make([][]int, M, M)
  for i := 0; i < M; i++ {
    c[i] = make([]int, N, N)
  }
  return &Image{N, M, c}
}

Running the tests again produces a pass.

make test
gotest
rm -f _test/test.a
8g  -o _gotest_.8 image.go  image_test.go
rm -f _test/test.a
gopack grc _test/test.a _gotest_.8
OK: 1 passed
PASS

Now lets provide a function to allow values to be read from the image, to add a small wrinkle let us make the origin of the image be at (1, 1) as opposed to (0, 0). Additionally in go style let us return an error with the result in a similar way to go maps. We use two auxiliary methods and a function to check that coordinates are valid.

func check(val, limit int) bool {
  return val > 0 && val <= limit
}

func (i Image) checkX(x int) bool {
  return check(x, i.M)
}

func (i Image) checkY(y int) bool {
  return check(y, i.N)
}

func (i Image) ValueAt(x, y int) (value int, ok bool)   {
  if i.checkX(x) && i.checkY(y) {
    return i.content[x - 1][y - 1], true
  }
  return 0, false
}

The accompanying test looks like this:

func (s *S) TestNewShouldPanicWithNegatives(c *C) {
  c.Assert(func() { New(-1, -2) }, Panics, fmt.Errorf(newError, -1, -2))
}

func (s *S) TestNew(c *C) {
  c.Assert(New(1, 2), NotNil)
}


func (s *S) TestValueAt(c *C) {
  i := New(1, 2)
  v, ok := i.ValueAt(-1, 2)
  c.Assert(v, Equals, 0)
  c.Assert(ok, Equals, false)

  v, ok = i.ValueAt(1, 1)
  c.Assert(v, Equals, 0)
  c.Assert(ok, Equals, true)
}

Running this gives us another pass.

make test
gotest
rm -f _test/test.a
8g  -o _gotest_.8 image.go  image_test.go
rm -f _test/test.a
gopack grc _test/test.a _gotest_.8
OK: 3 passed
PASS

Now lets add a function to put data into the image pixel, by pixel, this function also returns a value to indicate whether the pixel value was set, if you attempt to set a pixel outside of the image it will return false.

func (i* Image) SetValueAt(x, y, v int) (ok bool) {
  if i.checkX(x) && i.checkY(y) {
    i.content[x - 1][y - 1] = v
    return true
  }
  return false
}

With an accompanying test:

func (s *S) TestSetValueAt(c *C) {
  i := New(1, 2)
  c.Assert(i.SetValueAt(-1, -2, 3), Equals, false)
  c.Assert(i.SetValueAt(1, 2, 3), Equals, true)

  v, ok := i.ValueAt(1, 2)
  c.Assert(v, Equals, 3)
  c.Assert(ok, Equals, true)
}

Which will of course pass.

make test
gotest
rm -f _test/test.a
8g  -o _gotest_.8 image.go  image_test.go
rm -f _test/test.a
gopack grc _test/test.a _gotest_.8

----------------------------------------------------------------------
FAIL: image_test.go:36: test.S.TestSetValueAt

image_test.go:39:
    c.Assert(i.SetValueAt(1, 2, 3), Equals, true)
... obtained bool = false
... expected bool = true

OOPS: 3 passed, 1 FAILED
--- FAIL: image.Test (0.01 seconds)
FAIL
gotest: "./8.out" failed: exit status 1
make: *** [test] Error 2

And I have been overconfident, aarghhhhh! why doesn’t it pass? Much pondering later I realised that I was having problems with my alphabet in my New function since I swap N for M when I construct the struct. The correct New function looks like this:

func New(M, N int) *Image {
  if M < 0 || N < 0 {
    panic(fmt.Errorf(newError, M, N))
  }

  c := make([][]int, M, M)
  for i := 0; i < M; i++ {
    c[i] = make([]int, N, N)
  }
  return &Image{M, N, c}
}

Running the tests again:

make test
gotest
rm -f _test/test.a
8g  -o _gotest_.8 image.go  image_test.go
rm -f _test/test.a
gopack grc _test/test.a _gotest_.8
OK: 4 passed
PASS

Now I refactor my test to removes some of the duplication and put some common code into the fixture.

type S struct{
  i *Image
}

func (s *S) SetUpTest(c *C) {
  s.i = New(1, 2)
}

func (s *S) TearDownTest(c *C)  {
  s.i = nil
}

func (s *S) TestValueAt(c *C) {
  v, ok := s.i.ValueAt(-1, 2)
  c.Assert(v, Equals, 0)
  c.Assert(ok, Equals, false)

  v, ok = s.i.ValueAt(1, 1)
  c.Assert(v, Equals, 0)
  c.Assert(ok, Equals, true)
}

func (s *S) TestSetValueAt(c *C) {
  c.Assert(s.i.SetValueAt(-1, -2, 3), Equals, false)
  c.Assert(s.i.SetValueAt(1, 2, 3), Equals, true)

  v, ok := s.i.ValueAt(1, 2)
  c.Assert(v, Equals, 3)
  c.Assert(ok, Equals, true)
}

Now it would be handy to actually see the image so lets make it to conform to the Stringer interface required by the various Print* functions in the fmt package. First the test:

func (s *S) TestString(c *C) {
  var stringer fmt.Stringer
  c.Assert(s.i, Implements, &stringer)
}

Which fails:

make test
gotest
rm -f _test/test.a
8g  -o _gotest_.8 image.go  image_test.go
rm -f _test/test.a
gopack grc _test/test.a _gotest_.8

----------------------------------------------------------------------
FAIL: image_test.go:54: test.S.TestString

image_test.go:56:
    c.Assert(s.i, Implements, &stringer)
... obtained *image.Image = &image.Image{M:1, N:2, content:[][]int{[]int{0, 0}}}
... ifaceptr *fmt.Stringer = (*fmt.Stringer)(0x500017a8)

OOPS: 4 passed, 1 FAILED
--- FAIL: image.Test (0.01 seconds)
FAIL
gotest: "./8.out" failed: exit status 1
make: *** [test] Error 2

I add a simple String function:

func (i Image) String() string {
  return "Image"
}

And the tests pass:

make test
gotest
rm -f _test/test.a
8g  -o _gotest_.8 image.go  image_test.go
rm -f _test/test.a
gopack grc _test/test.a _gotest_.8
OK: 5 passed
PASS

So we have some basic functionality and it is all tested. Next time I will extend this example and write a custom checker for gocheck to help out.

by
tim
on
31/07/11
 
 


× 5 = forty five

2000-14 LShift Ltd, 1st Floor, Hoxton Point, 6 Rufus Street, London, N1 6PE, UK+44 (0)20 7729 7060   Contact us