Todo: testing

Who am I?

Damiano Petrungaro

Who am I?

Staff Engineer @Odin

Who am I?

Milan, Italy

Who am I?

Manga and anime

Database and testcontainers

Database and testcontainers

package user

import (
	"context"
	"errors"

	"github.com/google/uuid"
)

var (
	ErrNotFound   = errors.New("user not found")
	ErrNotAdded   = errors.New("user not added")
	ErrNotUpdated = errors.New("user not updated")
	ErrNotRemoved = errors.New("user not removed")
)

type User struct {
	ID    uuid.UUID
	Name  string
	Email string
}

type Repo interface {
	Get(context.Context, uuid.UUID) (*User, error)
	Add(context.Context, *User) error
	Update(context.Context, *User) error
	Remove(context.Context, uuid.UUID) error
}

Database and testcontainers

package user

import (
	"context"
	"errors"

	"github.com/google/uuid"
)

var (
	ErrNotFound   = errors.New("user not found")
	ErrNotAdded   = errors.New("user not added")
	ErrNotUpdated = errors.New("user not updated")
	ErrNotRemoved = errors.New("user not removed")
)

type User struct {
	ID    uuid.UUID
	Name  string
	Email string
}

type Repo interface {
	Get(context.Context, uuid.UUID) (*User, error)
	Add(context.Context, *User) error
	Update(context.Context, *User) error
	Remove(context.Context, uuid.UUID) error
}

Database and testcontainers

package postgres_test

import (
	"context"
	"database/sql"
	"fmt"
	"log"
	"os"
	"testing"
	"time"

	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"
)

var db *sql.DB

func TestMain(m *testing.M) {
	close := setupDB()
	defer close()

	os.Exit(m.Run())
}

func setupDB() func() {
	ctx := context.Background()

	container, err := testcontainers.GenericContainer(
		ctx,
		testcontainers.GenericContainerRequest{
			ContainerRequest: testcontainers.ContainerRequest{
				FromDockerfile: testcontainers.FromDockerfile{
					Context:    "./../../../",
					Dockerfile: "build/package/postgres.Dockerfile",
				},
				ExposedPorts: []string{"5432/tcp"},
				WaitingFor: (&wait.LogStrategy{
					Log:          "database system is ready to accept connections",
					Occurrence:   2,
					PollInterval: 100 * time.Millisecond,
				}).WithStartupTimeout(2 * time.Minute),
				Env: map[string]string{
					"POSTGRES_DB":       "postgres",
					"POSTGRES_PASSWORD": "postgres",
					"POSTGRES_USER":     "postgres",
				},
			},
			Started: true,
			Logger:  log.Default(),
		},
	)
	if err != nil {
		log.Fatalf("could not connect to the database: %s", err)
	}

	host, err := container.Host(ctx)
	if err != nil {
		log.Fatalf("could not get container host: %s", err)
	}

	port, err := container.MappedPort(ctx, "5432")
	if err != nil {
		log.Fatalf("could not get container port: %s", err)
	}

	db, err = sql.Open("postgres", fmt.Sprintf("postgres://postgres:postgres@%v:%v/postgres?sslmode=disable", host, port.Port()))
	if err != nil {
		log.Fatalf("Failed to connect to the database: %v", err)
	}

	return func() {
		if err := db.Close(); err != nil {
			log.Fatalf("Failed to close the database connection: %v", err)
		}
	}
}

Database and testcontainers

package postgres_test

import (
	"context"
	"errors"
	"testing"

	"github.com/google/uuid"

	"testing/pkg/user"
	"testing/pkg/user/postgres"
)

func TestRepository_Get(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	repo := postgres.New(db)

	t.Run("exists", func(t *testing.T) {
		given := &user.User{
			ID:    uuid.New(),
			Name:  "Luke Skywalker",
			Email: "Luke_Skywalker@gmail.com",
		}
		if _, err := db.ExecContext(
			ctx,
			"INSERT INTO users (id, name, email) VALUES ($1, $2, $3)",
			given.ID,
			given.Name,
			given.Email,
		); err != nil {
			t.Fatalf("could not insert user: %s", err)
		}

		got, err := repo.Get(ctx, given.ID)
		if err != nil {
			t.Fatalf("could not get user: %s", err)
		}

		match(t, got, given)
	})

	t.Run("not exists", func(t *testing.T) {
		id := uuid.New()
		got, err := repo.Get(ctx, id)
		if !errors.Is(err, user.ErrNotFound) {
			t.Fatalf("expected error: %s", err)
		}
		if got != nil {
			t.Fatalf("expected nil user, got %v", got)
		}
	})
}

