Learning the basics of Go with a test-driven development approach
Go, also known as Golang, is an open-source programming language that could match your business usecase especially if you are prioritizing built-in error-handling and speed. To increase the overall system quality and avoid defects, it’s also important to emphasize the tests for your go modules. Well, there are different approaches to Go testing but what we’d like to demonstrate is a way that’s familiar for developers in Python or Typescript using the
testify library. The first thing you’ll notice when you use the
testify library is how it is almost like
jest. Furthermore, our approach is simplified but would still follow the test-driven development (TDD) approach.
By the end of this tutorial, hopefully, the readers will gain a better understanding of go basic tests, go coverage and refactoring, and go advanced test modules. Below is the accompanying repo: https://github.com/williaminfante/go_test_starter
We’ll introduce three kinds of functions in our starter package. These functions were made specifically to introduce the go testing concepts.
TestPickAnInteger()— subtests, refactoring, coverage
TestCheckHealth()— use of more advanced testing libraries
To initially create the package, we can run
go mod init like below:
go mod init github.com/williaminfante/go_test_starter
For tutorials in creating Go modules or packages, I would suggest this tutorial link: https://golang.org/doc/tutorial/create-module
We’ll identify our main module as
package starter in the
For the tests in Go, we usually append
_test.go to their associated files. Go will then identify
*_test.go files when running tests and coverage. In this example, we’ll have
starter_test.go in the same directory as
starter.go and identify our tests with
package starter_test. The common module
testing will be handy to support automated testing and run our
go test commands later. With our simplified approach, we’ll mainly use the the
assert package in the
testify library to check equality for actual and expected values.
Basics of Go Testing
We’ll create a function,
SayHello , and by convention, it’s usually handy if we can map that function from the
package starter to test names like
We’d like the first function to accept a string name argument like
"William" that it uses to return a string
"Hello William. Welcome!". Notice how the
assert.Equal() is almost like the
expect().toEqual() from the Jest library.
The test still has a syntax error, so we’ll need to create the
SayHello() function in
starter.go to match the function signature in the test.
To run tests in Go, we can just use
go test. We can use
go test -v for a verbose version. During the initial phase of development, I prefer using
go test -v.
Cool, so it’s running, and we just need to find a way to use the input
"William" and plug it in the
SayHello() function. This is a straightforward one and we’ll use the value
%v in the argument so we’ll end up with:
You can try another name and it should still work as expected when you run
Coverage and Refactoring in Go Tests
For the next function, we’d like to test and create the function
OddOrEven() that accepts an integer and tells if it is an odd or even integer.
Initially, we wanted to check if the function accepts an integer like
45, the function returns the string
"45 is an odd number":
Like the first function we had, we’ll create a function with the same signature in
starter.go to remove the syntax error and run the test:
A quick run of
go test will provide the error:
To minimally satisfy the test requirement, we probably can just pass a string
"45 is an odd number" but let’s make it more dynamic and use the input.
This would make the test pass. But if we have another test item for an even number,
the test would fail:
We can use existing functions like the modulo
math.Mod() and just need a bit of conversion since
float64 as an input. We can see the following results to a modified function:
It satisfies the previous two items (
42), but can it satisfy
It does! Thinking about the test case, we also have negative values in integers. So, let’s try negative numbers this time like
Noticed that the test failed when we thought we almost had it. For this module, return value is
-1 instead of
1. So you can already see some parallelism to real-life use cases that could make your supposedly-very-reliable function to still potentially fail with some edge cases. So, we modified the test to:
We’ll also test that for a negative even number like
-42 and it works.
Subtests and refactoring
When we’re testing a function, there may be instances where we want to group the test items into meaningful subcategories. For example, for a more complicated function, we may want to have a subtest for the happy path, another subtest for an edge-case and maybe another subtest for error-handling.
In our example, we may want to create subgroups for non-negative cases and negative cases. Golang still allows us to do these subtests and it’s also a good way to reuse input values.
This also makes it easier to read the test cases. In our example, we don’t have common values that we can refactor for the simple usecase, but we can group the test items to easily see that the created function can work for both non-negative and negative integers like so:
It’s also easy to understand and see a summary of the test when we run
go test -v as they’re grouped together.
Speaking of creating a summary for the test, it’s also important to ensure that the conditions we mentioned the function that will actually be tested. A coverage to the desired output like
coverage.out is simply:
go test -v -coverprofile coverage.out
We can also create an
html formatted document by running a command similar to:
go tool cover -html=coverage.out -o coverage.html
Currently, we have 100% coverage!
Suppose that we did not check the even integers and only checked the odd integers, we may only get a coverage of 80% (if we include the
Just note that a line coverage of 100% does not automatically mean there are no additional tests needed. If we removed all the negative subtests, the coverage would still show 100% coverage but we have not tested the case when
math.Mod(float64(num), 2) == -1 . So yes, a 100% coverage is definitely a good goal but there’s more to line coverage that we’ll need.
Because we have followed the TDD approach, coverage will not be much of an issue for us! We are making sure that we are “covering” all the processes in our function since we created the tests first even before proceeding to create the function. We’re building the function incrementally while achieving almost 100% coverage in each step. Even during the times where we partially created the test, we have almost 100% coverage! Who wouldn’t want that, right? This is just one of the ways that we can benefit from TDD. Oh, and I also like that the tests using the TDD approach create a handy and automatically updated documentation for that package/module.
Advanced Testing Modules
For the third function in
starter.go, I intentionally did not follow the TDD approach as my main purpose here is to demonstrate how we can leverage advanced testing modules.
Now for example, we just wanted to quickly create a health check if a page could be loaded. For us to easily accomplish this task, we can make use of the
http package in golang with
import "net/http" . We can simply create a health check like:
Now, how do we plan to check this
http call? We can leverage the included testing library using
import "net/http/httptest". It’s not just important to do the line coverage here but to simulate the
http behaviour. We can create our mock writer and request using
httptest.NewRecorder(). And after running our
CheckHealth() function, we should expect that the body contains the
"health check passed" like what has been written in the
With the test module
httptest, additional relevant tests can be added. For example, we can check the
io.ReadAll() function does not cause any error returning a
nil value and that the status code is
200 (OK). More information on the error handling approach and advanced mocking will be demonstrated in a later article.
I hope that gave an overview of the basics of golang testing with some elements of the test-driven approach. With that, I am just listing here a quick rundown on what we’ve covered:
- generating automated test
- using test coverage
- using TDD elements with golang
- using specialised test packages like
A working module for the starter tests and functions is placed in here: https://github.com/williaminfante/go_test_starter
And if you’d like a quick look at the final files: