This page looks best with JavaScript enabled

Go unit tests

 ·  🎃 kr0m

Just as important as programming new features is ensuring that nothing breaks with each change. Fortunately, Go includes everything necessary to run tests quickly and conveniently.


If you are new to the world of Go, I recommend the following previous articles:


To achieve a reproducible build, Go recommends using similar versions of Go. For this, we can generate the go.mod file manually or initialize a module to have Go generate it for us:

mkdir go_testing
cd go_testing/
go mod init name

We can view the contents of the go.mod file:

cat go.mod

module name

go 1.21.12

Reproducible Build:

Including a go.mod file in your project can contribute to a reproducible build. This means that when sharing your code with
other developers or running tests in different environments, the same versions of dependencies will be used, which can help
avoid compatibility issues.

As an example, we will simply have a function called Hello that, if it receives an input string, will return [string,nil]. If it receives an empty input, it will return ["",ERROR].

vi name.go

package main

import (
    "errors"
    "fmt"
)

func hello(name string) (string, error) {
    if name == "" {
        return name, errors.New("Empty name")
    }
    return name, nil
}

func main() {
    name, error := hello("Kr0m")
    if error != nil {
        fmt.Println("ERROR:", error)
    } else {
        fmt.Println("Name:", name)
    }
}

All test files must follow the pattern XXX_test.go, the testing package must be imported, and the functions must start with Test.

We create the unit test which will check that the hello() function returns a correct value,

vi name_test.go
package main

import (
    "testing"
)

// Test hello() function with input string:
func TestHelloName(t *testing.T) {
    // Function input
    name := "Kr0m"
    // What we expect
    want := "Kr0m"
    // Save hello() returned values
    msg, err := hello(name)
    // Compare want with msg OR an error has occurred:
    if msg != want || err != nil {
        t.Fatalf(`hello(name) = %v, %v, want match for %v, nil`, msg, err, want)
    }
}

// Test hello() function with empty string:
func TestHelloEmpty(t *testing.T) {
    // Save hello() returned values
    msg, err := hello("")
    // If msg is not empty string OR err NULL:
    if msg != "" || err == nil {
        t.Fatalf(`hello("") = %v, %v, want "", error`, msg, err)
    }
}

We pass the tests without issues:

go test

PASS
ok  	name	0.002s

If we add the -cover parameter, we can see the percentage of code that our tests cover:

go test -cover

PASS
coverage: 42.9% of statements
ok  	name	0.002s

We introduce a bug, making the function always return [AAAA,nil]:

vi name.go

package main

import (
    "errors"
    "fmt"
)

func hello(name string) (string, error) {
    if name == "" {
        return name, errors.New("Empty name")
    }
    name = "AAAA"           // BUG
    return name, nil
}

func main() {
    name, error := hello("Kr0m")
    if error != nil {
        fmt.Println("ERROR:", error)
    } else {
        fmt.Println("Name:", name)
    }
}

We run the tests again:

go test

We can see that this time it failed:

--- FAIL: TestHelloName (0.00s)
    name_test.go:17: hello(name) = AAAA, <nil>, want match for Kr0m, nil
FAIL
exit status 1
FAIL	name	0.002s

Line 17 of name_test:

    if msg != want || err != nil {
        t.Fatalf(`hello(name) = %v, %v, want match for %v, nil`, msg, err, want)
    }

Indeed, want does not match msg:

name := "Kr0m"
want := "Kr0m"
// msg -> AAAA
If you liked the article, you can treat me to a RedBull here