testcontainer-go

#About Examples

  • We using Setups and Teardowns (so we using test suite)
  • We actually testing minimalistic implementation of repository pattern (see DDD and Ports and Adapters). And testing different adapters with out port (Repository)

#Suite

//  - suite_test.go - 
package testcontainer

import "github.com/stretchr/testify/suite"

type RepositoryTestSuite struct {
	suite.Suite

	Repo Repository
}

// Defining Test Suite

// Interface implementation
func (suite *RepositoryTestSuite) SetupSuite()                  { println("SetupSuite") }
func (suite *RepositoryTestSuite) TearDownTest()                { println("TearDownTest") }
func (suite *RepositoryTestSuite) BeforeTest(name, test string) { println("Before", name, test) }
func (suite *RepositoryTestSuite) AfterTest(name, test string)  { println("After", name, test) }
func (suite *RepositoryTestSuite) SetupTest()                   { println("SetupTest") }
func (suite *RepositoryTestSuite) TearDownSuite()               { println("TearDownSuite") }

func (suite *RepositoryTestSuite) TestSum() {
	suite.Equal(3, suite.Repo.Sum(1, 2))
}
//  - repository.go - 
package testcontainer

type Repository interface {
	Sum(int, int) int
}

#MySQL

//  - repository_mysql.go - 
package testcontainer

import (
	_ "github.com/go-sql-driver/mysql"
	"github.com/jmoiron/sqlx"
)

type MySQLRepository struct {
	db *sqlx.DB
}

func NewMySQLRepository(db *sqlx.DB) (MySQLRepository, error) {
	return MySQLRepository{db}, nil
}

func (repo MySQLRepository) Sum(a, b int) int {
	row := repo.db.QueryRow("SELECT ? + ?", a, b)

	var sum int
	if err := row.Scan(&sum); err != nil {
		panic(err)
	}

	return sum
}
//  - repository_mysql_test.go - 
package testcontainer

import (
	"context"
	"fmt"
	"log"
	"testing"

	_ "github.com/go-sql-driver/mysql"
	"github.com/jmoiron/sqlx"

	"github.com/docker/go-connections/nat"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/suite"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"
)

type (
	MySQLSettings struct {
		User  string
		Pass  string
		Host  string
		Ports []string
		Name  string
		Extra string

		terminate func()
	}
)

// Step 1: Define Setup and Terminate on db settings.

func (db *MySQLSettings) Setup() (*sqlx.DB, error) {
	var (
		ctx = context.Background()
		req = testcontainers.ContainerRequest{
			Image:        "mysql:latest",
			ExposedPorts: db.Ports,
			Env: map[string]string{
				"MYSQL_ROOT_PASSWORD": db.Pass,
				"MYSQL_DATABASE":      db.Name,
				"MYSQL_USER":          db.User,
				"MYSQL_PASSWORD":      db.Pass,
			},
			WaitingFor: wait.ForLog("port: 3306  MySQL Community Server - GPL"),
		}
	)

	mysql, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
		ContainerRequest: req,
		Started:          true,
	})
	if err != nil {
		return nil, err
	}

	db.terminate = func() {
		if err != mysql.Terminate(ctx) {
			log.Fatalf("error terminating mysql container: %s", err)
		}
	}

	var (
		host, _ = mysql.Host(ctx)
		port, _ = mysql.MappedPort(ctx, nat.Port(db.Ports[0]))

		dsn = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?%s",
			db.User, db.Pass, host, port.Port(), db.Name, db.Extra)
	)

	dbx, err := sqlx.Connect("mysql", dsn)
	if err == nil {
		return dbx, nil
	}

	return nil, fmt.Errorf("error connect to db: %+v\n", err)
}

func (db MySQLSettings) Terminate() {
	if db.terminate != nil {
		db.terminate()
	}
}

// Step 2: Crate Initial Test for MySQL.

func Test_MySQLRepository(t *testing.T) {
	mysql := MySQLSettings{
		User:  "db_user",
		Pass:  "db_pass",
		Host:  "localhost",
		Ports: []string{"3306/tcp", "33060/tcp"},
		Name:  "db_name",
		Extra: "tls=skip-verify&parseTime=true&multiStatements=true",
	}

	if db, err := mysql.Setup(); err == nil {

		t.Cleanup(mysql.Terminate)

		if repo, err := NewMySQLRepository(db); err == nil {
			suite.Run(t, &RepositoryTestSuite{Repo: repo})
		} else {
			assert.Fail(t, err.Error())
		}

	} else {
		assert.Fail(t, err.Error())
	}
}

