Craftsmanship

GoConvey and Deaf Grandma


If you have used Ruby on Rails, you may be familiar with Rspec, a gem that allows us to test your Ruby code in a very intuitive way. In Go, we have GoConvey: a library that allows us to write self-documenting tests for our Go code in an easy way.

Setting up GoConvey.

To get GoConvey, we open our terminal and type the following.

go get github.com/smartystreets/goconvey

Running tests with go test or goconvey

In the Go world, running our tests is as simple as typing go test . inside the folder of our Go code. With GoConvey we can still do this, or we can run the goconvey command to start using the web UI in our browser at localhost:8080. We can even turn desktop notifications on, so every time we save our Go files, GoConvey will build and run all the tests for us, notifying us if they passed, failed or if the build failed.
GoConvery web UI

Let's begin.

We are going to solve Chris Pine's “Deaf Grandma” exercise. For this, we are going to write the test first, and then the code to make pass our tests.

Deaf Grandma description.

Whatever you say to grandma (whatever you type in), she should respond with: "Huh?! Speak up, sonny!", unless you shout it (type in all capitals). If you shout, she can hear you (or at least she thinks so) and yells back: "No, not since 1938!"

To make your program really believable, have grandma shout a different year each time; maybe any year at random between 1930 and 1950.

Start writing our code.

First, let's create our test file deaf_grandma_test.go. Notice the _test notation at the end of any test file. Now we are ready to write our first test.

package DeafGrandma

import (
    . "github.com/smartystreets/goconvey/convey"
    "testing"
)

func TestDeafGrandma(t *testing.T) {
    Convey("When I speak to my grandma", t, func() {
        speak := "hello grandma!"

        Convey("She answers with 'Huh?! Speak up, sonny!'", func() {
            So(speakWithGrandma(speak), ShouldEqual, "Huh?! Speak up, sonny!")
        })
    })
}

Let me explain the code above. First of all, we need to import the convey and testing packages, in order to write our tests.

import (
    . "github.com/smartystreets/goconvey/convey"
    "testing"
)

Secondly, we need to define a test function. Every test function has to start with the Test word and we need a *testing.T parameter.

func TestDeafGrandma(t *testing.T) {
    ...
}

Inside our test function, we start describing our different scenarios. The first scenario is when we talk to our grandma. We use the Convey function to start writing our scenario, this is similar to describe or context in Rspec.

Convey("When I speak to my grandma", t, func() {
    ...
})

Notice that when we start writing a scenario, we need to pass our *testing.T struct to create a scope in GoConvey. The last parameter is an anonymous function where we can declare variables and call the methods to test them. To make assertions, we use the So function (similar to it in Rspec), where the first parameter is the value that you get using the method to test, the second parameter is the assertion. The third parameter is optional, depending in the assertion used.

speak := "hello grandma!"

Convey("She answers with 'Huh?! Speak up, sonny!'", func() {
    So(speakWithGrandma(speak), ShouldEqual, "Huh?! Speak up, sonny!")
})

We can nest as many Convey functions as we want. But the first Convey function should have the *testing.T struct.

Going to the browser we can see that our test failed because we haven't implemented the speakWithGrandma function.

GoConvey failed build

So let's create our deaf_grandma.go file and inside of it let's implement our function.

package DeafGrandma

func speakWithGrandma(speak string) string {
    return "Huh?! Speak up, sonny!"
}

After saving the file, we will notice that our test passes. Now, let's add another test: this time, when we shout to our grandma.

Convey("When I shout to my grandma", t, func() {
    speak := "HELLO GRANDMA!"

    Convey("She answers me with 'No, not since 1938!'", func() {
        So(speakWithGrandma(speak), ShouldEqual, "No, not since 1938!")
    })
})

If we go to our browser, we will notice that our new test is failing. It's expecting No, not since 1938!, but instead it's getting Huh?! Speak up, sonny!

To fix this, first we need to import the strings package inside our deaf_grandma.go file.

import (
    "strings"
)

And we need to change a little bit our speakWithGrandma function.

func speakWithGrandma(speak string) string {
    speakUpcase := strings.ToUpper(speak)
    if speak == speakUpcase {
        return "No, not since 1938!"
    }
    return "Huh?! Speak up, sonny!"
}

By doing that, our tests should be green as grass and we are almost there! What we need to do is to implement the random year, between 1930-1950. First we need to write our test. To do this, we are going to write another test function.

func TestRandomYear(t *testing.T) {
    Convey("When running randomYear function", t, func() {
        year, _ := strconv.Atoi(randomYear())

        Convey("We get a year between 1930-1950", func() {
            So(year, ShouldBeBetween, 1930, 1950)
        })
    })
}

As you noticed, we are using strconv to convert a string to an int. So we need to import that package inside deaf_grandmae_test.go file.

import (
    . "github.com/smartystreets/goconvey/convey"
    "strconv"
    "testing"
)

Once again, our test will fail, so to make it pass, we need to implement the randomYear function. First, we need to import the appropriate packages.

import (
    "math/rand"
    "strconv"
    "strings"
    "time"
)

After that, we can implement our function.

func randomYear() string {
    s1 := rand.NewSource(time.Now().UnixNano())
    r1 := rand.New(s1)
    yearStr := "19"
    yearConcat := 30 + r1.Intn(20)
    return yearStr + strconv.Itoa(yearConcat)
}

Cool! Our tests are green. Now, to finish this simple exercise, we need to use our randomYear function inside our speakWithGrandma function to return a random year.

First, we modify our test.

Convey("When I shout to my grandma", t, func() {
    speak := "HELLO GRANDMA!"

    Convey("Her answer should contain 'No, not since'", func() {
        So(speakWithGrandma(speak), ShouldContainSubstring, "No, not since")
    })
})

Because we are going to get a random year, we can't use the ShouldEqual assertion. We need to use the ShouldContainSubstring assertion to verify that our grandma is answering us correctly, at least the firsts words. For this test, we don't need to take care of the random year.

Even though our test passes, we have to modify the speakWithGrandma one more time. Just the return line inside our if statement.

return "No, not since " + randomYear() + "!"

And that's it! A simple exercise where we used GoConvey for testing. Here is the complete code.

Reset function.

One thing that I didn't cover in this tutorial was the Reset function. When your Conveys have some set-up involved, you may need to tear down after or between tests. A Convey's Reset runs at the end of each Convey within that same scope.

Convey("Top-level", t, func() {

    // setup (run before each `Convey` at this scope):
    db.Open()
    db.Initialize()

    Convey("Test a query", func() {
        db.Query()
        // TODO: assertions here
    })

    Convey("Test inserts", func() {
        db.Insert()
        // TODO: assertions here
    })

    Reset(func() {
        // This reset is run after each `Convey` at the same scope.
        db.Close()
    })

})

GoConvey Wiki

I encourage you to visit the GoConvey Wiki where you can find even more stuff, like creating your custom assertions. Or you can dive in to the GoConvey code to get a better understanding on how GoConvey works.

Thanks for reading!

Craftsmanship
How Accurate Are Your Estimations?
Community
The Rubber Duck Effect
Craftsmanship
Can’t keep up? A simple guide to give feedback.