New 42-day free trial
Smarty

Testing in Go by example: Part 1

February 27, 2015
Tags
Smarty header pin graphic

Here's part 1 of our "Testing in Go" series.

Introduction

Thinking about trying Go? You won't regret it! It's great that testing is baked into the "testing" package from the standard library and the corresponding go test command (which has all sorts of useful and interesting flags).

We'd like to show you how easy it is to get started using the built-in testing tools and introduce you to some tools we've created. This is the first installment of a series designed to do just that.

All you have to do is create a file named like *_test.go, and start with something like this:

package foo

import "testing"

func TestSomething(t *testing.T) {
	// test stuff here...
}

Notice that the test function starts with Test... and receives a single parameter: t *testing.T. Those are the only contractual obligations to get a test going. Then just run go test in that directory and your test will run:

$ go test -v
=== RUN TestSomething
--- PASS: TestSomething (0.00s)
PASS
ok  	github.com/smartystreets/goconvey/examples/vanilla/foo	0.005s

The -v (verbose) flag shows test function names as they run.


Test functions are given the benefit of the doubt and labeled with PASS unless there is definite failure:

func TestSomething(t *testing.T) {
	t.Fail()
}

Here's the output of go test:

=== RUN TestSomething
--- FAIL: TestSomething (0.00s)
FAIL
exit status 1
FAIL	github.com/smartystreets/goconvey/examples/vanilla/foo	0.005s

Typically you'll want to show why a test failed:

func TestSomething(t *testing.T) {
	t.Error("I'm in a bad mood.")
}

And the output of go test:

=== RUN TestSomething
--- FAIL: TestSomething (0.00s)
	foo_test.go:6: I'm in a bad mood.
FAIL
exit status 1
FAIL	github.com/smartystreets/goconvey/examples/vanilla/foo	0.004s

Recurring example

In this series about testing in Go I'll use a well-known unit testing example by Uncle Bob Martin called "Bowling Game".

The production code implements a scoring algorithm for a bowling game score card, incorporating bonus rules for strikes and spares. For reference, here's a correct implementation:

package vanilla

// Game contains the state of a bowling game.
type Game struct {
	rolls   []int
	current int
}

// NewGame allocates and starts a new game of bowling.
func NewGame() *Game {
	game := new(Game)
	game.rolls = make([]int, maxThrowsPerGame)
	return game
}

// Roll rolls the ball and knocks down the number of pins specified by pins.
func (self *Game) Roll(pins int) {
	self.rolls[self.current] = pins
	self.current++
}

// Score calculates and returns the player's current score.
func (self *Game) Score() (sum int) {
	for throw, frame := 0, 0; frame < framesPerGame; frame++ {
		if self.isStrike(throw) {
			sum += self.strikeBonusFor(throw)
			throw += 1
		} else if self.isSpare(throw) {
			sum += self.spareBonusFor(throw)
			throw += 2
		} else {
			sum += self.framePointsAt(throw)
			throw += 2
		}
	}
	return sum
}

// isStrike determines if a given throw is a strike or not. A strike is knocking
// down all pins in one throw.
func (self *Game) isStrike(throw int) bool {
	return self.rolls[throw] == allPins
}

// strikeBonusFor calculates and returns the strike bonus for a throw.
func (self *Game) strikeBonusFor(throw int) int {
	return allPins + self.framePointsAt(throw+1)
}

// isSpare determines if a given frame is a spare or not. A spare is knocking
// down all pins in one frame with two throws.
func (self *Game) isSpare(throw int) bool {
	return self.framePointsAt(throw) == allPins
}

// spareBonusFor calculates and returns the spare bonus for a throw.
func (self *Game) spareBonusFor(throw int) int {
	return allPins + self.rolls[throw+2]
}

// framePointsAt computes and returns the score in a frame specified by throw.
func (self *Game) framePointsAt(throw int) int {
	return self.rolls[throw] + self.rolls[throw+1]
}

// testing utilities:

func (self *Game) rollMany(times, pins int) {
	for x := 0; x < times; x++ {
		self.Roll(pins)
	}
}
func (self *Game) rollSpare() {
	self.Roll(5)
	self.Roll(5)
}
func (self *Game) rollStrike() {
	self.Roll(10)
}

const (
	// allPins is the number of pins allocated per fresh throw.
	allPins = 10

	// framesPerGame is the number of frames per bowling game.
	framesPerGame = 10

	// maxThrowsPerGame is the maximum number of throws possible in a single game.
	maxThrowsPerGame = 21
)

Example test suite

I hope that's pretty straightforward, but if not, here are some tests that document how one would calculate a bowling game score with the provided Game struct (above).

package vanilla

import "testing"

func TestGutterBalls(t *testing.T) {
	t.Log("Rolling all gutter balls... (expected score: 0)")
	game := NewGame()
	game.rollMany(20, 0)

	if score := game.Score(); score != 0 {
		t.Errorf("Expected score of 0, but it was %d instead.", score)
	}
}

func TestOnePinOnEveryThrow(t *testing.T) {
	t.Log("Each throw knocks down one pin... (expected score: 20)")
	game := NewGame()
	game.rollMany(20, 1)

	if score := game.Score(); score != 20 {
		t.Errorf("Expected score of 20, but it was %d instead.", score)
	}
}

func TestSingleSpare(t *testing.T) {
	t.Log("Rolling a spare, then a 3, then all gutters... (expected score: 16)")
	game := NewGame()
	game.rollSpare()
	game.Roll(3)
	game.rollMany(17, 0)

	if score := game.Score(); score != 16 {
		t.Errorf("Expected score of 16, but it was %d instead.", score)
	}
}

