Introduction to unit testing
Unit testing means testing your individual pieces of code by mocking the dependencies. Mocking means you are replacing the actual implementation with something you can control. Idea is to test your individual pieces of code without worrying about the other dependencies. So when you carry out the same unit testing to all the different pieces of your code, better code quality is ensured.
Benefits of unit testing
- Quality of code is improved
- Finding bugs easily as you can write tests for various scenarios
- Easier to implement new changes
- Provides documentation of your code through tests
What is Test Driven Development (TDD)?
Test Driven Development follows the principle of converting the requirements into a test case and developing code after the test case is written. This ensures code quality is maintained throughout the project lifecycle. We can follow simple steps to perform TDD development
- Add a test for the requirement
- Watch it fail because the code is not present for the test to pass
- Add the code for the test to pass
- Refractor your code as much as needed
We have spoken a lot about the theory of how to do these things. Let's look at a practical use case of how to follow the TDD approach in Go.
Unit testing with standard package
We will use the standard testing
package of go for writing our test. You don't have to install any separate package to start with TDD in your project which is a big advantage when using Go. This means that testing is considered a very fundamental thing by the Go team.
Start by creating a new go project
go mod init <project_name>
Create a new file main.go with the following simple code to start with
package main
import "fmt"
func bubbleSort(arr []int) []int {
return arr
}
func main() {
arr := []int{4, 23, 634, 25, 32, 3}
sortedArr := bubbleSort(arr)
fmt.Println(sortedArr)
}
We are setting up our file to call the bubble sort function and passing the required input which is an array of integers. Currently, we are just returning the same variable from the function and printing the value.
Now, let's create the testing file along with this and import the standard testing package
package main
import "testing"
func TestBubbleSort(t *testing.T) {
input := []int{4, 23, 634, 25, 32, 3}
expected := []int{3, 4, 23, 25, 32, 634}
result := bubbleSort(input)
if !compare(expected, result) {
t.Fatalf(`TestBubbleSort Result - %v - Expected - %v`, result, expected)
}
}
func compare(a, b []int) bool {
if len(a) != len(b) {
return false
}
for i, v := range a {
if v != b[i] {
return false
}
}
return true
}
Steps
- Import the testing standard package
- Create a function that begins with
Test
and has the argument from testing package - Define the input and expected output
- Call the function with the input
- Compare the expected and result variables
- Fail the test if the comparison fails
Note: compare function in the above file is needed just for comparing the array of items. If your comparison is simple you can use the default comparison operators
Watch the unit test fail
We have all the steps needed to start doing TDD. Yay !!!
As a developer, you might not be happy watching your test fail, but that is what you need to get used to when working with TDD. Think about the times when you will be happy when the issue is not present in production.
go test
Output
--- FAIL: TestBubbleSort (0.00s)
main_test.go:11: TestBubbleSort Result - [4 23 634 25 32 3] - Expected - [3 4 23 25 32 634]
FAIL
exit status 1
FAIL github.com/eternaldevgames/go-projects 0.006s
Write code to pass the unit test
package main
import "fmt"
func bubbleSort(arr []int) []int {
complete := false
for !complete {
complete = true
i := 0
for i < len(arr)-1 {
if arr[i] > arr[i+1] {
temp := arr[i]
arr[i] = arr[i+1]
arr[i+1] = temp
complete = false
}
i++
}
}
return arr
}
func main() {
arr := []int{4, 23, 634, 25, 32, 3}
sortedArr := bubbleSort(arr)
fmt.Println(sortedArr)
}
The algorithm for bubble sort is out of the scope for this tutorial but if you want to learn about that please refer to the following page
https://www.geeksforgeeks.org/bubble-sort/
Run the test again and see the output
PASS
ok github.com/eternaldevgames/go-projects 0.002s
Refractor the code
Update the code to make it better. Let's condense our swapping logic to a single line instead of using the temp variable
package main
import "fmt"
func bubbleSort(arr []int) []int {
complete := false
for !complete {
complete = true
i := 0
for i < len(arr)-1 {
if arr[i] > arr[i+1] {
arr[i], arr[i+1] = arr[i+1], arr[i]
complete = false
}
i++
}
}
return arr
}
func main() {
arr := []int{4, 23, 634, 25, 32, 3}
sortedArr := bubbleSort(arr)
fmt.Println(sortedArr)
}
Table Driven Unit Test
During unit test, most often the code to call the function and check for the expected result remains same. So that's why, we can automate those and just pass the input and expected values into a function that loops through them to validate the unit test
So it becomes easy to add new test case instead of writing one more function to do the same.
package main
import "testing"
func TestBubbleSort(t *testing.T) {
tests := []struct {
name string
input []int
want []int
}{
{
name: "Simple sort",
input: []int{4, 23, 634, 25, 32, 3},
want: []int{3, 4, 23, 25, 32, 634},
},
{
name: "negative number sort",
input: []int{4, 23, 64, -25, -3, 3},
want: []int{-25, -3, 3, 4, 23, 64},
},
{
name: "negative number and equal numbers sort",
input: []int{4, 23, 23, -25, -3, 163, 3},
want: []int{-25, -3, 3, 4, 23, 23, 163},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := bubbleSort(tt.input); !compare(got, tt.want) {
t.Errorf("TestBubbleSort() = %v, want %v", got, tt.want)
}
})
}
}
func compare(a, b []int) bool {
if len(a) != len(b) {
return false
}
for i, v := range a {
if v != b[i] {
return false
}
}
return true
}
Let's go through how to convert to table driven test
- Create a
tests
struct array which will hold all the test cases. This can have all the three necessary value for the unit test
Name - Name of the test case
Input - Input array for the sort function
Expected - Expected output from thee sort function
- Loop through the tests and run each of the test and do the same comparison
go test -v
Output
=== RUN TestBubbleSort
=== RUN TestBubbleSort/Simple_sort
=== RUN TestBubbleSort/negative_number_sort
=== RUN TestBubbleSort/negative_number_and_equal_numbers_sort
--- PASS: TestBubbleSort (0.00s)
--- PASS: TestBubbleSort/Simple_sort (0.00s)
--- PASS: TestBubbleSort/negative_number_sort (0.00s)
--- PASS: TestBubbleSort/negative_number_and_equal_numbers_sort (0.00s)
PASS
ok github.com/eternaldevgames/go-projects 0.003s
Table driven unit testing makes it easy for testing multiple scenarios and it is very suitable for unit testing when you need to assert for multiple inputs
Summary
TDD approach and unit test are essential if you are building an application which you need to constantly support and add enhancements.
Approach of writing unit test is time consuming and so you need to consider that aspect as well when you are estimating your project requirements.
Let us know your thoughts on TDD and write back to us if you have any improvements.
Stay tuned by subscribing to our mailing list and joining our Discord community
Top comments (0)