# Testing in Go: Intermediate Tips and Techniques

In the [previous article](https://betterstack.com/community/guides/testing/unit-testing-in-go/), I introduced the basics of
testing in Go, covering the standard library's testing capabilities, how to run
tests and interpret results, and how to generate and view code coverage reports.

While those techniques are a great starting point, real-world code often demands
more sophisticated testing strategies. You might face challenges like slow
execution, managing dependencies, and making test results easily understandable.

In this article, we'll dive into intermediate Go testing techniques that address
these issues by focusing on:

- Handling dependencies and reusing code in multiple test functions,
- Speeding up your test runs,
- Making the test output easy to read.

Ready to improve your Go testing skills? Let's dive in!

## 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.
- Familiarity with [basic unit testing concepts in Go](https://betterstack.com/community/guides/testing/unit-testing-in-go/).
- A recent version of [Docker](https://docs.docker.com/engine/install/)
  installed on your system.

## Step 1 — Setting up the demo project

To demonstrate the various techniques I'll be introducing in this article, I've
created a
[GitHub repository](https://github.com/betterstack-community/intermediate-go-unit-tests)
which you can clone and work with on your local machine. We'll be testing a
simple function that takes a JSON string and pretty prints it to make it more
human-readable.

You can clone the repo to your machine by executing:

```command
git clone https://github.com/betterstack-community/intermediate-go-unit-tests
```

Then, navigate to the project directory and open it in your preferred text
editor:

```command
cd intermediate-go-unit-tests
```

```command
code .
```

In the next section, we'll start by understanding test fixtures in Go.

## Step 2 — Understanding test fixtures in Go

When writing certain tests, you may need additional data to support the test
cases, and to enable consistent and repeatable testing. These are called test
fixtures, and it's a standard practice to place them within a `testdata`
directory alongside your test files.

For instance, consider a simple package designed to format JSON data. Testing
this package will involve using fixtures to ensure the formatter consistently
produces the correct output. These fixtures might include various files
containing JSON strings formatted differently.

The `fixtures` package in the demo project exports a single function which
formats a JSON string passed to it. The implementation of this function is
straightforward:

```go
[label fixtures/code.go]
package fixtures

import (
	"bytes"
	"encoding/json"
)

func PrettyPrintJSON(str string) (string, error) {
	var b bytes.Buffer
	if err := json.Indent(&b, []byte(str), "", "    "); err != nil {
		return "", err
	}
	return b.String(), nil
}
```

The next step involves be setting up the fixtures in `fixtures/testdata`
directory. We'll utilize two fixture files:

- `invalid.json`: This contains an invalid JSON object to test how the
  `PrettyPrintJSON()` function handles errors.

```json
[label fixtures/testdata/invalid.json]
{
  name: "John Doe",
  age: 30,
  isEmployed: true,
  phoneNumbers: [
    123-456-7890,
    987-654-3210
  ]
}
```

- `valid.json`: Contains a valid JSON object that is not well formatted.

```json
[label fixtures/testdata/valid.json]
{
  "name": "John Doe",
                   "age": 30, "isEmployed": true,
						"phoneNumbers": [
    "123-456-7890",
    "987-654-3210"
  ]
}
```

Since both JSON files are already set up, let's go ahead and use them in the
unit tests for the function. To do this, open up the `fixtures/code_test.go`
file in your editor and populate it as follows:

```command
code fixtures/code_test.go
```

```go
[label fixtures/code_test.go]
package fixtures

import (
	"bytes"
	"io"
	"os"
	"testing"
)

func TestPrettyPrintJSON(t *testing.T) {
	tt := []struct {
		name     string
		filePath string
		hasErr   bool
	}{
		{
			name:     "Invalid json",
			filePath: "testdata/invalid.json",
			hasErr:   true,
		},
		{
			name:     "valid json",
			filePath: "testdata/valid.json",
			hasErr:   false,
		},
	}

	for _, v := range tt {
		f, err := os.Open(v.filePath)
		if err != nil {
			t.Fatal(err)
		}

		defer func() {
			if err := f.Close(); err != nil {
				t.Fatal(err)
			}
		}()

		b := new(bytes.Buffer)
		_, err = io.Copy(b, f)
		if err != nil {
			t.Fatal(err)
		}

		_, err = PrettyPrintJSON(b.String())
		if v.hasErr {
			if err == nil {
				t.Fatal("Expected an error but got nil")
			}
			continue
		}

		if err != nil {
			t.Fatal(err)
		}
	}
}
```

The `TestPrettyPrintJSON()` function checks the normal operation and error
handling behavior of the `PrettyPrintJSON()` function by attempting to parse
both correctly formatted and malformed JSON files.

For each case in the test table (`tt`), the JSON file specified in `filePath` is
opened and its contents are read into a buffer, which is then subsequently
passed into the `PrettyPrintJSON()` function.

The outcome is then evaluated based on the `hasErr` field. If an error is
expected, and `PrettyPrintJSON` does not return an error, the test fails because
it indicates a failure in the function's error-handling logic. Conversely, if an
error occurs when none is expected, the test also fails.

Running the test is as simple as using the Go command below:

```command
go test ./... -v -run=PrettyPrintJSON
```

```text
[output]
=== RUN   TestPrettyPrintJSON
--- PASS: TestPrettyPrintJSON (0.00s)
PASS
ok  	github.com/betterstack-community/intermediate-go-unit-tests/fixtures
```

With these fixtures, you can always be sure that the `PrettyPrintJSON()`
function can parse any JSON files thrown at it and report errors when there are
parsing failures.

In the next section, you will verify the format of the prettified JSON.

## Step 3 — Working with golden files

Testing often involves asserting that the output from a function matches an
expected result. This becomes challenging with complex outputs, such as long
HTML strings, intricate JSON responses, or even binary data. To address this,
we'll use golden files.

A golden file stores the expected output for a test, allowing future tests to
assert against it. This helps with detecting unexpected changes in the output,
usually a sign of a bug in the program.

In the previous section, we used test fixtures to provide raw JSON data for
formatting. Now, we'll enhance our testing approach by using a golden file to
ensure that the formatted output from the `PrettyPrintJSON` function remains
consistent over time.

You can go ahead and add the highlighted content below to the `code_test.go`
file:

```go
[label fixtures/code_test.go]
package fixtures

import (
	"bytes"
[highlight]
	"encoding/json"
[/highlight]
	"io"
	"os"
	"testing"

[highlight]
	"github.com/sebdah/goldie/v2"
[/highlight]
)

[highlight]
func verifyMatch(t *testing.T, v interface{}) {
	g := goldie.New(t, goldie.WithFixtureDir("./testdata/golden"))

	b := new(bytes.Buffer)

	err := json.NewEncoder(b).Encode(v)
	if err != nil {
		t.Fatal(err)
	}
	g.Assert(t, t.Name(), b.Bytes())
}
[/highlight]

func TestPrettyPrintJSON(t *testing.T) {
    . . .

	for _, v := range tt {
        . . .
    [highlight]
        formattedJSON, err := PrettyPrintJSON(b.String())
    [/highlight]
        if v.hasErr {
            if err == nil {
                t.Fatal("Expected an error but got nil")
            }
            continue
        }

        if err != nil {
            t.Fatal(err)
        }

    [highlight]
		verifyMatch(t, formattedJSON)
    [/highlight]
	}
}
```

The [goldie package](https://github.com/sebdah/goldie) is a Go testing utility
that does the following:

- Automatically creates a golden file with the expected output of the function
  under test if it doesn't exist.
- Asserts that the current test output matches the contents of the golden file.
- Optionally modifies the golden file with updated data when the `-update` flag
  is used with the `go test` command.

Ensure to download the package with the command below before proceeding:

```command
go get github.com/sebdah/goldie/v2
```

The `verifyMatch()` function uses the `goldie` package to assert against the
formatted JSON output produced by the `PrettyPrintJSON()` function, but it will
fail initially because there's no golden file present at the moment:

```command
go test ./... -v -run=TestPrettyPrintJSON
```

```text
[output]
=== RUN   TestPrettyPrintJSON
    code_test.go:22: Golden fixture not found. Try running with -update flag.
--- FAIL: TestPrettyPrintJSON (0.00s)
FAIL
FAIL	github.com/betterstack-community/intermediate-go-unit-tests/fixtures	0.002s
FAIL
```

To fix this, you need to include the `-update` flag to create the golden file
for this specific test:

```command
go test ./... -update -v -run=TestPrettyPrintJSON
```

This creates the golden file in
`fixtures/testdata/golden/TestPrettyPrintJSON.golden`, so the test passes:

```text
[output]
=== RUN   TestPrettyPrintJSON
--- PASS: TestPrettyPrintJSON (0.00s)
PASS
ok      github.com/betterstack-community/intermediate-go-unit-tests/fixtures    0.002s
```

Examine the contents of the golden file in your text editor:

```text
[label fixtures/testdata/golden/TestPrettyPrintJSON.golden]
"{\n    \"name\": \"John Doe\",\n    \"age\": 30,\n    \"isEmployed\": true,\n    \"phoneNumbers\": [\n        \"123-456-7890\",\n        \"987-654-3210\"\n    ]\n}\n"
```

Any time you use the use the `-update` flag, the contents of the golden file for
the corresponding test will be created or updated in the `testdata/golden`
directory as shown above.

Before committing your changes, ensure that the contents of the file meets your
expectations as that is what future test runs (without using `-update`) will be
compared against.

It's also important to point out a few things:

- Only use the `-update` flag locally. Your CI server should not be using the
  `-update` flag.
- Always commit the golden files to your repository to make them available to
  your teammates and in your CI/CD pipelines.
- Never use the `-update` flag unless you want to update the expected output of
  the function under test.

With that said, let's now move on to the next section where you'll learn about
test helpers in Go.

## Step 4 — Using test helpers

Just like production code, test code should be maintainable and readable. A
hallmark of well-crafted code is its modular structure, achieved by breaking
down complex tasks into smaller, manageable functions. This principle holds true
in test environments as well, where these smaller, purpose-specific functions
are known as test helpers.

Test helpers not only streamline code by abstracting repetitive tasks but also
enhance re-usability. For instance, if several tests require the same object
configuration or database connection setup, it's inefficient and error-prone to
duplicate this setup code across multiple tests.

To illustrate the benefit of test helpers, let's update the `verifyMatch()`
function introduced earlier. To designate a function as a test helper in Go, use
`t.Helper()`. This call is best placed at the beginning of the function to
ensure that any errors are reported in the context of the test that invoked the
helper, rather than within the helper function itself.

```go
[label fixtures/code_test.go]
. . .
func verifyMatch(t *testing.T, v interface{}) {
[highlight]
	t.Helper()
[/highlight]

	g := goldie.New(t, goldie.WithFixtureDir("./testdata/golden"))

	b := new(bytes.Buffer)

	err := json.NewEncoder(b).Encode(v)
	if err != nil {
		t.Fatal(err)
	}
	g.Assert(t, t.Name(), b.Bytes())
}
. . .
```

Debugging can become more challenging without marking the function with
`t.Helper()`. When a test fails, Go's testing framework will report the error
location within the helper function itself, not at the point where the helper
was called. This can obscure which test case failed, especially when multiple
test functions use the same helper.

To demonstrate this, remove the `t.Helper()` line you just added above, then
delete the entire `golden` directory within `fixtures/testdata` like this:

```command
rm -r fixtures/testdata/golden
```

When you execute the tests now, it should fail once again with the following
error:

```command
go test ./... -v -run=TestPrettyPrintJSON
```

```text
[output]
=== RUN   TestPrettyPrintJSON
    code_test.go:22: Golden fixture not found. Try running with -update flag.
--- FAIL: TestPrettyPrintJSON (0.00s)
FAIL
FAIL	github.com/betterstack-community/intermediate-go-unit-tests/fixtures	0.002s
FAIL
```

The failure is reported to have occurred on line 22 of the `code_test.go` file,
which is the highlighted line below:

```go
[label fixtures/code_test.go]
func verifyMatch(t *testing.T, v interface{}) {
	g := goldie.New(t, goldie.WithFixtureDir("./testdata/golden"))

	b := new(bytes.Buffer)

	err := json.NewEncoder(b).Encode(v)
	if err != nil {
		t.Fatal(err)
	}
[highlight]
	g.Assert(t, t.Name(), b.Bytes())
[/highlight]
}
```

However, when you add the `t.Helper()` line back in, you get the same failure
but the reported line is different. Now it says `code_test:74` which directly
points to the invoking test:

```text
[output]
=== RUN   TestPrettyPrintJSON
    code_test.go:75: Golden fixture not found. Try running with -update flag.
--- FAIL: TestPrettyPrintJSON (0.00s)
FAIL
FAIL    github.com/betterstack-community/intermediate-go-unit-tests/fixtures    0.002s
FAIL
```

Ensure to fix the test failure with the `-update` flag once again as
demonstrated in Step 3 above before proceeding to the next section.

## Step 5 — Setting up and tearing down test cases

Testing often involves initializing resources or configuring dependencies before
executing the tests. This setup could range from creating databases and tables
to seeding data, especially when testing database interactions like with a
PostgreSQL database.

Implementing setup and teardown routines is essential to streamline this process
and avoid repetition across tests. For example, if you want to test your
PostgreSQL database implementation, several preparatory steps are necessary such
as:

1. Creating a new database
2. Creating the tables in the database
3. Optionally, add data to the tables

While the steps above can be easily achieved, it becomes a big pile of
repetition when you have to write multiple tests that have to do each step
repeatedly. This is where implementing setup and teardown logic makes sense.

To demonstrate this, we'll implement a CRUD system where you can fetch a user
and add a new user to the database. To do this, you need to create a few new
directories:

- `postgres`: Contains the CRUD application code interacting with the PostgreSQL
  database.
- `postgres/testdata/migrations`: Stores the SQL files for setting up database
  tables and indexes.
- `postgres/testdata/fixtures`: Contains sample data to preload into the
  database.

```command
mkdir -p postgres postgres/migrations postgres/testdata/fixtures
```

Go ahead and create the necessary files in the `postgres` directory:

```command
touch postgres/user.go
```

```command
touch postgres/user_test.go
```

Open the `user.go` file, and enter the following code:

```go
[label postgres/user.go]
package postgres

import (
	"context"
	"database/sql"

	"github.com/google/uuid"
	_ "github.com/lib/pq"
)

type User struct {
	ID       uuid.UUID
	Email    string
	FullName string
}

type userRepo struct {
	inner *sql.DB
}

func NewUserRepository(db *sql.DB) *userRepo {
	return &userRepo{inner: db}
}

func (u *userRepo) Get(ctx context.Context, email string) (*User, error) {
	sqlStatement := `SELECT id, email, full_name FROM users WHERE email=$1;`
	user := new(User)
	row := u.inner.QueryRow(sqlStatement, email)
	return user, row.Scan(&user.ID, &user.Email, &user.FullName)
}

func (u *userRepo) Create(ctx context.Context, user *User) error {
	sqlStatement := `INSERT INTO users (email, full_name) VALUES ($1, $2)`
	_, err := u.inner.Exec(sqlStatement, user.Email, user.FullName)
	return err
}
```

In the above code, there are two main functions:

- `Get()`: This method retrieves a user from the database through their email
  address.
- `Create()`: This method creates a new user in the database.

Before we can write the corresponding tests, let's create the migration files
that will contain the logic to set up the database tables and also make sense of
the data we want to load the database with.

To do that, you need to create a few more files through the commands below:

```command
touch postgres/testdata/fixtures/users.yml
```

```command
touch postgres/migrations/001_create_users_tables.up.sql
```

```command
touch postgres/migrations/001_create_users_tables.down.sql
```

In the `fixtures/users.yml` file, add a list of a few sample users to populate
the database:

```yml
[label postgres/testdata/fixtures/users.yml]
---
- id: b35ac310-9fa2-40e1-be39-553b07d6235b
  email: john.doe@gmail.com
  full_name: John Doe
  created_at: '2024-01-20 14:26:13.237292+00'
  updated_at: '2024-01-20 14:26:13.237292+00'
  deleted_at:
- id: df1f03c9-1831-442a-9035-0f77bc413ec1
  email: linus@torvalds.com
  full_name: Linus Torvalds
  created_at: '2024-01-20 14:25:28.301043+00'
  updated_at: '2024-01-20 14:25:28.301043+00'
  deleted_at:
```

Next, create the SQL migration for the `users` table like this:

```sql
[label postgres/migrations/001_create_users_tables.up.sql]
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

CREATE TABLE IF NOT EXISTS users(
    id uuid PRIMARY KEY DEFAULT uuid_generate_v4(),
    email VARCHAR (100) UNIQUE NOT NULL,
    full_name VARCHAR (100) NOT NULL,

    created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP,
    deleted_at TIMESTAMP WITH TIME ZONE
);
```

```sql
[label postgres/migrations/001_create_users_tables.down.sql]
DROP TABLE users;
```

We now have both our migrations and sample data ready. The next step is to
implement the setup function which will be called for each test function. We
have two methods in the `postgres/user.go` file so this ideally means we will
write two tests. Having a setup function means we can easily reuse the setup
logic for both tests.

To get started with creating a setup function, enter the following code in the
`postgres/user_test.go` file:

```go
[label postgres/user_test.go]
package postgres

import (
	"context"
	"database/sql"
	"fmt"
	"testing"

	testfixtures "github.com/go-testfixtures/testfixtures/v3"
	"github.com/golang-migrate/migrate/v4"
	"github.com/golang-migrate/migrate/v4/database/postgres"
	_ "github.com/golang-migrate/migrate/v4/source/file"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"
)

func prepareTestDatabase(t *testing.T, dsn string) {
	t.Helper()

	var err error

	db, err := sql.Open("postgres", dsn)
	if err != nil {
		t.Fatal(err)
	}

	err = db.Ping()
	if err != nil {
		t.Fatal(err)
	}

	driver, err := postgres.WithInstance(db, &postgres.Config{})
	if err != nil {
		t.Fatal(err)
	}

	migrator, err := migrate.NewWithDatabaseInstance(
		fmt.Sprintf("file://%s", "migrations"), "postgres", driver)
	if err != nil {
		t.Fatal(err)
	}

	if err := migrator.Up(); err != nil && err != migrate.ErrNoChange {
		t.Fatal(err)
	}

	fixtures, err := testfixtures.New(
		testfixtures.Database(db),
		testfixtures.Dialect("postgres"),
		testfixtures.Directory("testdata/fixtures"),
	)
	if err != nil {
		t.Fatal(err)
	}

	err = fixtures.Load()
	if err != nil {
		t.Fatal(err)
	}
}

// setupDatabase spins up a new Postgres container and returns a closure
// please always make sure to call the closure as it is the teardown function
func setupDatabase(t *testing.T) (*sql.DB, func()) {
	t.Helper()

	var dsn string

	containerReq := testcontainers.ContainerRequest{
		Image:        "postgres:latest",
		ExposedPorts: []string{"5432/tcp"},
		WaitingFor:   wait.ForListeningPort("5432/tcp"),
		Env: map[string]string{
			"POSTGRES_DB":       "betterstacktest",
			"POSTGRES_PASSWORD": "betterstack",
			"POSTGRES_USER":     "betterstack",
		},
	}

	dbContainer, err := testcontainers.GenericContainer(
		context.Background(),
		testcontainers.GenericContainerRequest{
			ContainerRequest: containerReq,
			Started:          true,
		})
	if err != nil {
		t.Fatal(err)
	}

	port, err := dbContainer.MappedPort(context.Background(), "5432")
	if err != nil {
		t.Fatal(err)
	}

	dsn = fmt.Sprintf(
		"postgres://%s:%s@%s/%s?sslmode=disable",
		"betterstack",
		"betterstack",
		fmt.Sprintf("localhost:%s", port.Port()),
		"betterstacktest",
	)

	prepareTestDatabase(t, dsn)

	db, err := sql.Open("postgres", dsn)
	if err != nil {
		t.Fatal(err)
	}

	err = db.Ping()
	if err != nil {
		t.Fatal(err)
	}

	return db, func() {
		err := dbContainer.Terminate(context.Background())
		if err != nil {
			t.Fatal(err)
		}
	}
}
```

The above code defines a setup for testing with a PostgreSQL database in Go,
using the
[testcontainers-go library](https://github.com/testcontainers/testcontainers-go)
to create a real database environment in Docker containers. We have the
following two functions:

- `setupDatabase()`: Acts as the main setup function that initializes a new
  PostgreSQL container, sets up the database, loads sample data, and returns a
  closure for tearing down the environment. This closure should be invoked at
  the completion of each test to properly clean up and shut down the database
  container.

- `prepareTestDatabase()`: Serves as a helper function to keep the
  `setupDatabase()` function concise. It is responsible for seeding the database
  with sample data using the
  [testfixtures](https://github.com/go-testfixtures/testfixtures) and
  [golang-migrate](https://github.com/golang-migrate/migrate) packages.

Ensure to download all the third-party packages used in the file by running:

```command
go mod tidy
```

Putting this together, an example of how to use the above code would be:

```go
func TestXxx(t *testing.T) {
  client, teardownFunc := setupDatabase(t)

  defer teardownFunc()

  . . .
}
```

The next step is to write the tests to validate the CRUD logic you previously
wrote. To do this, update the `user_test.go` file with the following contents:

```go
[label postgres/user_test.go]
package postgres

import (
	"context"
	"database/sql"
[highlight]
	"errors"
[/highlight]
	"fmt"
[highlight]
	"strings"
[/highlight]
	"testing"

	testfixtures "github.com/go-testfixtures/testfixtures/v3"
	"github.com/golang-migrate/migrate/v4"
	"github.com/golang-migrate/migrate/v4/database/postgres"
	_ "github.com/golang-migrate/migrate/v4/source/file"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"
)

. . .

func TestUserRepository_Create(t *testing.T) {
	client, teardownFunc := setupDatabase(t)
	defer teardownFunc()

	userDB := NewUserRepository(client)

	err := userDB.Create(context.Background(), &User{
		Email:    "ken@unix.org",
		FullName: "Ken Thompson",
	})
	if err != nil {
		t.Fatal(err)
	}
}

func TestUserRepository_Get(t *testing.T) {
	client, teardownFunc := setupDatabase(t)
	defer teardownFunc()

	userDB := NewUserRepository(client)

	// take a look at testdata/fixtures/users.yml
	// this email exists there so we must be able to fetch it
	_, err := userDB.Get(context.Background(), "john.doe@gmail.com")
	if err != nil {
		t.Fatal(err)
	}

	email := "test@test.com"
	firstName := "Ken Thompson"

	// email does not exist here
	_, err = userDB.Get(context.Background(), email)
	if err == nil {
		t.Fatal(errors.New("expected an error here. Email should not be found"))
	}

	if !errors.Is(err, sql.ErrNoRows) {
		t.Fatalf("Unexpected database error. Expected %v got %v", sql.ErrNoRows, err)
	}

	err = userDB.Create(context.Background(), &User{
		Email:    email,
		FullName: firstName,
	})
	if err != nil {
		t.Fatal(err)
	}

	// fetch the same email again
	user, err := userDB.Get(context.Background(), email)
	if err != nil {
		t.Fatal(err)
	}

	if !strings.EqualFold(email, user.Email) {
		t.Fatalf("retrieved values do not match. Expected %s, got %s", email, user.Email)
	}

	if !strings.EqualFold(firstName, user.FullName) {
		t.Fatalf("retrieved values do not match. Expected %s, got %s", firstName, user.FullName)
	}
}
```

You defined the following test cases in the file above:

- `TestUserRepository_Create()`: This test case handles the straightforward task
  of inserting a new user into the database.
- `TestUserRepository_Get()`: This test case checks the functionality of
  retrieving a user from the database. It also tests the retrieval of a
  non-existent user, followed by the creation of that user and a subsequent
  retrieval attempt to confirm the operation's success.

In both cases, the `setupDatabase()` function is called first, and the
`teardown()` function is deferred so that each test runs with a clean slate.

Our test suite for the `postgres` package is now complete so you can go ahead to
run them with the following command:

```command
go test ./... -v -run=TestUser
```

They should all pass successfully:

```text
[output]
=== RUN   TestUserRepository_Create
2024/01/22 19:31:21 github.com/testcontainers/testcontainers-go - Connected to docker:
  Server Version: 24.0.6
  API Version: 1.43
  Operating System: Docker Desktop
  Total Memory: 7844 MB
  Resolved Docker Host: unix:///var/run/docker.sock
  Resolved Docker Socket Path: /var/run/docker.sock
  Test SessionID: fd45df1e4f42f1f730acac7e01c3077b80d36e0875f33ecaeb03ed1ed0128f29
  Test ProcessID: e23dc38a-3263-462c-aaeb-110fcbb52f60
2024/01/22 19:31:21 🐳 Creating container for image testcontainers/ryuk:0.6.0
2024/01/22 19:31:21 ✅ Container created: ff74fcae2a70
2024/01/22 19:31:21 🐳 Starting container: ff74fcae2a70
2024/01/22 19:31:21 ✅ Container started: ff74fcae2a70
2024/01/22 19:31:21 🚧 Waiting for container id ff74fcae2a70 image: testcontainers/ryuk:0.6.0. Waiting for: &{Port:8080/tcp timeout:<nil> PollInterval:100ms}
2024/01/22 19:31:21 🐳 Creating container for image postgres:latest
2024/01/22 19:31:21 ✅ Container created: 72d02ad6eb2f
2024/01/22 19:31:21 🐳 Starting container: 72d02ad6eb2f
2024/01/22 19:31:22 ✅ Container started: 72d02ad6eb2f
2024/01/22 19:31:22 🚧 Waiting for container id 72d02ad6eb2f image: postgres:latest. Waiting for: &{Port:5432/tcp timeout:<nil> PollInterval:100ms}
2024/01/22 19:31:23 🐳 Terminating container: 72d02ad6eb2f
2024/01/22 19:31:24 🚫 Container terminated: 72d02ad6eb2f
--- PASS: TestUserRepository_Create (2.73s)
=== RUN   TestUserRepository_Get
2024/01/22 19:31:24 🐳 Creating container for image postgres:latest
2024/01/22 19:31:24 ✅ Container created: 57cfc1711ba1
2024/01/22 19:31:24 🐳 Starting container: 57cfc1711ba1
2024/01/22 19:31:24 ✅ Container started: 57cfc1711ba1
2024/01/22 19:31:24 🚧 Waiting for container id 57cfc1711ba1 image: postgres:latest. Waiting for: &{Port:5432/tcp timeout:<nil> PollInterval:100ms}
2024/01/22 19:31:25 🐳 Terminating container: 57cfc1711ba1
2024/01/22 19:31:25 🚫 Container terminated: 57cfc1711ba1
--- PASS: TestRepository_Get (1.36s)
PASS
ok      github.com/betterstack-community/intermediate-go-unit-tests/postgres    23.034s
```

## Step 6 — Running Go tests in parallel

Go tests are executed serially by default, meaning that each test runs only
after the previous one has completed. This approach is manageable with few
tests, but as your suite grows, the total execution time can become significant.

The end goal is to have a lot of tests, run them, and be confident they all pass
but not at the expense of the developer's time waiting for them to pass or fail.
To accelerate the testing process, Go can execute tests in parallel.

Here are a few benefits of running tests in parallel:

- **Increased speed**: Parallel testing can significantly reduce waiting time
  for test results.
- **Detection of flaky tests**: Flaky tests are those that produce inconsistent
  results, often due to dependencies on external states or interactions with
  shared resources. Running tests in parallel helps identify these issues early
  by isolating tests from shared states.

You can enable parallel test execution in Go using the following methods:

- **From the command line**: When running the `go test` command, you can use the
  `-parallel` flag to enable parallel test execution. This flag accepts a number
  indicating the maximum number of tests to run simultaneously, defaulting to
  the number of CPUs available on the machine.

```command
go test ./.. -v -parallel 2
```

```text
[output]
=== RUN   TestPrettyPrintJSON
--- PASS: TestPrettyPrintJSON (0.00s)
PASS
ok      github.com/betterstack-community/intermediate-go-unit-tests/fixtures    0.002s
2024/01/22 19:43:04 github.com/testcontainers/testcontainers-go - Connected to docker:
  Server Version: 24.0.6
  API Version: 1.43
  Operating System: Docker Desktop
  Total Memory: 7844 MB
  Resolved Docker Host: unix:///var/run/docker.sock
  Resolved Docker Socket Path: /var/run/docker.sock
  Test SessionID: bf8be00dd11df5712f7cde8e63c4e98679d6956b2943323f346aa0d3ca43764b
  Test ProcessID: 3dca0955-e549-4406-b3da-68a9c079267a
2024/01/22 19:43:04 🐳 Creating container for image testcontainers/ryuk:0.6.0
2024/01/22 19:43:04 ✅ Container created: bb751f45f9eb
2024/01/22 19:43:04 🐳 Starting container: bb751f45f9eb
2024/01/22 19:43:04 ✅ Container started: bb751f45f9eb
2024/01/22 19:43:04 🚧 Waiting for container id bb751f45f9eb image: testcontainers/ryuk:0.6.0. Waiting for: &{Port:8080/tcp timeout:<nil> PollInterval:100ms}
2024/01/22 19:43:04 🐳 Creating container for image postgres:latest
2024/01/22 19:43:04 🐳 Creating container for image postgres:latest
2024/01/22 19:43:04 ✅ Container created: 24ef76a672f3
2024/01/22 19:43:04 🐳 Starting container: 24ef76a672f3
2024/01/22 19:43:04 ✅ Container created: c1c43a4d4d14
2024/01/22 19:43:04 🐳 Starting container: c1c43a4d4d14
2024/01/22 19:43:04 ✅ Container started: 24ef76a672f3
2024/01/22 19:43:04 🚧 Waiting for container id 24ef76a672f3 image: postgres:latest. Waiting for: &{Port:5432/tcp timeout:<nil> PollInterval:100ms}
2024/01/22 19:43:04 ✅ Container started: c1c43a4d4d14
2024/01/22 19:43:04 🚧 Waiting for container id c1c43a4d4d14 image: postgres:latest. Waiting for: &{Port:5432/tcp timeout:<nil> PollInterval:100ms}
2024/01/22 19:43:06 🐳 Terminating container: c1c43a4d4d14
2024/01/22 19:43:06 🐳 Terminating container: 24ef76a672f3
2024/01/22 19:43:06 🚫 Container terminated: c1c43a4d4d14
--- PASS: TestUserRepository_Create (2.21s)
2024/01/22 19:43:06 🚫 Container terminated: 24ef76a672f3
--- PASS: TestUserRepository_Get (2.34s)
PASS
ok      github.com/betterstack-community/intermediate-go-unit-tests/postgres    22.331s
```

- **Within test code**: Invoking the `t.Parallel()` method in your test function
  instructs the test runner to run the test in parallel with others.

```go
func TestUserRepository_Get(t *testing.T) {
	t.Parallel() // instructs `go test` to run this test in parallel
    . . .
}
```

## Step 7 — Improving `go test` output

While Go's test runner produces output that can be easily read and understood,
there are ways to make it much more readable. For example, using colors to
denote failed and passed tests, getting a detailed summary of all executed tests
among others.

To demonstrate this, we will be using a project called
[gotestsum](https://github.com/gotestyourself/gotestsum), but there are others
like [gotestfmt](https://github.com/GoTestTools/gotestfmt) you can explore as
well. To install this package, you need to run the following command:

```command
go install gotest.tools/gotestsum@latest
```

The `gotestsum` package includes a few different ways to format the output of
the executed tests. The first one is `testdox`. This can be used by running the
following command:

```command
gotestsum --format testdox
```

```text
[output]

ayinke-llc/betterstack-articles/advanced-unittest/configuration:

ayinke-llc/betterstack-articles/advanced-unittest/fixtures:
 ✓ Pretty print JSON (0.00s)

ayinke-llc/betterstack-articles/advanced-unittest/postgres:
 ✓ UserRepository create (3.12s)
 ✓ UserRepository get (1.49s)


DONE 3 tests in 0.503s
```

Another popular option is to list the packages that have been tested. This can
be used by running the following command:

```command
gotestsum --format pkgname
```

```text
[output]
✓  fixtures (765ms)
✓  postgres (3.638s)

DONE 3 tests in 5.487s
```

An added advantage of using `gotestsum` is that it can automatically rerun tests
upon any changes to Go files in the project through `--watch` flag:

```command
gotestsum --watch --format testname
```

```text
[output]
Watching 2 directories. Use Ctrl-c to to stop a run or exit.
```

## Step 8 — Understanding Blackbox and Whitebox testing

Testing in Go is generally a straightforward process: invoke a function or
system, provide inputs, and verify the outputs. However, there are two primary
approaches to this process: Whitebox testing and Blackbox testing.

### 1. Whitebox testing

Throughout this tutorial, we've primarily engaged in Whitebox testing. This
approach involves accessing and inspecting the internal implementations of the
functions under test by placing the test file in the same package as the code
under test.

For example, if you have a package `calc` with the following code:

```go
[label calc/calc.go]
package calc

// Add adds two integers and returns the sum
func Add(x, y int) int {
  return x + y
}
```

The test for the `Add()` method will be in the same package like this:

```go
[label calc/calc_test.go]
package calc

import (
	"testing"
)

func TestAdd(t *testing.T) {
	tests := []struct {
		a, b int
		want int
	}{
		{1, 2, 3},
		{5, -3, 2},
		{0, 0, 0},
	}

	for _, tt := range tests {
		t.Run("", func(t *testing.T) {
			got := Add(tt.a, tt.b) // You can access any public or private function defined in the `calc` package directly
			if got != tt.want {
				t.Errorf(
					"Add(%d, %d) want %d, got %d",
					tt.a,
					tt.b,
					tt.want,
					got,
				)
			}
		})
	}
}
```

Since Whitebox testing allows you to access internal state, you can often catch
certain bugs by asserting against the internal state of the function under test.

Its main disadvantage is that such tests can be more brittle since they are
coupled to the program's internal structure. For example, if you change the
algorithm used to compute some result, the test can break even if the final
output is exactly the same.

### 2. Blackbox testing

Blackbox testing involves testing a software system without any knowledge of the
application's internal workings. The test does not assert against the underlying
logic of the function but merely checks if the software behaves as expected from
an external viewpoint.

To implement Blackbox testing in Go, place your tests in an inner package by
appending `_test` to the package name, which effectively restricts access to
internal-only states and functions.

With the `calc` example, the test will be placed in a `calc/calc_test` directory
like this:

```go
[label calc/calc_test/calc_test.go]
package calc_test

import (
	"github.com/betterstack-community/intermediate-go-unit-tests/calc"
	"testing"
)

func TestAdd(t *testing.T) {
	tests := []struct {
		a, b int
		want int
	}{
		{1, 2, 3},
		{5, -3, 2},
		{0, 0, 0},
	}

	for _, tt := range tests {
		t.Run("", func(t *testing.T) {
			got := calc.Add(tt.a, tt.b) // You can only access public functions that are exported from the `calc` package
			if got != tt.want {
				t.Errorf(
					"Add(%d, %d) want %d, got %d",
					tt.a,
					tt.b,
					tt.want,
					got,
				)
			}
		})
	}
}
```

This method of testing prevents you from being able to access the internal state
of the `calc` package, thus allowing you to focus on ensuring that the
function being tested produces the correct output.

If you're practicing Blackbox testing, and you also need to test implementation
details, a common pattern is to create an `_internal_test.go` within the package
under test:

```go
[label calc/calc_internal_test.go]
package calc

// Test implementation details separately here
```

## Final thoughts

As your test suite expands, the complexity can also increase. However, by
applying the patterns and techniques discussed in this article, you can keep
your tests organized and manageable.

Thanks for reading, and happy testing!
