Monday, October 31, 2011

gotest - unit testing and benchmarking Go programs

The Go installation that you have comes with a unit testing package called testing and a tool called go test that you can use to write unit tests. This unit testing framework, like frameworks in other languages, allow you to write any number of tests that you can run frequently to check the correctness of your code in small units. You can also write benchmark tests that you can use to check the performance of your functions.

Writing and executing unit Tests

We shall create a separate directory to create and execute our trial testing code since go test works with all files in a directory. It is convenient as all packages usually have their own directory. For the purpose of this tutorial, I am calling my directory projtst. I’ve also set the value of an environment variable $GOROOT to where the go SDK installation is - on my machine this is ~/coding/golang/go. This will be useful later on as we have to access two files in the $GOROOT/src/ folder for inclusion in our Makefile.

Into your work directory (projtst for me, but the name does not matter), create the following three files: intlib.go, intlib_test.go, Makefile.

1. intlib.go: this file contains a function that adds two integers and this is your source file.
Full program - intlib.go
package intpkg

func Add2Ints(i, j int) int {
    return i + j
}


2. intlib_test.go: this file contains the tests that will be run. Please note the following with respect to this code:
* the file name has to end with _test.go to be picked up as a set of tests by go test
* the package name has to be the same as in the source file that has to be tested
* you have to import the package testing
* all test functions should start with Test to be run as a test
* the tests will be executed in the same order that they are appear in the source
* the test function TestXxx functions take a pointer to the type testing.T. You use it to record the test status and also for logging.
* the signature of the test function should always be func TestXxx ( *testing.T). You can have any combination of alphanumeric characters and the hyphen for the Xxx part, the only constraint that it should not begin with a small alphabet, [a-z].
* a call to any of the following functions of testing.T within the test code Error, Errorf, FailNow, Fatal, FatalIf will indicate to go test that the test has failed.

Full program - intlib_test.go
package intpkg //same package name as source file

import (
    "testing" //import go package for testing related functionality
    )

func Test_Add2Ints_1(t *testing.T) { //test function starts with "Test" and takes a pointer to type testing.T
    if (Add2Ints(3, 4) != 7) { //try a unit test on function
        t.Error("Add2Ints did not work as expected.") // log error if it did not work as expected
    } else {
        t.Log("one test passed.") // log some info if you want
    }
}

func Test_Add2Ints_2(t *testing.T) { //test function starts with "Test" and takes a pointer to type testing.T
    t.Error("this is just hardcoded as an error.") //Indicate that this test failed and log the string as info
}


3. Makefile: go test requires you to have a valid Makefile that can be used to build the target.

Full program - Makefile
# include this file which comes with go installation
include ${GOROOT}/src/Make.inc 

TARG=mylibpkg

# the *_test.go files are not to be included here. Only those your would use to build the actual program.  go test will figure out the *_test.go files for itself.
GOFILES=\
 intlib.go\

# include this file which comes with go installation
include ${GOROOT}/src/Make.pkg


If you have all the three both files in place, you can run your tests by executing go test at the command line. The executable is in the bin directory of your Go SDK installation. If it is not in your PATH already, I would suggest you add it. The result of running executing go test is shown below.

rm -f _test/mylibpkg.a
rm -f _test/mylibpkg.a
gopack grc _test/mylibpkg.a _gotest_.6
--- FAIL: intpkg.Test_Add2Ints_2 (0.00 seconds)
this is just hardcoded as an error.
FAIL
exit status 1
FAIL trials/projtst 0.010s


Let us take a minute to analyze that output.
* the first 3 lines is some internal work done by the program recreate the package
* then there is the result of our hardcoded test fail in Test_Add2Ints_2
* what happened to the first test? It was executed and it passed, so that is fine. But by default go test hides all passed results. If you want to see all results, including PASSed tests, execute it with the -v option thus: go test -v.
* the last two lines is an overall status saying that not all tests passed.

Writing and executing Benchmark tests

