author
Kevin Kelche

Writing Tests in Golang. (A Complete Guide)


Introduction

Writing tests is one aspect of development that is as important as writing the core code. This step is important as it ensures that the code works as expected. Tests can be of two types: unit tests and integration tests. Unit tests are tests that test a single function or method. Integration tests are tests that test the interaction between multiple functions or methods. In this article, we’ll explore how to write tests in Golang.

How to write tests in Golang

Golang has an inbuilt testing package that allows us to write tests. This package provides a framework for writing tests and running them. It also, provides a set of tools for writing tests.

To write a test in Golang, we need to create a file with the suffix _test.go based on the file we are testing. This file will contain the tests for the code in the file that it is testing. The test file will contain a function that starts with the word Test followed by the name of the function that it is testing. The function will take a pointer to a testing.T type as its only argument. The testing.T type provides a set of methods that we can use to write our tests.

main.go
package main

import "fmt"

func main() {
  fmt.Println("Hello World")
}

Copied!

main_test.go
package main

import "testing"

func TestMain(t *testing.T) {
  // Write your tests here.
}

Copied!

To run our tests, we can use the go test command. This command will run all the tests in the current directory. We can also use the -v flag to get more information about the tests that are being run.

terminal
go test -v

Copied!

Table Driven Tests

Table-driven tests are a way to write tests in Golang using a table-like format. This allows us to write tests more concisely. We can use a table to store the input and expected output for our tests. We can then loop through the table and run our tests. The tale is a slice of structs.

main_test.go
package main

import "testing"

func TestMain(t *testing.T) {
    tests := []struct{
        name string
        input string
        expected string
    }{
        {
            name: "Test 1",
            input: "Hello World",
            expected: "Hello World",
        },
        {
            name: "Test 2",
            input: "Hello World",
            expected: "Hello World",
        },
    }

    for _, test := range tests {
        t.Run(test.name, func(t *testing.T) {
            // Write your tests here.
        })
    }
}

Copied!

How to interpret test results

When we run our tests, we will get a summary of the tests that we run. The summary will tell us how many tests were run, how many tests passed, and how many tests failed. The summary will also tell us how long it took to run the tests.

Test Summary
=== RUN   TestMain
--- PASS: TestMain (0.00s)
PASS
ok      Testly  0.010s

Copied!

Mocking

Mocking is a way to test our code in isolation. We can mock a function by creating a function with the same signature as the function that we want to mock. We can then use this function to test our code. We can also use the testing package to mock functions. The testing package provides a Mock type that we can use to mock functions. The Mock type provides a Call method that we can use to mock a function. The Call method takes a function as its only argument. The function that we pass to the Call method will be called when the function that we are mocking is called.

main.go
package main

import "fmt"

func main() {
  fmt.Println("Hello World")
}

Copied!

main_test.go
package main

import (
    "testing"
    "github.com/stretchr/testify/mock"
)

type MockedFunction struct {
    mock.Mock
}

func (m *MockedFunction) MockedFunction() {
    m.Called()
}

func TestMain(t *testing.T) {
    mockedFunction := new(MockedFunction)
    mockedFunction.On("MockedFunction").Return()

    mockedFunction.MockedFunction()

    mockedFunction.AssertExpectations(t)
}

Copied!

In the above example, we are mocking the MockedFunction function. We are then calling the MockedFunction function and asserting that it was called. We can also use the On method to set the return value of the function that we are mocking.

Testing HTTP

To test HTTP requests in Golang, we can use the httptest package. The httptest package provides a NewRecorder function that we can use to create a ResponseRecorder type. The ResponseRecorder type implements the http.ResponseWriter interface. We can use the ResponseRecorder type to test our HTTP handlers. We can use the ResponseRecorder type to get the response from our HTTP handler.

main.go
package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello World")
    })

    http.ListenAndServe(":8080", nil)
}

Copied!

main_test.go
package main

import (
    "net/http"
    "net/http/httptest"
    "testing"
)

func TestMain(t *testing.T) {
    req, err := http.NewRequest("GET", "/", nil)
    if err != nil {
        t.Fatal(err)
    }

    rr := httptest.NewRecorder()

    handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello World")
    })

    handler.ServeHTTP(rr, req)

    if status := rr.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
    }

    expected := "Hello World"

    if rr.Body.String() != expected {
        t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
    }
}

Copied!

The output of the above test will be:

Output
=== RUN   TestMain
--- PASS: TestMain (0.00s)
PASS
ok      Testly  0.010s

Copied!

In the main.go file, we are creating an HTTP server that listens on port 8080. We are creating an HTTP handler that returns the string Hello World when it receives a request. We can then test the handlers.

Testing concurrency

To test concurrency in Golang, we can use the sync package. The sync package provides a WaitGroup type that we can use to wait for a group of goroutines to finish. We can use the Add method to add a goroutine to the WaitGroup type. We can then use the Wait method to wait for all the goroutines to finish. We can also use the Done method to signal that a goroutine has finished.