#Postgress (& pgx)

//  - repository_pgx.go - 
package testcontainer

import (
	_ "github.com/jackc/pgx/v4"
	_ "github.com/jackc/pgx/v4/stdlib"
	"github.com/jmoiron/sqlx"
)

type PgXSQLRepository struct {
	db *sqlx.DB
}

func NewPgXSQLRepository(db *sqlx.DB) (PgXSQLRepository, error) {
	return PgXSQLRepository{db}, nil
}

func (repo PgXSQLRepository) Sum(a, b int) int {

	var (
		result struct {
			N int `db:"n"`
		}

		query = "select sum(x::int) as n from (values ($1::NUMERIC), ($2::NUMERIC)) as dt(x)"
	)

	row := repo.db.QueryRowx(query, a, b)
	if err := row.StructScan(&result); err != nil {
		panic(err)
	}

	return result.N
}
//  - repository_pgx_test.go - 
package testcontainer

import (
	"context"
	"fmt"
	"log"
	"testing"

	_ "github.com/jackc/pgx/v4"
	_ "github.com/jackc/pgx/v4/stdlib"
	"github.com/jmoiron/sqlx"

	"github.com/docker/go-connections/nat"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/suite"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"
)

type (
	PGXSettings struct {
		User  string
		Pass  string
		Host  string
		Ports []string
		Name  string
		Extra string

		terminate func()
	}

	RepositoryPGXTestSuite struct {
		suite.Suite

		Repo Repository
	}
)

// Step 1: Define Setup and Terminate on db settings.

func (db *PGXSettings) Setup() (*sqlx.DB, error) {
	ctx := context.Background()

	pg, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
		ContainerRequest: testcontainers.ContainerRequest{
			Image:        "postgres:11.4-alpine",
			ExposedPorts: db.Ports,
			Env: map[string]string{
				"POSTGRES_PASSWORD": db.Pass,
				"POSTGRES_USER":     db.User,
				"POSTGRES_DB":       db.Name,
			},
			WaitingFor: wait.ForAll(
				wait.ForLog("database system is ready to accept connections"),
				wait.ForListeningPort(nat.Port(db.Ports[0])),
			),
		},
		Started: true,
	})
	if err != nil {
		return nil, err
	}

	db.terminate = func() {
		if err != pg.Terminate(ctx) {
			log.Fatalf("error terminating mysql container: %s", err)
		}
	}

	var (
		host, _ = pg.Host(ctx)
		port, _ = pg.MappedPort(ctx, nat.Port(db.Ports[0]))

		dsn = fmt.Sprintf("postgres://%s:%s@%s:%s/%s?%s",
			db.User, db.Pass, host, port.Port(), db.Name, db.Extra)

		// using this (canonical) dsn is also OK!
		// dsn = fmt.Sprintf("user=%[1]s password=%[2]s host=%[3]s port=%[4]s dbname=%[5]s %[6]s",
		// 	db.User, db.Pass, host, port.Port(), db.Name, db.Extra)
	)

	dbx, err := sqlx.Connect("pgx", dsn)
	if err == nil {
		return dbx, nil
	}

	return nil, fmt.Errorf("error connect to db: %+v\n", err)
}

func (db PGXSettings) Terminate() {
	if db.terminate != nil {
		db.terminate()
	}
}

// Step 2: Crate Initial Test for Postgres using PGX driver.

func Test_PGXRepository(t *testing.T) {
	pg := PGXSettings{
		User:  "db_user",
		Pass:  "db_pass",
		Host:  "localhost",
		Ports: []string{"5432/tcp"},
		Name:  "db_name",
		Extra: "sslmode=disable",
	}

	if db, err := pg.Setup(); err == nil {
		t.Cleanup(pg.Terminate)

		if repo, err := NewPgXSQLRepository(db); err == nil {
			suite.Run(t, &RepositoryTestSuite{Repo: repo})
		} else {
			assert.Fail(t, err.Error())
		}
	} else {
		assert.Fail(t, err.Error())
	}
}
//  - repository_postgres.go - 
package testcontainer

import (
	"github.com/jmoiron/sqlx"
	_ "github.com/lib/pq"
)