func TestRepository_Add(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	repo := postgres.New(db)

	given := &user.User{
		ID:    uuid.New(),
		Name:  "John Doe",
		Email: "johndoe@gmail.com",
	}
	if err := repo.Add(ctx, given); err != nil {
		t.Fatalf("could not add user: %s", err)
	}

	got := &user.User{}
	if err := db.QueryRowContext(
		ctx,
		"SELECT * FROM users WHERE ID = $1",
		given.ID.String(),
	).Scan(&got.ID, &got.Name, &got.Email); err != nil {
		t.Fatalf("could not query user: %s", err)
	}

	match(t, got, given)
}

func TestRepository_Update(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	repo := postgres.New(db)

	t.Run("not exists", func(t *testing.T) {
		given := &user.User{
			ID:    uuid.New(),
			Name:  "Fred Flintstone",
			Email: "flintstone@gmail.com",
		}
		if err := repo.Update(ctx, given); !errors.Is(err, user.ErrNotUpdated) {
			t.Fatalf("expected error: %s", err)
		}
	})

	t.Run("exists", func(t *testing.T) {
		given := &user.User{
			ID:    uuid.New(),
			Name:  "Rich Hickey",
			Email: "rich@hickey.com",
		}
		if _, err := db.ExecContext(
			ctx,
			"INSERT INTO users (id, name, email) VALUES ($1, $2, $3)",
			given.ID,
			given.Name,
			given.Email,
		); err != nil {
			t.Fatalf("could not insert user: %s", err)
		}

		given.Name = "Rich Hickey Jr."
		given.Email = "richjr@hickey.com"

		if err := repo.Update(ctx, given); err != nil {
			t.Fatalf("could not update user: %s", err)
		}

		var id, name, email string
		err := db.QueryRowContext(ctx, "SELECT * FROM users WHERE id = $1", given.ID).Scan(
			&id,
			&name,
			&email,
		)
		if err != nil {
			t.Fatalf("could not query user: %s", err)
		}

		if id != given.ID.String() {
			t.Errorf("expected ID %s, got %s", given.ID.String(), id)
		}
		if name != given.Name {
			t.Errorf("expected Name %s, got %s", given.Name, name)
		}
		if email != given.Email {
			t.Errorf("expected Email %s, got %s", given.Email, email)
		}
	})
}

func TestRepository_Remove(t *testing.T) {
	t.Parallel()

	ctx := context.Background()
	repo := postgres.New(db)

	t.Run("not exists", func(t *testing.T) {
		id := uuid.New()
		if err := repo.Remove(ctx, id); !errors.Is(err, user.ErrNotRemoved) {
			t.Fatalf("expected error: %s", err)
		}
	})

	t.Run("exists", func(t *testing.T) {
		given := &user.User{
			ID:    uuid.New(),
			Name:  "Johnathan Seagull",
			Email: "seagull@gmail.com",
		}

		if _, err := db.ExecContext(
			ctx,
			"INSERT INTO users (id, name, email) VALUES ($1, $2, $3)",
			given.ID,
			given.Name,
			given.Email,
		); err != nil {
			t.Fatalf("could not insert user: %s", err)
		}

		if err := repo.Remove(ctx, given.ID); err != nil {
			t.Fatalf("could not remove user: %s", err)
		}

		var count int
		err := db.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE id = $1", given.ID).Scan(&count)
		if err != nil {
			t.Fatalf("could not query user: %s", err)
		}

		if count != 0 {
			t.Errorf("expected count 0, got %d", count)
		}
	})
}

func match(t *testing.T, got, want *user.User) {
	t.Helper()

	if got.ID != want.ID {
		t.Errorf("expected ID %s, got %s", want.ID, got.ID)
	}
	if got.Name != want.Name {
		t.Errorf("expected Name %s, got %s", want.Name, got.Name)
	}
	if got.Email != want.Email {
		t.Errorf("expected Email %s, got %s", want.Email, got.Email)
	}
}

Recording HTTP&gRPC

Recording HTTP&gRPC

BDD

BDD