Advanced Golang

Core packages

Testing

In this lesson, we'll explore how to write unit tests in Go while observing best practices for testing. Go’s built-in test runner and framework seamlessly integrate with standard language tooling, allowing you to execute tests simply by running:

go test ./

This command automatically discovers and runs tests in the current directory and its subdirectories. Remember that test files should be separate from your main package code and must have filenames ending with _test.go.

Below, you’ll learn how to test a function that determines whether an integer is even.

Creating the Function to Test

Begin by creating a function called checkEven in your main package. This function accepts an integer argument and returns "YES" if the number is even; otherwise, it returns "NO".

package main

func checkEven(i int) string {
    if i%2 == 0 {
        return "YES"
    }
    return "NO"
}

func main() {
    // Application entry point; can be left empty for testing purposes.
}

Writing Unit Tests in the Same Package

To test the checkEven function, create a file named process_test.go in the same directory. In Go, test functions must start with a capital letter Test. The following example shows how to write a test case for checkEven:

package main

import "testing"

func TestCheckEven(t *testing.T) {
    i := 10
    expected := "YES"
    res := checkEven(i)
    if expected != res {
        t.Errorf("expected: %v, got: %v", expected, res)
    }
}

When you run:

go test .

the test will execute and pass if checkEven returns the correct output.

Tip

To see a failing test in action, you can intentionally set the expected string to "YESs":

package main

import "testing"

func TestCheckEven(t *testing.T) {
    i := 10
    expected := "YESs" // Intentionally incorrect for demonstration purposes
    res := checkEven(i)
    if expected != res {
        t.Errorf("expected: %v, got: %v", expected, res)
    }
}

Executing the test with the modified expectation produces output similar to:

go test .
--- FAIL: TestCheckEven (0.00s)
    process_test.go:10: expected: YESs, got: YES
FAIL
FAIL    example.com/learn    0.407s

It is crucial to always begin your test function names with a capital T. If you name a test function with a lowercase t (e.g., func testCheckEven(t *testing.T) { ... }), the test runner will not recognize it:

> go test .
ok      example.com/learn  (cached) [no tests to run]

Testing Exported Functions from a Separate Package

Another effective strategy is testing functions from an external package. You may move your code into its own package (for example, process) and then write tests that import this package.

Imagine moving your process.go file into a subdirectory named process, and updating the package name accordingly:

package process

func CheckEven(i int) string {
    if i%2 == 0 {
        return "YES"
    }
    return "NO"
}

Next, create a test file (e.g., process_test.go) with the following code:

package main

import (
    "example.com/learn/process"
    "testing"
)

func TestCheckEven(t *testing.T) {
    i := 10
    expected := "YES"
    res := process.CheckEven(i)
    if expected != res {
        t.Errorf("expected %s, got %s", expected, res)
    }
}

Running:

go test .

should produce an output like:

ok      example.com/learn  0.479s

confirming that externally exported functions are tested as a consumer would use them.

Organizing Tests in a Separate Directory

Alternatively, you can maintain a dedicated directory (such as tests) for your test code. This method is suitable when testing exported functions. After moving your test file into the tests directory, your test might look like this:

package main

import (
    "example.com/learn/process"
    "testing"
)

func TestCheckEven(t *testing.T) {
    i := 10
    expected := "YES"
    res := process.CheckEven(i)
    if expected != res {
        t.Errorf("expected: %v, got: %v", expected, res)
    }
}

Executing tests from within the tests directory with:

go test .

will produce:

ok      example.com/learn/tests  0.449s

This approach is effective for verifying only the publicly exported functions. However, note that testing internal (non-exported) functions requires placing the tests within the same package as the code.

Best Practices for Testing in Go

  • Use separate test files with the _test.go suffix.
  • Always name test functions starting with a capital Test.
  • For internal functions, write tests in the same package; for exported functions, consider using a separate test package.
  • Keep your test code modular and reflective of actual consumer usage.
  • Consolidate similar code examples to maintain clarity and conciseness in documentation.

By following these best practices, you ensure that your test code remains clear and effective, while also making your projects easier to maintain and scale.

For additional information, consider exploring Go Testing Documentation.

Watch Video

Watch video content

Practice Lab

Practice lab

Previous
Hashes and cryptography