Quality assurance is a fundamental part of software engineering. It allows you to have confidence in the product being deployed into production, at the same time it ensures regression, allowing you to change the code later for refactoring or adding new features without breaking anything else.

The good thing about testing is that most of the times they are possible to automate in the scope of a continuous delivery pipeline (e.g. through Jenkins), so we don’t have to run them manually each time we need to release.

Golang Jenkins

This post is not covering automation, it introduces Golang’s testing semantics and how it’s applied to test routines and channels.

One may want to refer to my last post on a Simple Data Processing Pipeline with Golang for a full example using Golang routines and channels. The Github’s repository includes a full set of tests for the pipeline components.

Testing in Golang

Testing in Golang is no different than testing in any other language, as the basic concepts are the same. While this is a fact, it’s also true that unlike many other languages, Golang provides an excelent testing package out of the box.

Golang testing package is designed to not exit upon a fail unless explicitly instructed to do so.

It’s pretty much similar with an exam on the high school, but in this case you need to have a 100% to succeed. Once the student delivers the exam, the teacher will check for every response and note down if the answer is right or wrong. If the student fails, there’s usually another opportunity to repeat the exam and (hopefully) provide the correct responses.

Imagine how this world would be if the the teacher checked for one response at a time, give you the exam back, you study and fix the response, teacher checks, you fix it, …, teacher checks, you fix it, teacher checks, you fix it…

Not efficient delivery

You can still force the exit upon failure if there’s no point on continuing the test. In our short metaphor, if the student does not show up, the fail is immediate.

This makes sense, if we think of a test as just a set of checks, in which some of them can be prohibitive to proceed, and some do not. To demonstrate this, let’s say we want to build an indexed counter, meaning we can have a count indexed by a string. An logic-free implementation could look like the following:

type Counter struct {
	value map[string]int
}

func (counter *Counter) Increment(key string) int {
	return 0
}

func NewCounter() *Counter {
	return nil
}

One possible test for this counter could look like:

func TestCounterIncrement(t *testing.T) {
    counter := NewCounter()

    if (counter == nil) {
        t.Fatal("could not create counter instance")
        t.FailNow()
    }

    count := counter.Increment()

    if (count != 1) {
        t.Error("Expected 1, got", count)
    }

    count = counter.Increment()

    if (count != 2) {
        t.Error("Expected 2, got", count)
    }
}

Not being able to create the counter instance is a fatal error, we will not be able to proceed with any other check without it so we use t.FailNow() to indicate that the test shall be terminated immediately. If we run the test just now, we will have something like:

$ go test counter_test.go
--- FAIL: TestCounterIncrement (0.00s)
	counter_test.go:13: could not create counter instance
FAIL

Now let’s develop our counter properly, starting with the factory method.

func NewCounter() *Counter {
	return &Counter{value:0}
}

If we run the test now, it will be able to create the counter instance, and it will give you a report with all the check failures until the next failNow or the end of the test.

$ go test counter_test.go
--- FAIL: TestCounterIncrement (0.00s)
	counter_test.go:20: Expected 1, got 0
	counter_test.go:24: Expected 2, got 0
FAIL

Let’s implement the increment method and run the test again.

func (counter *Counter) Increment() int {
	counter.value++
	return counter.value
}

The result of the test would be:

$ go test counter_test.go
ok  	command-line-arguments	0.006s

Testing Routines and Channels

Testing routines and channels is a bit more complex than testing anything else in Golang, but not that much, one just needs to be fully aware of how the channel primitives work. Picking up the data pipeline example example, let’s say we have a function that reads from an input channel, executes some operation over the input and writes the result to an output channel.

In order to be able to use this function in the scope of a pipeline, we need to trigger a routine that reads the input and executes the operation, another routine that waits for the operation to be complete, and return immediately the output channel so it can be used to build the rest of the pipeline.

func Process(input <- chan string) <- chan string {
    var wg sync.WaitGroup
    wg.Add(1)

    output := make(chan string)

    go func() {
        for str := range input {
            output <- doHeavyOperation(str)
        }

        wg.Done()
    }()

    go func() {
        wg.Wait()
        close(output)
    }()

    return output
}

func doHeavyOperation(str string) string {
    return "(" + str + ")"
}

The test for the flow above will need to do the following:

  • Create an input channel
  • Write some test data into it
  • Call the Process function with the input channel as an argument
  • Read data from the output channel
  • Assert the results

Something like:

func TestProcess(t *testing.T) {
    // GIVEN
    input := make(chan string)
    defer close(input)
    input <- "hello world"

    // WHEN
    output := Process(input)

    // THEN
    expected := "(hello world)"
    found := <-output

    if found != expected {
        t.Errorf("Expected %s, found %s", expected, found)
    }
}

It would be clean and simple though the above will not work due to the blocking nature of channel operations. Writing to a channel blocks until a reader is available for reading it. Go is able to detect deadlocks at runtime, so we would end up with an error like this:

$ go test processor_test.go
fatal error: all goroutines are asleep - deadlock!

To sort this out, one could use a buffered channel, for which writing operation only blocks if the buffer is full.

input := make(chan string, 1)

As buffered channels have a different semantics, they may not suit your test scenario. If that’s the case, the other option is to ensure that the write to the channel happens in a separate routine, though that will reduce the test readability, as shown below.

func TestProcess(t *testing.T) {
    // GIVEN
    input := make(chan string)
    defer close(input)

    done := make(chan bool)
    defer close(done)

    go func() {
        input <- "hello world"
        done <- true
    }()

    // WHEN
    output := Process(input)
    <-done // blocks until the input write routine is finished

    // THEN
    expected := "(hello world)"
    found := <-output // blocks until the output has contents

    if found != expected {
        t.Errorf("Expected %s, found %s", expected, found)
    }
}

Benchmarking

Golang testing package offers a way of easily benchmarking the critical flows. In our design we isolate our heavy operation in a separate function in order to be able to benchmark it. Let’s build a benchmark test for it.

for i := 0; i < b.N; i++ {
    HeavyOperation("hello world")
}
$ go test -bench . processor_test.go
BenchmarkHeavyOperation-8       50000000                33.5 ns/op
PASS

If we tweak a bit our heavy operation just for demonstration purposes…

func HeavyOperation(str string) string {
	return fmt.Sprintf("(%s)", str)
}

… the result will be enlightening:

$ go test -bench . processor_test.go
BenchmarkHeavyOperation-8       10000000               181 ns/op
PASS

Conclusion

Golang offers a complete testing package that allows not only for validating your application functionally, but also to benchmark it in a really simple way. This post demonstrated how can we test a routing that consumes from an input channel and produces to an output channel.

The testing package works having in mind that a test is a sequence of checks and the test does not need to finish because of a check fail, though it can if instructed to do so.

If you are a Java developer and a Golang enthusiast, you might want to know that Dan’s JGoTesting library brings these testing concepts into JUnit. Feel free to take a look.

Happy testing!