func TestSingleStrike(t *testing.T) {
	t.Log("Rolling a strike, then 3, then 7, then all gutters... (expected score: 24)")
	game := NewGame()
	game.rollStrike()
	game.Roll(3)
	game.Roll(4)
	game.rollMany(16, 0)

	if score := game.Score(); score != 24 {
		t.Errorf("Expected score of 24, but it was %d instead.", score)
	}
}

func TestPerfectGame(t *testing.T) {
	t.Log("Rolling all strikes... (expected score: 300)")
	game := NewGame()
	game.rollMany(21, 10)

	if score := game.Score(); score != 300 {
		t.Errorf("Expected score of 300, but it was %d instead.", score)
	}
}

Test output

$ go test -v

=== RUN TestGutterBalls
--- PASS: TestGutterBalls (0.00s)
	bowling_game_test.go:6: Rolling all gutter balls... (expected score: 0)
=== RUN TestOnePinOnEveryThrow
--- PASS: TestOnePinOnEveryThrow (0.00s)
	bowling_game_test.go:16: Each throw knocks down one pin... (expected score: 20)
=== RUN TestSingleSpare
--- PASS: TestSingleSpare (0.00s)
	bowling_game_test.go:26: Rolling a spare, then a 3, then all gutters... (expected score: 16)
=== RUN TestSingleStrike
--- PASS: TestSingleStrike (0.00s)
	bowling_game_test.go:38: Rolling a strike, then 3, then 7, then all gutters... (expected score: 24)
=== RUN TestPerfectGame
--- PASS: TestPerfectGame (0.00s)
	bowling_game_test.go:51: Rolling all strikes... (expected score: 300)
PASS
ok  	github.com/smartystreets/goconvey/examples/vanilla	0.005s

Analysis

Good stuff

  • Testing is a first-class citizen in Go.
  • The testing package is very easy to learn and use.
  • The go test command is an adequate, flexible test runner.
  • The go test command line flags are helpful and provide lots of interesting utility.

Not-so-good stuff

  • Tests are always run in the same order.
  • There's no convenient way to run shared "setup" or "teardown" logic to be run for each test function.
  • There are no helpful "assertion/helper" methods provided by the testing package.
  • You have to compose your own error messages in failure situations.
  • The convention is to check that what shouldn't have happened didn't happen, rathern than to ensure/assert that what should have happened actually did happen. This is a subtle yet important point which we will discuss more in a later post.

The "Not-So-Good Stuff" above can be rephrased this way: the Go testing package just isn't xunit. Depending on your point of view and experience this isn't a bad thing, it's just an observation.

Kent Beck came up with xunit (first called SUnit for the smalltalk language) to facilitate TDD and unit testing with a more expressive, automatable API. Later, Dan North coined the term "Behavoir Driven Development", or BDD, from which jBehave and other libraries were born.

So, when we came to Go we were specifically accustomed to:

You can probably understand that we were a little disappointed that the Go testing package did so little by comparison. This lack of features has been addressed by the Go authors so this wasn't an oversight on their part. They strived, successfully, to create the simplist solution for the largest audience. So, in the next few posts we'll demonstrate some of the things we've created at Smarty to achieve the kind of testing toolkit we were missing.

Subscribe to our blog!
Learn more about RSS feeds here.
rss feed icon
Subscribe Now
Read our recent posts
Improving user/customer experience in every industry with clean address data
Arrow Icon
You finally track down an essential addition to your collector’s set of [insert item of your choice], and you're hyped to buy it until the chaos begins. The cart is hidden in a fly-out on the side, cluttered with blocky, overwhelming text. You spend way too long just trying to find the "Proceed to Checkout" button. 👎 That’s bad UI (user interface): messy, confusing design that makes navigation a chore. You make it to the checkout and start entering your info, but the site keeps rejecting your address.
Dashboard essentials for Smarty users
Arrow Icon
The Smarty dashboard is your central hub for managing address verification, geocoding, and property data services. Whether you're just starting or looking to optimize your current setup, understanding the dashboard's full capabilities can significantly streamline your address data operations. We recently held a webinar in which we reviewed all of the Smarty dashboard's items and features. Missed it? That's OK; we've got all the information right here. You can expect to read about:Accessing your dashboardSetting up your account for successUnderstanding your active subscriptionsManaging API keys effectivelyStreamlining billing and financial managementStaying informed with smart notificationsTeam management and access controlsWeb toolsMaking the most of free trialsKey takeawaysLet’s get going!Accessing your dashboardGetting to your dashboard is straightforward.
Take charge of your API usage with Smarty’s key management features
Arrow Icon
Ever wondered, “Where did all my lookups go?!” Without proper API management, you may burn through your lookups quicker, experience runaway code, and encounter unexpected usage. That’s why Smarty created usage by key (included in all annual plans) and limit by key (included in some plans; you can add them by contacting sales) for its APIs. Why key management mattersCommon API usage challenges (problems to solve):Unexpected spikes in lookupsDifficulty tracking specific key usageWhich keys are calling which Smarty licenseNeed for better control over API consumptionDifficulty allocating Smarty lookups across an organizationWith Smarty's key management features, you gain more control by having better visibility of your usage, eliminating the element of surprise (you know, the bad kind, like when you’re suddenly out of lookups).

Ready to get started?