technology from back to front

Testing with gocheck – custom checkers

This post follows on from my previous post about gocheck. gocheck uses a checker abstraction to test arbitrary properties in tests, in the previous post for example we used the Equals and Panic checkers to test our code. It is very simple to write your own checkers and I will walk through some more complicated checker in this post.

For our first example I will produce a simple checker to test whether a slice of int contains a specific int. It is very simple to write a function to check a slice of int contains a specific int, like this:

func contains(slice []int, value int) bool {
  for _, v := range slice {
    if v == value {
      return true
    }
  }
  return false
}

To use this function in a custom checker we must implement the gocheck.Checker interface, this requires two methods to be implemented Info() and Check(...). Helpfully gocheck supplies a CheckerInfo type so you don't have to implement it yourself, you can just use an anonymous embedding in your type. So here I define my type for my checker like so:

type containsChecker struct {
  *gocheck.CheckerInfo
}

We can then implement the Check method that will use our original contains method:

func (c *containsChecker) Check(params []interface{}, names []string) (result bool, error string) {
  var (
    ok    bool
    slice []int
    value int
  )
  slice, ok = params[0].([]int)
  if !ok {
    return false, "First parameter is not a []int"
  }
  value, ok = params[1].(int)
  if !ok {
    return false, "Second parameter is not an int"
  }
  return contains(slice, value), ""
}

We then create an instance of the struct to use in our gocheck.Check methods:

var Contains gocheck.Checker = &containsChecker{&gocheck.CheckerInfo{Name: "Contains", Params: []string{"Container", "Expected to contain"}}}

The CheckerInfo used to instantiate the Checker provides the values displayed when a check fails to describe the error.

We can now use this variable in our gocheck code:

func (s *S) TestContains(c *C) {
  a := []int{1, 2, 3}
  c.Check(a, Contains, 3)
  c.Check(a, Not(Contains), 4)
  c.Check(a, Contains, 4)
}

and view the results of running the test:

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

----------------------------------------------------------------------
FAIL: image_test.go:60: test.S.TestContains

image_test.go:64:
    c.Check(a, Contains, 4)
... Container []int = []int{1, 2, 3}
... Expected to contain int = 4
OOPS: 0 passed, 1 FAILED
--- FAIL: image.Test (0.01 seconds)
FAIL
gotest: "./8.out" failed: exit status 1

In the output you can see the labels we provided as the CheckerInfo being used.

The Contains checker takes a single parameter to check against but a checker can take an arbitrary number of parameters. I have used this to create two additional checkers that I have been using when working on some numerical algorithms to aid in testing floating point values. I have two new checkers one for testing that a floating point value is within a certain tolerance of a value and another for testing that a floating value is between a pair of bounds. My test code looks like this:

func (s *S) TestToleranceEquality(c *C) {
  c.Check(1.0, EqualsWithTolerance, 1.25, 0.5)
  c.Check(1.0, Not(EqualsWithTolerance), 1.25, 0.05)
}

func (s *S) TestBounds(c *C) {
  c.Check(1.0, Between, 0.0, 1.5)
  c.Check(1.0, Not(Between), 2.0, 2.5)
}

The new checkers are shown below:

func equalWithTolerance(a, b, tolerance float64) bool {
  return math.Fabs(a-b) <= math.Fabs(tolerance)
}

func withinBound(value, lower, upper float64) bool {
  return value >= lower && value <= upper
}

type equalsWithToleranceChecker struct {
  *gocheck.CheckerInfo
}

func (c *equalsWithToleranceChecker) Check(params []interface{}, names []string) (result bool, error string) {
  var (
    ok                            bool
    obtained, expected, tolerance float64
  )
  obtained, ok = params[0].(float64)
  if !ok {
    return false, "Obtained value is not a float64"
  }
  expected, ok = params[1].(float64)
  if !ok {
    return false, "Expected value is not a float64"
  }
  tolerance, ok = params[2].(float64)
  if !ok {
    return false, "Tolerance value is not a float64"
  }

  return equalWithTolerance(obtained, expected, tolerance), ""
}

var EqualsWithTolerance gocheck.Checker = &equalsWithToleranceChecker{&gocheck.CheckerInfo{Name: "EqualsWithTolerance", Params: []string{"obtained", "expected", "tolerance"}}}

type betweenChecker struct {
  *gocheck.CheckerInfo
}

func (c *betweenChecker) Check(params []interface{}, names []string) (result bool, error string) {
  var (
    ok                     bool
    obtained, lower, upper float64
  )
  obtained, ok = params[0].(float64)
  if !ok {
    return false, "Obtained value is not a float64"
  }
  lower, ok = params[1].(float64)
  if !ok {
    return false, "Lower value is not a float64"
  }
  upper, ok = params[2].(float64)
  if !ok {
    return false, "Upper value is not a float64"
  }

  return withinBound(obtained, lower, upper), ""
}

var Between gocheck.Checker = &betweenChecker{&gocheck.CheckerInfo{Name: "Between", Params: []string{"obtained", "lower", "upper"}}}

by
tim
on
31/08/11
 
 


two + 9 =

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