Golabs-I,Introduction to Test Driven Development In Golang
Last updated: Apr 7, 2017
Introduction
Recently I started with Golabs. Golabs is intended to be a series of blog posts about implementing and adopting modern software development practices and disciplines in Golang.
We kick off today with a very important and invaluable discipline which is not specific to “modern” software development but is very much relevant today as well: Test Driven Development.
What?
I will not drone about what Test Driven Development is and the topic has been covered by great luminaries.In short what TDD means
Write code that lays out expectations to how your code should behave.This expectation “artifact” then becomes your insurance against the only permanent thing in software:change.
How?
TDD typically involves the following cycle as mentioned in Martin Fowler’s post :
1.Write a test for the next bit of functionality you want to add.
2.Write the functional code until the test passes.
3.Refactor both new and old code to make it well structured.
4.Repeat
Another variation for developing a feature from scratch is as follows:
1.Before even a single line of code is written, jot down a collection of expectations (i.e. Functional behavior) of the code also called as a Test Suite.This typically can be done with a business owner/business analyst.
2.Write out the skeletal structure of the functional code that intends to do the job as described in the above point.
3.At this point you would have a failing test suite as the functional expectations are not fulfilled by the code (In fact we still haven’t written any functional code yet)
4.Now start writing functional code that meets the expectations of the suite or in other words the tests pass.
5.At a point when all the expectations laid down in #1 are met or when the test suite passes, you have a version one of the feature that can be potentially be shipped.
6.If during the lifecycle of a software if a bug is reported, give evidence about existence of the bug by writing a failing test case. What the failing test case does is it augments the expectations of a code by uncovering a scenario or a flow that was hitherto ignored or overlooked.Once a failing test case is in place, write code to make the test pass (also called as fixing the bug).In doing so we are able to enhance our code with bug fixes and have a iron clad mechanism in place that will prevent regression issues.
Test Driven Development in Golang
Golang ships with an inbuilt package called testing. Golang’s documentation is near perfect and lucid.I wont try to simplify it any further but just run the highlight reel.
1.Golang’s testing package comes armed with teeth to enable you to write automated tests on your code.
2.As you know Go code is shipped in packages. A package can have multiple files(of course logically they have to be logically cohesive).The tests for the different .go files in the package reside in the same package.
3.The convention for naming a test file is filename_test.go (where filename.go is the functional code)
4.Name of the tests begin have the format TestXxx where the suffix Xxx is in wordcase and typically describes the expected behaviour of the code. The signature of a test function is
func TestCanDoSomethingReallyNice(t *testing.T)
(t is the object pointer to the T struct defined in the testing package.Check the source code for more information on how it is structured.)
T exposes nice methods like Fail(),Skip(),Log() to assist you in testing your code.
5.The tests are executed by the go test command.
6.the testing package has mechanisms to parallelise test execution and benchmark the code for gauging performances.(We will explore this later in upcoming Golabs)
7.Start testing out the Public APIs(functions in a package which are exposed to the outside world) of a package and don’t bother with the internals initially.There are a few techniques to test out the internal/private functions of a package if and when such a need arises.(We shall explore this in upcoming Golabs as this is an advanced topic).
8.Also the tests should test out one scenario at a time and not have side effects for reproducible and dependable tests.
9.TDD in Golang makes you the first user of your package and helps you to design it better in keeping it simple.
10.Eliminate boilerplate by refactoring tests into a table driven structure.
Real world Example
Now that we have absorbed the highlights we will try to see it in action with a real world example(simplified to a great extent and some code). We will take the scenic route and try to correspond every commit with the steps mentioned in the variation 2 of the TDD
The code is pushed to a public repository here.
Example:
Lets assume that we decided to implement a calculator package with basic operations viz. Addition,Subtraction,Multiplication and Division.
1.At the start, a business analyst and we will sit down together and spec what the system should do.The BA might do it in JIRA or a PM tool.We would do the same thing in Go.
We create a package named calculator and create 2 files in that namely calculator.go which is an empty file and calculator_test.go which will hold the testing code.
The contents of the calculator_test.go are as follows
Listing 1
package calculator
import "testing"
func TestCanAdd2Numbers(t *testing.T) {
}
func TestCanSubtract2Numbers(t *testing.T) {
}
func TestCanMultiple2Numbers(t *testing.T) {
}
func TestCanDivide2Numbers(t *testing.T) {
}
Looks pretty sorted?At this point in time we raise a Pull Request for review.This will be merged the feature integration branch without a problem.
2.Once all such expectations are in place, we will now move onto the implementation part of the simplest of the operations:Addition.
First, we write a simplest expectation i.e Addition of 5 and 6 should yield 11 in the calculator_test.go. Please note that we are changing the content of the TestCanAdd2Numbers() that we defined in #1
Listing 2
func TestCanAdd2Numbers(t \*testing.T) {
result := Add(5, 6)
if result != 11 {
t.Log("Addition of 5 and 6 should yield 11 but yielded", result)
t.Fail()
}
}
Note how we use the t.Log() and t.Fail() functions to log a error message and fail the test as shown in the output below.
Once this is done, we write a skeletal structure of the Addition function in calculator.go file.
Listing 3
package calculator
//Add performs the addition of two numbers
func Add(a, b int) int {
return 0
}
At this point when we run go test we see that the test fails with the following output:
Listing 4
FAIL: TestCanAdd2Numbers (0.00s)
calculator_test.go:37: Addition of 5 and 6 should yield 11 but yielded 0
FAIL
exit status 1
FAIL github.com/kaddiya/golabs-tdd/calculator 0.005s
D’oh!This is expected as our Add(a,b int) returns 0 and not a+b
We now raise this as a PR to our feature-integration branch.The PR will submit a failing test case for addition.
3.We now change the Add function to below
Listing 5
//Add performs the addition of two numbers
func Add(a, b int) int {
return a + b
}
Running the go test yields the output like :
Listing 6
PASS
ok github.com/kaddiya/golabs-tdd/calculator 0.005s
Yep.The addition is behaving as expected.We now raise a PR to the feature integration branch.This PR will pass the addition tests and deem the addition function to be feature complete
4.We will do steps #2-#3 for all the remaining operations.(Subtraction,Multiplication and Division) and raise a PR for the same. This PR will Submit feature completion of all the calculator operations
Also worth noting that in this changeset, we have refactored the assertion and the logging code into a single private function logAndAssert(expected, actual int, message string, t *testing.T) which does the assertion, logs and fails the test if the expectations are not met.
5.At this point we are confident on the package stability and we release the same.
6.People will start using the package.Every thing is all right till some one uses the Divide(a,b int) to perform a division by 0 and they run into a error like this:
Listing 7
panic: runtime error: integer divide by zero
goroutine 1 [running]:
panic(0x59460, 0xc42000a0d0)
/usr/local/go/src/runtime/panic.go:500 +0x1a1
main.main()
/Users/Webonise/projects/codebases/go-workspace/src/github.com/kaddiya/golabs-tdd/main.go:8 +0x14
exit status 2
Well yeah, we were naive enough not to have a foresight that any number divded by 0 gives infinity and computers cant process infinity.Thats why the code barfs and throws a runtime error.Consumers of this package will have difficulty to handle this runtime error and panic and it is upto us to handle this elegantly by programming by defense This is essentially a bug and needs to be fixed soon enough.
7.While fixing the bug, we follow the protocol of submitting a failing test case.We write the test as follows:
Listing 8
func TestDivisionBy0ShouldNotBeAllowed(t *testing.T) {
_, err := Divide(10, 0)
if err == nil || "Divisor cant be equal to 0" != err.Error() {
t.Log("Incorrect error message found for division by 0")
t.Fail()
}
}
If you notice we have fiddled around the API a bit and changed the Divide(a,b int) to return an int on successful execution and an error when something goes wrong.If this was a widely used API,we would need to handle the release management effectively.Better yet, having a foresight and design an API which is future proof by making a provision of returning an error as an extra parameter will mitigate such backward compatibility issues.The Divide(a,b int) function now looks like this
Listing 9
//Divide performs the division of two numbers
func Divide(a, b int) (int, error) {
return a / b, nil
}
Running go test will give the following output
Listing 10
FAIL: TestDivisionBy0ShouldNotBeAllowed (0.00s)
panic: runtime error: integer divide by zero [recovered]
panic: runtime error: integer divide by zero
goroutine 7 [running]:
.
.
.
exit status 2
FAIL github.com/kaddiya/golabs-tdd/calculator 0.009s
We will now try to fix the bug where division by 0 is allowed.The Divide(a,b int) function now looks like this:
Listing 11
//Divide performs the division of two numbers
func Divide(a, b int) (int, error) {
if b == 0 {
return 0, errors.New("Divisor cant be equal to 0")
}
return a / b, nil
}
Running go test will give the following output:
Listing 12
PASS
ok github.com/kaddiya/golabs-tdd/calculator 0.005s
At this point of time it can be seen that the bug is fixed and the tests pass.The Divide(a,b int) now returns an error for invalid divisor input like 0 so that the caller can handle it properly.
We even have an assertion on the error message so that if we inadvertently change the message(“Divisor cant be equal to 0”),our kind hearted consumers wont bear the brunt of it.
We now are ready to submit a fix for this bug as a PR.This PR will fix the bug where division by 0 was giving a runtime error
Our package is now more robust and bug free than before and we have adopted “Test Driven Development” in Golang
EDIT 1
As you can see in the above tests there are 2 tests for the Divide() function for different flows.In this simplified example our sample inputs and outputs are covering only few scenarios.In the real world there will be many such inputs and corresponding outputs for a function under test.In order to achieve this,we would need to invoke the function in multiple tests which will give rise to a lot of boilerplate code.And no one wants to write boring repetitive code(or actually copy-paste).
To avoid this there is a trick called Table Driven Tests.It is not specific to Go but can be implemented really elegantly in Go.This section will aim to describe that
1.To do this we create an array of structs named divideTests.Each struct will have 6 fields viz
dividend, divisor, result, message and error.
Lets look at this as a tuple.Each tuple will have the inputs(dividend and divisor) and outputs(result and error) and a field name message that is to be be displayed on test failure.Each tuple like this will distinctly represent a scenario under test.We can initialise them by using the typical Go syntax of inline initialisation.
Listing 13
var divideTests = []struct {
dividend int
divisor int
result int
message string
err error
}{
{90, 9, 10, "Division of 90 by 9 doesnt not yield 10", nil},
{12, 4, 3, "Division of 12 by 4 doesnt not yield 3", nil},
{45, 7, 6, "Division of 45 by 7 doesnt not yield a round number 6", nil},
{0, 12, 0, "Division of 0 by 12 should give 0 and a nil error", nil},
{45, 0, 0, "Divisor by 0 should not be allowed", errors.New("Divisor cant be equal to 0")},
{0, 0, 0, "Divisor by 0 should not be allowed", errors.New("Divisor cant be equal to 0")},
}
The eagle eyed will notice that the scenarios tested by TestCanDivide2Numbers() and TestDivisionBy0ShouldNotBeAllowed() are included in the 1st and 5th fixture.In addition to them we have added some other fixtures as well like:
- dividing when there will be some remainder and the quotient is floored/ceiled ,
- division of 0 by any other number ,
- division of 0 by 0
2.We can execute the tests as follows:
Listing 14
func TestDivide(t *testing.T) {
for _, tuple := range divideTests {
actual, err := Divide(tuple.dividend, tuple.divisor)
if err != nil {
if !reflect.DeepEqual(err, tuple.err) {
t.Fail()
t.Log("Error mismatch")
}
}
logAndAssert(tuple.result, actual, tuple.message, t)
}
}
Once this is done we can safely remove the two redundant methods and run go test, we get the following output
Listing 15
PASS
ok github.com/kaddiya/golabs-tdd/calculator 0.005s
We see that our tests are passing and the coverage is as before.In doing so we have refactored our tests to eliminate boilerplate and have setup a variety of tests elegantly.You will see this pattern being extensively used in Go and is seriously slick!
As always, we submit a PR to refactor the division tests into a table driven structure
Note This is an overly simplified example.Most modern languages have the ability to detect such obvious flaws and recovery mechanism is baked in the runtime itself.For the sake of simplicity of the example and the primary focus being on TDD in Golang, I have taken some liberties with the assumptions that Go runtime cant handle division by 0
The code listings do have links to the respective PRs in the repository on Github for better understanding.I recommend the audience cross reads the content with the code.
Conclusion
We have seen through a highly simplified example how we can implement and adopt TDD in Go.The opinionated nature of Go and the inbuilt testing package makes the test setup different from say a JVM tool like Spock.But once understood, testing is a breeze in Go.
This blog entry overlooks the other advanced parts of automated testing like Setup and Teardown ,Test Doubles, Internals testing etc.
I will definitely cover it in upcoming Golabs.
Hope you got a kickstart about TDD in Golang with this post.Please let me know your feedback in the comments sections below.
Cheers!