type PostgreSQLRepository struct {
	db *sqlx.DB
}

func NewPostgreSQLRepository(db *sqlx.DB) (PostgreSQLRepository, error) {
	return PostgreSQLRepository{db}, nil
}

func (repo PostgreSQLRepository) Sum(a, b int) int {
	row := repo.db.QueryRow("select sum(x::int) from (values ($1), ($2)) as dt(x)", a, b)

	var sum int
	if err := row.Scan(&sum); err != nil {
		panic(err)
	}

	return sum
}
//  - repository_postgres_test.go - 
package testcontainer

import (
	"context"
	"fmt"
	"log"
	"testing"

	"github.com/jmoiron/sqlx"
	_ "github.com/lib/pq"

	"github.com/docker/go-connections/nat"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/suite"
	"github.com/testcontainers/testcontainers-go"
	"github.com/testcontainers/testcontainers-go/wait"
)

type (
	PostgreSettings struct {
		User  string
		Pass  string
		Host  string
		Ports []string
		Name  string
		Extra string

		terminate func()
	}
)

// Step 1: Define Setup and Terminate on db settings.

func (db *PostgreSettings) Setup() (*sqlx.DB, error) {
	ctx := context.Background()

	pg, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
		ContainerRequest: testcontainers.ContainerRequest{
			Image:        "postgres:11.4-alpine",
			ExposedPorts: db.Ports,
			Env: map[string]string{
				"POSTGRES_PASSWORD": db.Pass,
				"POSTGRES_USER":     db.User,
				"POSTGRES_DB":       db.Name,
			},
			WaitingFor: wait.ForAll(
				wait.ForLog("database system is ready to accept connections"),
				wait.ForListeningPort(nat.Port(db.Ports[0])),
			),
		},
		Started: true,
	})
	if err != nil {
		return nil, err
	}

	db.terminate = func() {
		if err != pg.Terminate(ctx) {
			log.Fatalf("error terminating mysql container: %s", err)
		}
	}

	var (
		host, _ = pg.Host(ctx)
		port, _ = pg.MappedPort(ctx, nat.Port(db.Ports[0]))

		dsn = fmt.Sprintf("postgres://%s:%s@%s:%s/%s?%s",
			db.User, db.Pass, host, port.Port(), db.Name, db.Extra)

		// using this (canonical) dsn is also OK!
		// dsn = fmt.Sprintf("user=%[1]s password=%[2]s host=%[3]s port=%[4]s dbname=%[5]s %[6]s",
		// 	db.User, db.Pass, host, port.Port(), db.Name, db.Extra)
	)

	dbx, err := sqlx.Connect("postgres", dsn)
	if err == nil {
		return dbx, nil
	}

	return nil, fmt.Errorf("error connect to db: %+v\n", err)
}

func (db PostgreSettings) Terminate() {
	if db.terminate != nil {
		db.terminate()
	}
}

// Step 2: Crate Initial Test for Postgres using PQ driver.

func Test_PostgressRepository(t *testing.T) {
	pg := PostgreSettings{
		User:  "db_user",
		Pass:  "db_pass",
		Host:  "localhost",
		Ports: []string{"5432/tcp"},
		Name:  "db_name",
		Extra: "sslmode=disable",
	}

	if db, err := pg.Setup(); err == nil {
		t.Cleanup(pg.Terminate)

		if repo, err := NewPostgreSQLRepository(db); err == nil {
			suite.Run(t, &RepositoryTestSuite{Repo: repo})
		} else {
			assert.Fail(t, err.Error())
		}

	} else {
		assert.Fail(t, err.Error())
	}
}

#Extras

//  - Makefile - 
psql:  ## Run PostgreSQL shell
	docker exec -it testcontainer_postgresql_1 psql -d db_name -U db_user
//  - docker-compose.yml - 
version: '3.7'

services:

  postgresql:
    image: postgres:alpine
    ports: [ "5432:5432" ]
    environment:
      POSTGRES_DB: db_name
      POSTGRES_USER: db_user
      POSTGRES_PASSWORD: db_pass
    volumes:
      - postgres:/var/lib/postgresql/data
    healthcheck:
      test: "exit 0"
    restart: "on-failure"
    networks: [ "go_develop" ]

networks:
  go_develop:
    driver: bridge

volumes:
  postgres: {}