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())
}
}
// - 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: {}