# A Gentle Introduction to Unit Testing in Go

In any well-organized codebase, most functionality is broken into small and
reusable functions, often referred to as units.

Unit testing is the practice of testing these functions with various inputs to
ensure they return the expected results in all scenarios.

This approach offers numerous advantages, including increased confidence when
refactoring, acting as documentation, and boosting overall quality by
encouraging thoughtful API design.

These tests are typically automated and integrated into continuous integration
pipelines so that they can run repeatedly without human intervention to help
prevent regressions.

In this post, you will learn to implement unit tests in Go using its built-in
testing framework. By the end of this tutorial, you will understand:

- The Go standard library's testing capabilities.
- How to run and interpret test results.
- How to resolve issues in failing tests.
- How to create and analyze code coverage reports.

Let's dive in!

[ad-logs-small]

## Prerequisites

Before proceeding with this tutorial, ensure that you've met the following
requirements:

- Basic familiarity with the Go programming language.
- A [recent version of Go](https://go.dev/doc/install) installed on your local
  machine.

## Step 1 — Setting up the demo project

To demonstrate the basics of testing in Go, I've prepared a simple provision of
a simple entitlements claim implementation, which can be found in
[this GitHub repository](https://github.com/betterstack-community/gounittests).

You can clone it to your machine by executing:

```command
git clone https://github.com/betterstack-community/gounittests.git
```

Then, navigate to the project directory and open it:

```command
cd gounittests
```

```command
code .
```

To understand what the program does, consider a web application with both free
and premium subscriptions. While both tiers have access to a "teams" feature,
free users are limited to adding a maximum of three team members, while premium
users can add up to 10.

The logic for this feature is modeled in the provided `entitlement.go` file:

```go
[label entitlement.go]
package main

import (
	"errors"
)

type Entitlement struct {
	Teams struct {
		Enabled bool    `json:"enabled,omitempty"`
		Size    Counter `json:"size,omitempty"`
	} `json:"teams,omitempty"`
}

var ErrCounterExhausted = errors.New("counter exhausted")

type Counter int64

func NewCounter(i int64) *Counter {
	c := Counter(i)
	return &c
}

func (c *Counter) Value() int64 {
	if *c == 0 {
		return 0
	}

	return int64(*c)
}

func (c *Counter) Take() error { return c.TakeN(1) }

func (c *Counter) TakeN(i int64) error {
	if *c <= 0 {
		return ErrCounterExhausted
	}

	*c -= Counter(i)

	return nil
}
```

The program above defines two custom types. The `Entitlement` struct specifies
team actions. For instance, if `Teams.Enabled` is set to `false`, the addition
of team members is blocked.

The `Counter` type is a numerical count of how many additional team members can
be added. It will have an initial value that decrements with each new addition,
stopping at 0, indicating the limit is reached.

`Counter` has three methods:

- `Value()`: Returns the current count or available spots.
- `Take()`: Decreases the count by one.
- `TakeN(i int64)`: Decreases the count by `i`, used when adding multiple
  members, like `TakeN(4)` for 4 new members.

To add a new user, call `Entitlement.Teams.Size.Take()`. For instance, if a user
initially could add 5 members, using `Take()` will reduce this number to 4.

Here's a basic example:

```go
func reduceEntitlement() error {
	e := &Entitlement{
		Teams: struct {
			Enabled bool     "json:\"enabled,omitempty\""
			Size    *Counter "json:\"size,omitempty\""
		}{
			Enabled: true,
			Size:    NewCounter(5),
		},
	}

	return e.Teams.Size.Take()
}
```

In the next section, you will write a test that verifies that the `Take()`
method functions correctly.

## Step 2 — Writing your first test

Now that you understand the program's logic, you will write some tests to
validate its accuracy. Remember that unit tests involve feeding inputs into a
program and checking the resulting output or the altered state of the program.

For our initial test, we'll start with a `Counter` that's set to five, then
invoke the `Take()` method to decrement its value. The test will then confirm
whether the resulting value is indeed four.

In Go, tests are written in files named `<name>_test.go`, where `<name>`
corresponds to the file with the target functions. The `_test.go` suffix is a
standard Go convention, enabling the compiler to overlook test files during
program compilation.

Start by creating a test file named `entitlement_test.go`:

```command
code entitlement_test.go
```

Now, proceed to pen your first test like this:

```go
[label entitlement_test.go]
package main

import (
	"testing"
)

func TestCounter_Take(t *testing.T) {
	c := NewCounter(10)

	err := c.Take()
	if err != nil {
		t.Fatalf("Could not take entitlement counter... %v", err)
	}

	if c.Value() != 9 {
		t.Fatalf("counter did not reduce. Expected 9, got %d", c.Value())
	}
}
```

The `testing` package is the backbone for all Go tests. The `T` type represents
the test's state and provides access to testing functionalities. For instance,
`t.Fatalf()` indicates a test failure and stops it immediately.

As per [the testing package documentation](https://pkg.go.dev/testing), test
functions should be named `TestXxx`, with `Xxx` being the name of the function
being tested. For example, a test for a `Multiply` function should be named
`TestMultiply`.

In this scenario, our test function is `TestCounter_Take`, where `Counter` is
the type and `Take` is the method being tested.

The function begins by creating a new `Counter` instance set to 10. After
calling `Take()`, the function checks for errors to ensure the operation didn't
fail.

Since `Take()` should decrease the counter by one, the test confirms the
counter's new value is 9. If it's not, this will indicate a flaw in the program
logic.

Next, we will explore how to execute these tests and interpret their results.

## Step 3 — Running tests in Go

Go differentiates itself from many languages by including built-in, robust
testing support through the `go test` command, eliminating the need for external
dependencies. This command works in two distinct modes:

1. **Local directory mode**: When you run `go test` without specifying package
   arguments, it operates in local directory mode. Here, Go compiles and
   executes all test files in your current working directory.

2. **Package mode**: This mode is engaged when you run `go test` with specific
   package paths. Examples include:

   - `go test .` to test the current package.
   - `go test ./...` to test the current package and all its sub-packages.
   - `go test net/http` to test the `net/http` package.

Given that we only have a single test file, we'll use the local directory mode
to run the tests.

Execute `go test` in the root directory of your project. The expected output
should resemble:

```text
[output]
PASS
ok      github.com/adelowo/unittests    0.5s
```

A useful feature of `go test` is the `-v` flag, enabling verbose mode:

```command
go test -v
```

It provides detailed insights about each test function, including their status
and execution duration:

```text
[output]
=== RUN   TestCounter_Take
--- PASS: TestCounter_Take (0.00s)
PASS
ok      github.com/adelowo/unittests    0.626s
```

This detailed output is especially helpful for identifying test performance and
failures, making it a valuable tool for debugging and optimizing your test
suite.

## Step 4 — Dealing with failing tests

Test failures can arise for various reasons, including supplying unexpected
data, incorrect logic, etc. In this section, you'll observe how Go manages
failing tests and how to debug and rectify them to achieve success.

First, update your `entitlement_test.go` file by adding a new test designed to
fail initially:

```go
[label entitlement_test.go]
func TestCounter_Take(t *testing.T) {
	. . .
}

func TestCounter_TakeN(t *testing.T) {
	c := NewCounter(2)

	err := c.TakeN(3)
	if err != nil {
		t.Fatalf("Could not take entitlement counter... %v", err)
	}

	if c.Value() != 0 {
		t.Fatalf("counter should be 0, but got %d", c.Value())
	}
}
```

Running `go test` now should yield the following error:

```text
--- FAIL: TestCounter_TakeN (0.00s)
    entitlement_test.go:29: counter did not reduce. Expected 0, got -1
FAIL
exit status 1
FAIL    github.com/adelowo/unittests    0.658s
```

This test failure highlights a bug: we attempted to decrease a `Counter` of size
two by three, which is not feasible and should not result in a negative number.

To address this, update the `TakeN()` method to prevent negative counters:

```go
[label entitlement.go]
. . .

var (
	ErrCounterExhausted = errors.New("counter exhausted")
[highlight]
	ErrNegativeCounter  = errors.New("counter cannot be negative")
[/highlight]
)

. . .

func (c *Counter) TakeN(i int64) error {
	if *c <= 0 {
		return ErrCounterExhausted
	}

    [highlight]
	if remaining := *c - Counter(i); remaining < 0 {
		return ErrNegativeCounter
	} else {
		*c = remaining
	}
    [/highlight]

	return nil
}
```

In the above code, the `ErrNegativeCounter` error was added to identify
operations that trip the `Counter` into a negative state.

After modifying the code, re-run the tests with `go test -v`. You should now
encounter a different error:

```command
go test -v
```

```text
=== RUN   TestCounter_Take
--- PASS: TestCounter_Take (0.00s)
=== RUN   TestCounter_TakeN
    entitlement_test.go:25: Could not take entitlement counter... counter cannot be negative
--- FAIL: TestCounter_TakeN (0.00s)
FAIL
exit status 1
FAIL    github.com/adelowo/unittests    1.280s
```

### Running specific tests

When fixing or adding new tests, it's often useful to run only the relevant
ones. The `-run` flag is provided to allow for test filtering based on a regular
expression.

For example, to run only the failing test, execute:

```command
go test -v -run=TestCounter_TakeN
```

Or even:

```command
go test -v -run=TakeN
```

The `TestCounter_Take` test will now be omitted from the output:

```text
[output]
=== RUN   TestCounter_TakeN
    entitlement_test.go:30: counter did not reduce. Expected 0, got 2
--- FAIL: TestCounter_TakeN (0.00s)
FAIL
exit status 1
FAIL    github.com/adelowo/unittests    0.499s
```

While this might not seem significantly impactful in this scenario, it becomes
extremely useful when dealing with large test suites comprising hundreds or even
thousands of test functions.

### Fixing the failing test

Let's now proceed to fix the test so that it passes. Modify the
`TestCounter_TakeN` test in `entitlement_test.go` to correctly handle the
negative counter error:

```go
[label entitlement_test.go]
func TestCounter_TakeN(t *testing.T) {
	c := NewCounter(2)

[highlight]
	err := c.TakeN(3)
	if !errors.Is(err, ErrNegativeCounter) {
		t.Fatalf("Expected a negative counter error, but got: %v", err)
	}
[/highlight]
}
```

Here, the test is adjusted to expect the `ErrNegativeCounter` error when
attempting to reduce the counter by more than its current value. If this
specific error is not encountered, the test will fail.

Running `go test -v` again should show all tests passing:

```text
[output]
=== RUN   TestCounter_Take
--- PASS: TestCounter_Take (0.00s)
=== RUN   TestCounter_TakeN
--- PASS: TestCounter_TakeN (0.00s)
PASS
ok      github.com/adelowo/unittests    0.001s
```

In the upcoming section, you'll learn how to ensure thorough testing by
providing multiple inputs to a test function.

## Step 5 — Utilizing table-driven tests

Currently, we have two separate test functions performing similar tasks. To
streamline and expand our testing capabilities, we'll use table-driven testing.

This approach uses a test array containing various input values and their
expected results, allowing us to test multiple scenarios within a single
function.

Replace the contents of `entitlement_test.go` with the following code:

```go
[label entitlement_test.go]
package main

import (
	"errors"
	"testing"
)

func TestCounter_Take(t *testing.T) {
	testTable := []struct {
		name          string
		startingValue int64
		expectedValue int64
		valueToRemove int64
		gotError      error
	}{
		{
			name:          "one item can be taken successfully",
			startingValue: 10,
			expectedValue: 9,
			valueToRemove: 1,
			gotError:      nil,
		},
		{
			name:          "all items can be taken successfully",
			startingValue: 10,
			expectedValue: 0,
			valueToRemove: 10,
			gotError:      nil,
		},
		{
			name:          "cannot take more items than what the counter has",
			startingValue: 10,
			expectedValue: 0,
			valueToRemove: 16,
			gotError:      ErrNegativeCounter,
		},
		{
			name:          "report counter exhausted error",
			startingValue: 0,
			expectedValue: 0,
			valueToRemove: 1,
			gotError:      ErrCounterExhausted,
		},
	}

	for _, v := range testTable {
		t.Run(v.name, func(t *testing.T) {
			c := NewCounter(v.startingValue)

			err := c.TakeN(v.valueToRemove)

			if v.gotError != nil {
				if !errors.Is(err, v.gotError) {
					t.Fatalf(
						"expected an error: %v,  but got: %v",
						v.gotError,
						err,
					)
				}

				return
			}

			if c.Value() != v.expectedValue {
				t.Fatalf("Expected %d got %d", v.expectedValue, c.Value())
			}
		})
	}
}
```

In this revised version, a `testTable` slice contains various test cases, each
with a unique set of parameters and expected outcomes.

The `name` field describes each test case, `startingValue` sets the initial
`Counter` value, `valueToRemove` defines the amount to be subtracted,
`expectedValue` denotes the expected Counter value after the operation and
`gotError` handles expected error scenarios.

Each test case in the table is executed within the `t.Run()` method, creating a
subtest for each case. This format keeps tests concise and easily readable, and
it simplifies adding new test cases.

Run these tests with go test -v, and you should receive a successful output:

```text
=== RUN   TestCounter_Take
=== RUN   TestCounter_Take/one_item_can_be_taken_successfully
=== RUN   TestCounter_Take/all_items_can_be_taken_successfully
=== RUN   TestCounter_Take/cannot_take_more_items_than_what_the_counter_has
=== RUN   TestCounter_Take/report_counter_exhausted_error
--- PASS: TestCounter_Take (0.00s)
    --- PASS: TestCounter_Take/one_item_can_be_taken_successfully (0.00s)
    --- PASS: TestCounter_Take/all_items_can_be_taken_successfully (0.00s)
    --- PASS: TestCounter_Take/cannot_take_more_items_than_what_the_counter_has (0.00s)
    --- PASS: TestCounter_Take/report_counter_exhausted_error (0.00s)
PASS
ok      github.com/adelowo/unittests    0.001s
```

Although it's a single test function, each case is treated as an individual
subtest, identifiable by the `name` field (spaces replaced with underscores).

To run a specific subtest, use the `-run` flag:

```command
go test -v -run 'TestCounter_Take/one'
```

```text
[output]
=== RUN   TestCounter_Take
=== RUN   TestCounter_Take/one_item_can_be_taken_successfully
--- PASS: TestCounter_Take (0.00s)
    --- PASS: TestCounter_Take/one_item_can_be_taken_successfully (0.00s)
PASS
ok      github.com/adelowo/unittests    0.001s
```

## Step 6 — Checking code coverage

Code coverage is a crucial metric in testing, indicating the extent to which
your code is tested. It's expressed as a percentage representing the proportion
of your codebase that your tests cover.

While aiming for high code coverage is beneficial, achieving 100% is often
unrealistic for most applications, and even 90% can be unnecessarily ambitious,
especially for larger projects.

The `go test` command provides a few different flags for reporting the code
coverage for the program under test.

### 1. Basic coverage reporting

To get a quick overview of your code coverage, use `go test -cover`. This
command gives a simple percentage output indicating the extent of code coverage:

```text
PASS
coverage: 91.7% of statements
ok      github.com/adelowo/unittests    0.001s
```

This output shows that over 90% of the `unittests` module is tested. However, it
doesn't specify which parts of your code are untested.

### 2. HTML coverage reporting

For a more detailed view, you can generate an HTML report in two steps.

First, create a coverage report file:

```command
go test -coverprofile=coverage.out
```

This command generates a `coverage.out` file and prints a summary like before:

```text
[output]
PASS
coverage: 91.7% of statements
ok      github.com/adelowo/unittests    0.001s
```

Next, convert this file into an HTML report:

```command
go tool cover -html=coverage.out
```

This command opens the report in your default web browser. The report
color-codes the lines as follows: red lines are not covered by tests, green
lines are covered, and grey lines don't count towards coverage.

![htmlcoverage.png](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/796f50e6-6b18-4d04-1784-122773ac0b00/lg1x =2722x2318)

To enhance test coverage, modify your test function as follows:

```go
[label entitlement_test.go]
func TestCounter_Take(t *testing.T) {
	 . . .

	for _, v := range testTable {
		t.Run(v.name, func(t *testing.T) {
			c := NewCounter(v.startingValue)

[highlight]
			var err error
			if v.valueToRemove == 1 {
				err = c.Take()
			} else {
				err = c.TakeN(v.valueToRemove)
			}
[/highlight]

			if v.gotError != nil {
				if !errors.Is(err, v.gotError) {
					t.Fatalf(
						"expected an error: %v,  but got: %v",
						v.gotError,
						err,
					)
				}

				return
			}

			if c.Value() != v.expectedValue {
				t.Fatalf("Expected %d got %d", v.expectedValue, c.Value())
			}
		})
	}
}
```

After these changes, re-run the coverage report:

```command
go test -cover
```

```text
[output]
PASS
coverage: 100.0% of statements
ok      github.com/adelowo/unittests    0.001s
```

This should now show 100% coverage, indicating that every statement in your code
is now being tested.
## Final thoughts

This article provided a comprehensive guide to writing, running, and debugging
your first tests in Go. It also introduced you to code analysis and coverage
metrics, which are essential for assessing the effectiveness of your tests.

While this marks the beginning of your testing journey, it sets a solid
foundation for learning advanced Go testing concepts. For further exploration,
see the [testing package documentation](https://pkg.go.dev/testing) and our
[Scaling Go](https://betterstack.com/community/guides/scaling-go/) guides.

Thanks for reading!