Skip to content

Integration Testing

Johnny Boursiquot edited this page Jun 7, 2024 · 1 revision

Consider the following LibraryService which relies on a live PostgreSQL server connection to operate. We could mock the database connection using something like sqlmock but there are times when we want to test against a real system over the network.

testing/integration/library.go

package integration

import (
	"gorm.io/gorm"
)

type Author struct {
	gorm.Model
	Name string
}

type Book struct {
	gorm.Model
	Title   string
	Authors []Author `gorm:"many2many:book_authors;"`
}

type LibraryService struct {
	db *gorm.DB
}

func NewLibraryService(db *gorm.DB) *LibraryService {
	return &LibraryService{db: db}
}

func (s *LibraryService) CreateBook(book *Book) error {
	return s.db.Create(book).Error
}

func (s *LibraryService) GetBook(id uint) (*Book, error) {
	var book Book
	err := s.db.Preload("Authors").First(&book, id).Error
	return &book, err
}

Your options include:

  1. Have a local server running either directly on your machine
  2. Have a PostgreSQL docker container running
  3. Testcontainers

The following example uses the Testcontainers approach.

You'll need to familiarize yourself with the Go Quickstart guide. Once you do, we can dive into the following code which uses a test flag that we toggle whenever we want to run integration tests along with our unit tests.

package integration_test

import (
	"context"
	"flag"
	"log/slog"
	"os"
	"testing"
	"time"

	"github.com/idiomat/dodtnyt/testing/integration"
	"github.com/stretchr/testify/assert"
	"github.com/testcontainers/testcontainers-go"
	tpg "github.com/testcontainers/testcontainers-go/modules/postgres"
	"github.com/testcontainers/testcontainers-go/wait"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

var db *gorm.DB
var runIntegrationTests = flag.Bool("integration", false, "run integration tests")

func TestMain(m *testing.M) {
	flag.Parse()

	if *runIntegrationTests {
		ctx := context.Background()
		var err error

		pgUser := "user"
		pgPass := "password"
		pgDB := "test"
		pgc, err := tpg.RunContainer(ctx,
			testcontainers.WithImage("postgres:16-alpine"),
			tpg.WithDatabase(pgDB),
			tpg.WithUsername(pgUser),
			tpg.WithPassword(pgPass),
			testcontainers.WithWaitStrategy(
				wait.ForLog("database system is ready to accept connections").
					WithOccurrence(2).
					WithStartupTimeout(5*time.Second)),
		)
		if err != nil {
			slog.Error("failed to start postgres container", "error", err)
			os.Exit(1)
		}

		defer pgc.Terminate(ctx) // nolint:errcheck

		dsn, err := pgc.ConnectionString(ctx, "sslmode=disable")
		if err != nil {
			slog.Error("failed to get connection string", "error", err)
			os.Exit(1)
		}

		db, err = gorm.Open(
			postgres.Open(dsn),
			&gorm.Config{},
		)
		if err != nil {
			slog.Error("failed to connect to database", "error", err)
			os.Exit(1)
		}

		if err := db.AutoMigrate(&integration.Author{}, &integration.Book{}); err != nil {
			slog.Error("failed to migrate database", "error", err)
			os.Exit(1)
		}
	}

	// Run the tests
	exitCode := m.Run()

	os.Exit(exitCode)
}

func TestCreateBook_Integration(t *testing.T) {
	if !*runIntegrationTests {
		t.Skip("skipping integration test")
	}

	service := integration.NewLibraryService(db)

	book := &integration.Book{
		Title:   "Meditations",
		Authors: []integration.Author{{Name: "Marcus Aurelius"}},
	}

	err := service.CreateBook(book)

	assert.Nil(t, err)
}

func TestGetBook_Integration(t *testing.T) {
	if !*runIntegrationTests {
		t.Skip("skipping integration test")
	}

	service := integration.NewLibraryService(db)

	book := &integration.Book{
		Title:   "Meditations",
		Authors: []integration.Author{{Name: "Marcus Aurelius"}},
	}

	err := service.CreateBook(book)
	assert.Nil(t, err)

	book, err = service.GetBook(book.ID)
	assert.Nil(t, err)
	assert.Equal(t, "Meditations", book.Title)
	assert.Equal(t, "Marcus Aurelius", book.Authors[0].Name)
}
$ go test -v ./testing/integration/... -integration=true

Expect output along the line of the following:

2024/06/07 12:42:28 github.com/testcontainers/testcontainers-go - Connected to docker: 
  Server Version: 26.1.1
  API Version: 1.44
  Operating System: Docker Desktop
  Total Memory: 7940 MB
  Resolved Docker Host: unix:///var/run/docker.sock
  Resolved Docker Socket Path: /var/run/docker.sock
  Test SessionID: 8b2614dc444bf0cfc92ece05fa3a0b70c5bf7eb2bb676e99e068d44896360b71
  Test ProcessID: 9aa41bd3-4801-443d-a1b7-36d7312baf13
2024/06/07 12:42:28 🐳 Creating container for image testcontainers/ryuk:0.7.0
2024/06/07 12:42:28 βœ… Container created: 492604143fa9
2024/06/07 12:42:28 🐳 Starting container: 492604143fa9
2024/06/07 12:42:28 βœ… Container started: 492604143fa9
2024/06/07 12:42:28 🚧 Waiting for container id 492604143fa9 image: testcontainers/ryuk:0.7.0. Waiting for: &{Port:8080/tcp timeout:<nil> PollInterval:100ms}
2024/06/07 12:42:28 πŸ”” Container is ready: 492604143fa9
2024/06/07 12:42:28 🐳 Creating container for image postgres:16-alpine
2024/06/07 12:42:28 βœ… Container created: a521ef3a5991
2024/06/07 12:42:28 🐳 Starting container: a521ef3a5991
2024/06/07 12:42:28 βœ… Container started: a521ef3a5991
2024/06/07 12:42:28 🚧 Waiting for container id a521ef3a5991 image: postgres:16-alpine. Waiting for: &{timeout:<nil> deadline:0x1400047ef70 Strategies:[0x14000194780]}
2024/06/07 12:42:29 πŸ”” Container is ready: a521ef3a5991
=== RUN   TestCreateBook_Integration
--- PASS: TestCreateBook_Integration (0.01s)
=== RUN   TestGetBook_Integration
--- PASS: TestGetBook_Integration (0.02s)
PASS
ok      github.com/idiomat/dodtnyt/testing/integration  2.051s
Clone this wiki locally