New 42-day free trial
Smarty

Testing in Go by example: Part 4

Michael Whatcott
Michael Whatcott
 | 
August 11, 2015
Tags
Smarty header pin graphic

I think it's time for a slight detour. In part 1 we covered the basics of testing in go. In part 2 we covered a few slick ways to execute tests. In part 3 we covered some of our recent endeavors at Smarty to build on the basics. Toward the end of that post, we went into some detail regarding our approach to assertions. The assertions referenced in the GoConvey project are actually their own separate project that are imported into GoConvey. The nice thing about separating the assertions into their own separate project is that they can be used, well, separately. In this post, I'll show you some of the different ways we use the assertions project in our code outside of the GoConvey project.

In GoConvey, the So function exported by the convey package is void. All results of calling So() are swallowed by the convey functionality and passed to the reporting mechanisms. The assertions package (in the assertions project) exports its own version of the So() function, but it returns 2 parameters:

func So(actual interface{}, assert assertion, expected ...interface{}) (bool, string)

Here's the GoDoc comment:

So is a convenience function (as opposed to an inconvenience function?) for running assertions on arbitrary arguments in any context, be it for testing or even application logging. It allows you to perform assertion-like behavior (and get nicely formatted messages detailing discrepancies) but without the program blowing up or panicking. All that is required is to import this package and call So with one of the assertions exported by this package as the second parameter. The first return parameter is a boolean indicating if the assertion was true. The second return parameter is the well-formatted message showing why an assertion was incorrect, or blank if the assertion was correct.

You probably noticed the unexported type assertion in the So() signature above. Here's what it looks like:

// assertion is an alias for a function with a signature that the So()
// function can handle. Any future or custom assertions should conform to this
// method signature. The return value should be an empty string if the assertion
// passes and a well-formed failure message if not.
type assertion func(actual interface{}, expected ...interface{}) string

Each of the Should assertion functions in the assertions package can be used as an assertion func when calling So().

Here's a simple example of how to use So() with an assertion function in your code:

x, y := 99, 100
if ok, message := So(x, ShouldBeGreaterThan, y); !ok {
    log.Println(message)
    // or log.Fatal(message) if you're feeling unforgiving...
}

Notice that this example makes no reference to the "testing" package's *testing.T, but it could:

x, y := 99, 100
if ok, message := So(x, ShouldBeGreaterThan, y); !ok {
    t.Error(message)
}

If you know you're going to be using So within your test functions and calling t.Error() and the like, here's another way to use the assertions package:

func TestSomething(t *testing.T) {
	x, y := 99, 100
	assertions.New(t).So(x, ShouldBeGreaterThan, y)
}

Did you notice how we passed in t in the assertions.New() function? The nice thing about this approach is you don't actually have to call t.Error() in failure scenarios. That is handled by the Assertion.So() function because it is given a reference to t.

You can actually call .So() multiple times on an Assertion instance:

assert := assertions.New(t)
assert.So(1, ShouldBeGreaterThan, 2)
assert.So(true, ShouldBeFalse)
assert.So(time.Now(), ShouldHappenBefore, time.Now())
// etc...

You can find out if any of those assertions failed and act accordingly:

if assert.Failed() {
	log.Fatal("Ouch")
}

You've probably noticed that to be able to reference assertions.New and ShouldEqual you'll need an import statement like this:

import (
	"github.com/smartystreets/assertions"
	. "github.com/smartystreets/assertions"
)

func TestSomething(t *testing.T) {
	assert := assertions.New(t)
	assert.So(1, ShouldBeGreaterThan, 2)
	assert.So(true, ShouldBeFalse)
	assert.So(time.Now(), ShouldHappenBefore, time.Now())
}

That's somewhat awkward, so we created an alias package for the assertion functions called should:

import (
	"github.com/smartystreets/assertions"
	"github.com/smartystreets/assertions/should"
)

func TestSomething(t *testing.T) {
	assert := assertions.New(t)
	assert.So(1, should.BeGreaterThan, 2)
	assert.So(true, should.BeFalse)
	assert.So(time.Now(), should.HappenBefore, time.Now())
}

Did you catch the difference? ShouldBeGreaterThan becomes should.BeGreaterThan. If you're using goimports, then this approach works out really smoothly. If you're not using goimports, why not? It's easy to set up.

The moral of the story is that it should be easy to check that a value is what you expect it to be. We prefer not to write if statements and hand-crafted log statements whenever possible, so we've created the assertions package that helps us in various contexts, both in testing and otherwise. In the next post, we'll talk about yet another use of the assertions package as we talk about our newest test library called gunit.

Before signing off, I would be remiss if I didn't give credit to Aaron Jacobs, the creator of the oglematchers package, which many of the assertion functions use internally to do their jobs.

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?