Go's testing package side-effects

Problem

I’ve been working on a Go CLI app to push sensor data to Cloud™ and hit the problem.

In order to read the sensor data I had a very simple piece of code:

package main //analog_read.go

import (
	"flag"
	"fmt"
	"os"

	"github.com/hybridgroup/gobot/platforms/intel-iot/edison"
)

var (
	pin = flag.String("pin", "", "Sensor's pin-ID to read analog value from. Required.")
)

func main() {
	flag.Parse()

	if *pin == "" {
		flag.PrintDefaults()
		os.Exit(1)
	}

	var ed = edison.NewEdisonAdaptor("edison")

	v, err := ed.AnalogRead(*pin)
	if err != nil {
		fmt.Fprintln(os.Stderr, err)
	}

	fmt.Println(v)
}

If the above code runs without pin flag specified then it dumps flag defaults prompting to specify the pin.

But instead of expected prompt:

$ go run analog_read.go
  -pin string
        Sensor's pin-ID to read analog value from. Required.

I got completely unexpected prompt:

$ go run analog_read.go
  -pin string
        Sensor's pin-ID to read analog value from. Required.
  -test.bench string
        regular expression to select benchmarks to run
  -test.benchmem
        print memory allocations for benchmarks
  -test.benchtime duration
  #
  # ... and many more
  #

Somehow flags from testing package got mixed into flags of my app. WTF?

Testing package

testing package is not intended to be used outside of *_test.go files(unless it’s Ok to have side-effects the package produces).

If you look at its source code, it adds the flags I didn’t expect to show up in my little CLI tool:

  var matchBenchmarks = flag.String("test.bench", "", "regular expression to select benchmarks to run")
  var benchTime = flag.Duration("test.benchtime", 1*time.Second, "approximate run time for each benchmark")
  // etc

But why would those flags show up if no code imports the package? Well, because some other code imports the package!!?

Investigation

Luckily I had only single suspect to investigate: Gobot, because there’s no other packages imported aside ones from standard library in my app.

Quickly grepping through Gobot’s sources revealed what non-test code imports testing package:

gobot $ grep -nr '"testing"' ./|grep -v "_test.go"
./gobot/generate.go:317:	"testing"
./utils.go:13:	"testing"

where,

gobot/generate is not the offender as its "testing" is just a template string.

So, turns out gobot/utils.go(which is not a test) was importing "testing" and causing problems. Apparently just to introduce test helpers Assert and Refute and enable other packages to re-use them.

Refactoring

Right solution for sharing code that imports "testing" package is to extract it into separate package(gobot/gobottest in this case), so only test code could import it.

Given the fact that a lot of tests depend on Assert and Refute helpers it was a good opportunity to use goftm as a refactor tool:

$ gofmt -r 'gobot.Assert -> gobottest.Assert' 
$ gofmt -r 'gobot.Refute -> gobottest.Refute' 

Did the job and after moving helper code to the new package everything started working as expected!

Great Success!

Conclusion

Do not import "testing" package in your non-test code!

Links

Comments