main.go
package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("Hello World")
    time.Sleep(5 * time.Second)
}

Copied!

main_test.go
package main

import (
    "sync"
    "testing"
    "time"
)

func TestMain(t *testing.T) {
    wg := sync.WaitGroup{}
    wg.Add(1)

    go func() {
        defer wg.Done()
        time.Sleep(5 * time.Second)
    }()

    wg.Wait()
}

Copied!

In the above example, we are creating a WaitGroup type and adding a goroutine to it. We are then waiting for the goroutine to finish. We can also use the Done method to signal that a goroutine has finished.

Testing Time

To test time in Golang, we can use the time package. The time package provides a Now function that we can use to get the current time. We can use the Add method to add a duration to the current time. We can then use the Equal method to compare the current time with the time that we want to test.

main.go
package main

import (
    "fmt"
    "time"
)

func main() {
    fmt.Println("Hello World")
}

Copied!

main_test.go
package main

import (
    "testing"
    "time"
)

func TestMain(t *testing.T) {
    now := time.Now()
    later := now.Add(5 * time.Second)

    if !later.Equal(now) {
        t.Errorf("time is not equal")
    }
}

Copied!

In the above example, we are getting the current time and adding 5 seconds to it. We are then comparing the current time with the time that we want to test. If the times are equal, the test will pass. If the times are not equal, the test will fail.

Testing Errors

To test errors in Golang, the tesing package provides an Errorf function that we can use to fail a test. We can use the Errorf function to print an error message and fail a test.

main.go
package main

import (
    "fmt"
)

func failingFunction() error {
    return fmt.Errorf("error")
}

func main() {
    fmt.Println("Hello World")
}

Copied!

main_test.go
package main

import (
    "testing"
)

func TestFailingFunction(t *testing.T) {
    err := failingFunction()

    if err != nil {
        t.Errorf("failingFunction returned an error: %v", err)
    }
}

Copied!

In the above example, we are creating a function that returns an error. We are then calling the function and checking if the function returned an error. If the function returned an error, we are failing the test.

Testing Panics

Panics are a way to handle errors in Golang by crashing the program. To test panics in Golang, we can use the recover function. The recover function can be used to recover from a panic. We can use this to test if a function panics.

main.go
package main

import (
    "fmt"
)

func funcPanics() {
    panic("panic")
}

func doesNotPanic() {
    fmt.Println("does not panic")
}

func main() {
    fmt.Println("Hello World")
}

Copied!

main_test.go
package main

import (
    "testing"
)

func TestFuncPanics(t *testing.T) {
    defer func() {
        if r := recover(); r == nil {
            t.Errorf("funcPanics did not panic")
        }
    }()

    funcPanics()
}

func TestDoesNotPanic(t *testing.T) {
    defer func() {
        if r := recover(); r == nil {
            t.Errorf("doesNotPanic panicked")
        }
    }()

    doesNotPanic()
}

Copied!

In the above example, we created two functions one that panics and one that does not panic. We are then using the recover function to test if the function panics. The first test will pass and the second test will fail.

Output
=== RUN   TestFuncPanics
--- PASS: TestFuncPanics (0.00s)
=== RUN   TestDoesNotPanic
Hello World
    main_test.go:20: doesNotPanic panicked
--- FAIL: TestDoesNotPanic (0.00s)
FAIL
exit status 1
FAIL    Testly  0.007s

Copied!

Testing Logging

To test logging in Golang, we use Logf and Log functions from the testing package. We can use the Logf function to print a formatted message. We can use the Log function to print a message.

main.go
package main

import (
    "fmt"
    "log"
)

func funcLogs() {
    log.Println("log")
}

func funcLogsF() {
    log.Printf("log %v", "formatted")
}

func main() {
    fmt.Println("Hello World")
}

Copied!

main_test.go
package main

import (
    "testing"
)

func TestFuncLogs(t *testing.T) {
    t.Logf("funcLogs: %v", "log")
}

func TestFuncLogsF(t *testing.T) {
    t.Logf("funcLogsF: %v", "log formatted")
}

Copied!

Test Benchmarks

To test benchmarks in Golang, we can use the testing package. The testing package provides a Benchmark function that we can use to run benchmarks. We can use the Benchmark function to run benchmarks for a function.

main.go
package main

import (
    "fmt"
)

func main() {
    fmt.Println("Hello World")
}

Copied!

main_test.go
package main

import (
    "testing"
)

func BenchmarkMain(b *testing.B) {
    for i := 0; i < b.N; i++ {
        main()
    }
}

Copied!

Testing benchmarks are covered in more detail in the Golang Benchmarking article.

Conclusion

In this article, we explored techniques for testing Golang code. We discussed how to test functions, variables, constants, time, errors, panics, logging, and benchmarks. Additionally, we discussed the use of the testing package to help facilitate the testing of the Golang code.

Subscribe to my newsletter

Get the latest posts delivered right to your inbox.