When you write the first lines of some library, it's difficult to introduce many bugs. But once the source code gets bigger and bigger, it becomes easier to break things. The team grows and now many people are writing the same source code, new functionality is added on top of the code that you wrote at the beginning. And code stopped working by some modification in some function that now nobody can track down.
This is a common scenario in enterprises that testing tries to reduce (it doesn't completely solve it, it's not a holy grail). When you write unit tests during your development process, you can check whether some new feature is breaking something older or whether your current new feature is achieving everything expected in the requirements.
Go has a powerful testing package that allows you also to work in a TDD environment quite easily. It is also very convenient to check the portions of your code without the need to write an entire main application that uses it.
Testing is very important in every programming language. Go creators knew it and decided to provide all libraries and packages needed for the test in the core package. You don't need any third-party library for testing or code coverage.
The package that allows for testing Go apps is called, conveniently, testing. We will create a small app that sums two numbers that we provide through the command line:
func main() {
//Atoi converts a string to an int
a, _ := strconv.Atoi(os.Args[1])
b, _ := strconv.Atoi(os.Args[2])
result := sum(a,b)
fmt.Printf("The sum of %d and %d is %d\n", a, b, result)
}
func sum(a, b int) int {
return a + b
}
Let's execute our program in the terminal to get the sum:
$ go run main.go 3 4
The sum of 3 and 4 is 7
By the way, we're using the strconv
package to convert strings to other types, in this case, to int
. The method Atoi
receives a string and returns an int
and an error
that, for simplicity, we are ignoring here (by using the underscore).
Tip
You can ignore variable returns by using the underscores if necessary, but usually, you don't want to ignore errors.
Ok, so let's write a test that checks the correct result of the sum. We're creating a new file called main_test.go
. By convention, test files are named like the files they're testing plus the _test
suffix:
func TestSum(t *testing.T) {
a := 5
b := 6
expected := 11
res := sum(a, b)
if res != expected {
t.Errorf("Our sum function doens't work, %d+%d isn't %d\n", a, b, res)
}
}
Testing in Go is used by writing methods started with the prefix Test
, a test name, and the injection of the testing.T
pointer called t
. Contrary to other languages, there are no asserts nor special syntax for testing in Go. You can use Go syntax to check for errors and you call t
with information about the error in case it fails. If the code reaches the end of the Test
function without arising errors, the function has passed the tests.
To run a test in Go, you must use the go test -v
command (-v
is to receive verbose output from the test) keyword, as following:
$ go test -v
=== RUN TestSum
--- PASS: TestSum (0.00s)
PASS
ok github.com/go-design-patterns/introduction/ex_xx_testing 0.001s
Our tests were correct. Let's see what happens if we break things on purpose and we change the expected value of the test from 11
to 10
:
$ go test
--- FAIL: TestSum (0.00s)
main_test.go:12: Our sum function doens't work, 5+6 isn't 10
FAIL
exit status 1
FAIL github.com/sayden/go-design-patterns/introduction/ex_xx_testing 0.002s
The test has failed (as we expected). The testing package provides the information you set on the test. Let's make it work again and check test coverage. Change the value of the variable expected
from 10
to 11
again and run the command go test -cover
to see code coverage:
$ go test -cover
PASS
coverage: 20.0% of statements
ok github.com/sayden/go-design-patterns/introduction/ex_xx_testing 0.001s
The -cover
options give us information about the code coverage for a given package. Unfortunately, it doesn't provide information about overall application coverage.
TDD is the acronym for Test Driven Development. It consists of writing the tests first before writing the function (instead of what we did just before when we wrote the sum
function first and then we wrote the test
function).
TDD changes the way to write code and structure code so that it can be tested (a lot of code you can find in GitHub, even code that you have probably written in the past is probably very difficult, if not impossible, to test).
So, how does it work? Let's explain this with a real life example--imagine that you are in summer and you want to be refreshed somehow. You can build a pool, fill it with cold water, and jump into it. But in TDD terms, the steps will be:
- You jump into a place where the pool will be built (you write a test that you know it will fail).
- It hurts... and you aren't cool either (yes... the test failed, as we predicted).
- You build a pool and fill it with cold water (you code the functionality).
- You jump into the pool (you repeat the point 1 test again).
- You're cold now. Awesome! Object completed (test passed).
- Go to the fridge and take a beer to the pool. Drink. Double awesomeness (refactor the code).
So let's repeat the previous example but with a multiplication. First, we will write the declaration of the function that we're going to test:
func multiply(a, b int) int {
return 0
}
Now let's write the test that will check the correctness of the previous function:
import "testing"
func TestMultiply(t *testing.T) {
a := 5
b := 6
expected := 30
res := multiply(a, b)
if res != expected {
t.Errorf("Our multiply function doens't work, %d*%d isn't %d\n", a, b, res)
}
}
And we test it through the command line:
$ go test
--- FAIL: TestMultiply (0.00s)
main_test.go:12: Our multiply function doens't work, 5+6 isn't 0
FAIL
exit status 1
FAIL github.com/sayden/go-designpatterns/introduction/ex_xx_testing/multiply
0.002s
Nice. Like in our pool example where the water wasn't there yet, our function returns an incorrect value too. So now we have a function declaration (but isn't defined yet) and the test that fails. Now we have to make the test pass by writing the function and executing the test to check:
func multiply(a, b int) int {
return a*b
}
And we execute again our testing suite. After writing our code correctly, the test should pass so we can continue to the refractoring process:
$ go test
PASS
ok github.com/sayden/go-design-patterns/introduction/ex_xx_testing/multiply
0.001s
Great! We have developed the multiply
function following TDD. Now we must refactor our code but we cannot make it more simple or readable so the loop can be considered closed.
During this book, we will write many tests that define the functionality that we want to achieve in our patterns. TDD promotes encapsulation and abstraction (just like design patterns do).