If you have any programming experience, whether that’s as a student or a professional, there’s a good chance you have heard about testing. It’s an ominpresent topic, be it on conferences, books or articles. (See what I did there?)
Also, it seems like a topic that everyone agrees on - yes, testing is good and we should do it. There are many reasons why folks consider testing good for you code. But, before we go down the rabbit hole and discuss the pros and cons of testing, let’s learn how we can test our Go code.
Of course, through actual examples that you can follow along.
What is testing?
Now, before we go on, I’ll take you on a short trip down memory lane. At this moment of my carreer I have probably written thousands (if not tens of thousands) of test cases. And I have failed quite a bit at testing, especially as a novice. So if you are new to test or just haven’t gotten it under your belt yet – worry not, I got you covered.
Before you start with testing there’s one idea I would like you to know: just like your programs are consisted of code, also your tests are code. And really, that’s something you have to remember. It’s simple - test are code.
What this testing code does is it invokes the code that powers your programs and checks if what it returns is what is expected. Quite simple, innit?
It all revolves around setting expectations and then making your program meet these expectations. Usually languages provide you with packages or libraries to test your code, with each language or library having its own conventions on what testing looks like. But really, it’s just that - meeting expectations.
That’s testing in a nutshell. Let’s move on.
What is a test?
So, what is a test then?
It’s quite simple - repeatable steps by which one can verify if something is working as it is supposed to.
What is a test in Go terms?
Quite similarly, it’s a piece of code that you can run many times and it will check if your code is working as intended.
That’s it. Shall we write one?
Testing our Go code
To write a test, we first have to have a program to test. Let’s implement a simple
function that will take a slice of
int and return its biggest number:
Pretty simple. We take a slice of
ints and return the largest. So, how can we
test that our code works as expected?
Let’s write a function that will take two arguments: a slice of
ints and the
maximum of the
ints in that slice. We will call it
TestMax function will check if the result of the
Max function call
expected result. If it does, it will simply return
otherwise it will return an informative string with what it was expecting and
what it got.
Let’s use it in our
main function will invoke the
TestMax once here, with a slice thbat
1,2,3,4 as argument and
4 as the expected maximum.
You might already be thinking that the example will pass. Let’s run it:
$ go run max.go Pass
As expected - the example passed. Let’s add two more:
Here we add two more examples, with two different pairs of arguments:
- A slice containing
3, the expected maximum
- A slice containing four zeroes, and
1as the expected maximum
If we would run it, both of these examples would fail. The reason for the
failures would be that none of the two slices of
ints that we added do not
match the expected maximum that we pass as a second argument to both of the
calls. In the second example the maximum is
4, while we expect a
3. In the
third example, the maximum is
0 while we expect
Max function can be done with just one function (
long as we can supplly an input for the function and an expected output we can
test our functions.
What is important to understand here is that testing can be very simple. Here, we are able to check if our function is working as expected without any fancy frameworks and libraries - just plain Go code. If you have any experience with testing you already know that this approach does not scale too far, but it’s very good to understand the idea that tests are just code.
Using this approach, we could technically even write our own testing framework/library. The good thing is that Go already has a testing package included in it’s standard library, so we can avoid that.
Testing with Go’s
testing package provides support
for automated testing of Go packages. It exposes a set of useful functions that
we can use to get couple of benefits: a better looking code, standardised
approach to testing and nicer looking output. Also, we eliminate the need to
create our own reporting of failed/passed tests.
Let’s see how we could test our
Max function using the
First, we need to create a
max_test.go file, which will be the test
counterpart to our
max.go (where our
Max function is defined). Most
imporantly, both of the files have to be part of the same package (in our
testing package that is imported allows for benchmarking and testing. In
our test we use the
testing.T type, which when passed to Test functions
manages the test state and formats the test logs.
TestMax function, we get the result of the
Max function and we
assign it to
actual. Then, we compare
actual to the expected result (
If the comparison fails, then we make the test fail by using the
function and we supply the error message.
What happens when we run
testing package also comes with its buddy – the
go test command.
This command automates testing the packages named by the import paths.
test recompiles each package along with any files with names matching the file
To run this file, we have to use the
go test command:
› go test PASS ok github.com/fteem/testing_in_go 0.007s
As you can see, we did not need to tell Golang which tests to run - it figured
this out on its own. This is because
go test is smartly done, with two
differen running modes.
The first mode, is called local directory mode. This mode is active whe the
command invoked with no arguments. In local directory mode,
go test will
compile the package sources and the tests found in the current directory and
then will run the resulting test binary.
After the package test finishes,
go test prints a summary line showing the
test status (
FAIL), package name, and elapsed time. This is what
we actually see in our output - our test have passed. Looking at the output
above we can also see that they passed in
0.007s. Pretty fast!
The second mode is called package list mode. This mode is activated when the
command is invoked with explicit arguments. In this mode,
go test will
compile and test each of the packages listed as arguments. If a package test
go test prints only the final ‘ok’ summary line. If a package test
fails, go test prints the full test output.
For now, we can stick with using the first mode. We will see when and how to use the second mode in one of the next articles.
Dealing with test failures
Now that we have a passing example, let’s see what tests failures look like. Let’s add another example, which we will purposely fail:
TestMaxInvalid is very similar to the test function we had before, with
having the wrong expectations being the only difference. Simply put - we know
Max will return
4 here, but we are expecting a
While we are here, let’s add one more example where we would pass an empty slice
as an argument to
Max and expect
-1 as a result:
go test again and see the output:
$ go test --- FAIL: TestMaxInvalid (0.00s) max_test.go Expected 5, got 4 --- FAIL: TestMaxEmpty (0.00s) max_test.go Expected -1, got 0 FAIL exit status 1 FAIL github.com/fteem/testing_in_go 0.009s
The two new tests failed unsurprisingly. If we inspect the output here we will
notice that there are two lines per failed tests. Both of these lines, start
--- FAIL: and have the test function name after. At the end of the line
there’s also the time it took for the test function to run.
In the second lines, we see the test file name with the line number of where the
failure occurred. More specifically, this is where in both of our test files
Let’s make our tests pass. First, we need to fix the expectation in the
TestMaxInvalid test function:
Now, when we run it we should see one less failure:
› go test --- FAIL: TestMaxEmpty (0.00s) max_test.go Expected -1, got 0 FAIL exit status 1 FAIL github.com/fteem/testing_in_go 0.006s
Good. We coud technically remove the
TestMaxInvalid as it is the same as the
TestMax function. To make the other test pass, we need to return
-1 when the
slice received as argument in
Max is empty:
len function will check the length of the
numbers slice. If it’s
it will return
-1. Let’s run the tests again:
› go test PASS ok github.com/fteem/testing_in_go 0.006s
Back to passing tests. With our new change the
Max function will return
when the slice in arguments is empty.
What we talked about in this article is about what tests are in fact. We understood that testing is useful and that test are just code - nothing more. We saw how we could test our own code without any libraries or frameworks, with just simple Golang code.
Then, we went on to explore Golang’s
testing package. We saw how an actual
test function looks like. We talked about function definitions, the
argument that we have to pass in and how to fail a test. Then we added some
more tests for our
Max function and made its tests pass.
As you can see, testing in a nutshell is a very simple but powerful technique. With a little bit of simple code we can assure that our code functions in an expected matter, that we can control. And with any new functionality added to our code, we can easily throw in another test to make sure it is covered.
Obviously, there is much more to testing that we will explore in other articles, but now that we are confident with these basic ideas and approaches we can build our knowledge on top of.
Before we stop here, please let me know in the comments what you like and dislike about testing your code? Also, what topics in testing you find confusing or challenging?