DEV Community

Eternal Dev for Eternal Dev

Posted on • Originally published at eternaldev.com on

Go Testing with Test Driven Development

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

  1. Quality of code is improved
  2. Finding bugs easily as you can write tests for various scenarios
  3. Easier to implement new changes
  4. 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

  1. Add a test for the requirement
  2. Watch it fail because the code is not present for the test to pass
  3. Add the code for the test to pass
  4. Refractor your code as much as needed

TddIntro.jpg

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>
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Steps

  1. Import the testing standard package
  2. Create a function that begins with Test and has the argument from testing package
  3. Define the input and expected output
  4. Call the function with the input
  5. Compare the expected and result variables
  6. 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
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Let's go through how to convert to table driven test

  1. 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

  1. 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
Enter fullscreen mode Exit fullscreen mode

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

Discord

Discussion (0)