Benchmark tests allow you to check the performance of your code. A few things to note:
* benchmark tests must have the signature func BenchmarkXxx ( *testing.B). You can have any combination of alphanumeric characters and the hyphen for the Xxx part, the only constraint that it should not begin with a small alphabet, [a-z]
* benchmark tests are not run by default by go test
* benchmark tests have to be specifically run by giving the command line option -test.bench="test_name_regex" or its equivalent -bench="test_name_regex". E.g. go test -bench=".*" to run all benchmarks tests
* go test automatically figures out how long the test has to be run to get a reasonable benchmark result
* to make that work, use the field testing.B.N within a loop to repeatedly run a certain function
* the performance is, I am assuming, from the time that the execution enters the Benchmark function until its exit
* if therefore, you are doing other tasks within the Benchmark function, like time consuming initialization, this will give you incorrect benchmark results. In these instances, stop and start the timer by calling functions testing.B.StopTimer() and testing.B.StartTimer() as appropriate.
Note: I discovered through time consuming trial and error that if any of the TestXxx functions fail, then the BenchmarkXxx tests are either not run or its results are not displayed. Either ways, no benchmark results if all the unit tests do not pass successfully. So either make sure all tests pass successfully, which of course should be the plan in any case. But if you have failed tests and want to run benchmark tests, use the -file “filename"_test.go to run the tests in just the specified file.

Add a new file shown below.
Full program - intlib_b_test.go
package intpkg //same package name as source file

import (
    "testing" //import go package for testing related functionality
    )

func Benchmark_TheAddIntsFunction(b *testing.B) { //benchmark function starts with "Benchmark" and takes a pointer to type testing.B
    for i := 0; i < b.N; i++ { //use b.N for looping 
        Add2Ints(4, 5)
    }
}

func Benchmark_TimeConsumingFunction(b *testing.B) { //benchmark function starts with "Benchmark" and takes a pointer to type testing.B
    b.StopTimer() //stop the performance timer temporarily while doing initialization

    //do any time consuming initialization functions here ... 
    //database connection, reading files, network connection, etc.

    b.StartTimer() //restart timer
    for i := 0; i < b.N; i++ {
        Add2Ints(4, 5)
    }
}


Now we want to run only the tests in this new file, so execute go test with the -file option:
go test -file intlib_b_test.go -bench=".*"

You should be able to see results similar to that shown below:
...
testing: warning: no tests to run
PASS
intpkg.Benchmark_TheAddIntsFunction 500000000 4.23 ns/op
intpkg.Benchmark_TimeConsumingFunction 500000000 4.26 ns/op


The results show that there were no regular TestXxx type of tests to run. The next two lines are the results of our benchmark tests. This one shows that the first function was run 500000000 times and each of them completed in an average time of 4.23 nanoseconds.

6 comments:

  1. It seems that the command is now called "go test" and a makefile is no longer necessary in golang 1.0

    ReplyDelete
    Replies
    1. Hey oers, I know it's been some time but I've updated it now. Thank you.

      Delete
  2. Note, "the package name has to be the same as in the source file that has to be tested"

    That's actually not entirely true. You can label the tests as _test (even if they're in the same directory as the original package) and then they run as if they're outside the package (good for black box testing, and for examples with proper package.Name usage).

    However, if you want to use unexported parts of the package, you do need the test to be in the same package.

    ReplyDelete
    Replies
    1. Sorry, that should say package_test (I used brackets which evidently got stripped out).

      Delete
  3. it seems that -file is not used in go version 1.0.3

    i use the command:
    go test -bench=".*" -v -file=intlib_b_test.go

    it still run the
    === RUN Test_Add2Ints_1
    --- PASS: Test_Add2Ints_1 (0.00 seconds)
    intlib_test.go:11: one test passed.
    === RUN Test_Add2Ints_2
    --- PASS: Test_Add2Ints_2 (0.00 seconds)

    ReplyDelete

If you think others also will find these tutorials useful, kindly "+1" it above and mention the link in your own blogs, responses, and entries on the net so that others also may reach here. Thank you.