· Techtribe team · Tutorials · 8 min read
Introduction to TDD - Test Driven development in Go
Learn the base about what you need to know about performing Test Driven Development - TDD in Go
Introduction to TDD ( Test Driven Development )
Tabela de conteúdos
- What is TDD
- Why use TDD
- Cons TDD
- TDD Cycle - RED, GREEN, REFACTOR
- Starting off on the right foot
- Hello Victor
- Iterations: Benchmark Tests
What is TDD?
TDD stands for Test Driven Development. It consists of the development approach where we write unit tests first.
Why use TDD
Improves code quality
- Code written using TDD has higher quality.
- It has fewer errors.
- It will help you create more objective code (addressing specific problems directly).
- The process of doing TDD will automatically help you develop practices that improve your code.
- It will exponentially improve your logical approach to seeing and solving a problem.
- You will focus your efforts on solving smaller, easier-to-read code segments, helping create more robust code.
Improves your system’s design
Since we write the test before the functionality, the code written afterward is easier to check, leading to a better-developed system.
This way, we developers can achieve a more modular system that is easier to understand, maintain, expand later (scale the system), test, and refactor (as it won’t allow errors in functions created later).
Increases developer productivity
TDD increases development speed since you will spend less time debugging - think about debugging a heavy system all the time, which takes a long time to run on your machine to fix a bug.
In the initial stages of a project, it may take more time to create tests and code that will go to production. Despite this, as the project develops, adding and testing new features will go faster with less effort.
Four development teams participated in a joint study by Microsoft and IBM. The study concluded that adopting TDD reduced the number of bugs by 40 to 90%. The time required to complete the projects also increased by 15 to 35%, HOWEVER, maintenance costs decreased exponentially, compensating for this due to improved quality.
Note: This research was conducted in 2008.
Source: https://community.nasscom.in/communities/project-management/software-maintenance-step-step-guide
Reduces project costs over time
Since TDD reduces the need for code maintenance, it lowers costs over time and increases team productivity.
Fewer issues mean fewer development hours, which directly impacts project costs. Keep in mind that TDD will almost always be slightly more expensive in the initial project period, but as the project progresses, the cost-benefit ratio will improve with TDD.
Helps prevent bugs
Since running tests is the main focus of TDD, the developer can ensure the application will function as it should, needing only minor fixes later. It’s crucial to understand that in the TDD method, developers emphasize developing tests before errors occur rather than fixing them after the code has been developed.
TDD acts as documentation
Written tests can serve as documentation because when we run and read the tests, we understand the developer’s intention in implementing the functionality. Based on the tests, the developer can understand the expected input for a functionality and the desired results.
Maintains sustainability in dependent processes
With TDD, we can be confident that code dependencies will continue to function as they should. After refactoring or introducing a new feature, the tests will help you assess if everything is working correctly. Without TDD, we developers can’t track if the currently developed code is disrupting previously created functionalities.
Cons of TDD
- TDD will slow down development speed in the initial stages of the project.
- The testing part of TDD itself can become difficult to maintain. The developer will have to constantly improve or change the tests as the system’s functionalities may change over time, requiring constant adaptations to the tests.
- It’s challenging to learn the TDD method because it involves implementing the test first and then the code. This creates a significant initial discomfort.
- TDD code can sometimes be difficult to understand.
TDD Cycle - RED, GREEN, REFACTOR
The TDD process, simply put, consists of letting the compiler guide you when developing tests. Fix what the compiler reports as errors, then logically improve the scope of the tests.
Red
Write the test for a specific functionality. It should be simple, one step at a time. The test must fail.
Green
Write the minimum code necessary to make the test pass.
Refactor
After the tests pass, refactor the code to reduce redundancies and repetitions.
The process repeats…
fonte: https://www.nimblework.com/pt-br/agile/desenvolvimento-orientado-a-testes-tdd/
Starting off on the right foot
Every developer knows that whenever we start something, the sacred ritual must be performed to start off on the right foot: printing “Hello world!”
Let’s write a test that will check if we are correctly printing hello world using Golang.
Note: In Golang, every test file must end with the suffix “test”.
Note 2: In Go, every test function must have the prefix “Test” unless it’s a benchmark test.
- We will test a function to be implemented called Hello().
hello_test.go
package main
import (
"fmt"
"testing"
)
func TestHelloWorld(t *testing.T) {
t.Run("Testing Hello world", func(t *testing.T) {
got := Hello()
want := "Hello world!"
if got != want {
t.Errorf("Not blessed yet, got %q, wanted %q", got, want)
}
})
}
Running: go test -v
# testing [testing.test]
./hello_test.go:9:10: undefined: Hello
FAIL testing [build failed]
We can see that the compiler indicated the Hello function is undefined, so let’s implement it.
- Implementing the minimum code possible to pass the test.
main.go
package main
import "fmt"
func Hello() string {
helloWorld := "Hello world!"
return helloWorld
}
func main() {
fmt.Println(Hello())
}
- Vamos rodar o teste novamente: go test -v
=== RUN TestHelloWorld
=== RUN TestHelloWorld/Testing_Hello_world
--- PASS: TestHelloWorld (0.00s)
--- PASS: TestHelloWorld/Testing_Hello_world (0.00s)
PASS
ok testing 0.001s
Hello Victor
Our next step now is to specify the person we want to greet. Let’s implement this functionality.
Let’s test a Hello, Victor.
Red -> Let’s write a test that will fail. Initially, the intention is to let the compiler guide us.
hello_test.go
func TestHelloWorld(t *testing.T) {
t.Run("Testing Hello world", func(t *testing.T) {
got := Hello("Victor")
want := "Hello, Victor"
if got != want {
t.Errorf("Incorrect greeting, got %q, wanted %q", got, want)
}
})
}
- Now let’s test this:: go test -v
# testing [testing.test]
./hello_test.go:9:16: too many arguments in call to Hello
have (string)
want ()
FAIL testing [build failed]
The compiler is telling us that there are more arguments in the Hello function than expected. Let’s fix our function.
Green -> Let’s implement a simple code that passes the test.
main.go
package main
import "fmt"
func Hello(name string) string {
helloWorld := fmt.Sprintf("Hello, %s", name)
return helloWorld
}
- Vamos testar esse código:: go test -v
=== RUN TestHelloWorld
=== RUN TestHelloWorld/Testing_Hello_world
--- PASS: TestHelloWorld (0.00s)
--- PASS: TestHelloWorld/Testing_Hello_world (0.00s)
PASS
ok testing 0.002s
Iterations: Benchmark Test
A good way to test benchmarks is to check the number of iterations or operations a particular functionality in our system performs.
Fortunately, the Go standard library comes with the testing.B functionality designed for benchmark tests, so we don’t need any external libraries to test benchmarks in Go!
Let’s simulate this simply with iterations.
Red -> Let’s write a test that will fail. We will implement a test that checks if a function repeats the character V 10 times.
benchmark_test.go
package benchmark
import "testing"
func TestRepeat(t *testing.T) {
got := Repeat("v")
want := "vvvvvvvvvv"
if got != want {
t.Errorf("Expected %q but got %q", want, got)
}
}
- Testing this:: go test -v
# testing [testing.test]
./hello_test.go:19:9: undefined: Repeat
FAIL testing [build failed]
Green -> Writing the minimum code to pass the test.
In Go, there is no while or do loop. All loops are done with for, though there are different ways to use for in the language.
benchmark.go
package benchmark
func Repeat(character string) string {
var repeated string
for i := 0; i < 10; i++ {
repeated = repeated + character
}
return repeated
}
Note that: we can also store variables in Go using var. Notice also that in our loop, we don’t use any parentheses for the condition, unlike other known languages
Testing: go test -v
=== RUN TestRepeat
--- PASS: TestRepeat (0.00s)
PASS
ok testing/benchmark 0.001s
As we can see, the test passed, and unlike the previous one, it came with less information since we didn’t use t.Run() for the test.
Refactoring -> Let’s refactor this now. We will also use another operator to sum in our var variable; let’s use the += operator.
benchmark.go
package benchmark
const repeatCount = 10
func Repeat(character string) string {
var repeated string
for i := 0; i < repeatCount; i++ {
repeated += character
}
return repeated
}
- Testing again: go test -v
=== RUN TestRepeat
--- PASS: TestRepeat (0.00s)
PASS
ok testing/benchmark 0.001s
Note that: I declared the constant without specifying the type. Go inferred it automatically, but I could have inferred the type too if I wanted to:
const repeatCount int = 10
This nomenclature is also valid.
Benchmarking
Now let’s simulate a benchmark in Go. Writing benchmarks is very similar to writing normal tests in Go, and we don’t need any external libraries for this.
func BenchmarkTest(b *testing.B) {
b.Run("Testing benchmark", func(b *testing.B) {
for i := 0; i < b.N; i++ {
Repeat("v")
}
})
}
To test benchmarks we use the command: go test -bench=.
goos: linux
goarch: amd64
pkg: testing/benchmark
cpu: Intel(R) Core(TM) i7-7500U CPU @ 2.70GHz
BenchmarkTest/Testing_benchmark-4 3777550 302.8 ns/op
PASS
ok testing/benchmark 1.469