1
0
mirror of https://github.com/xorcare/testing-go-code-with-postgres.git synced 2025-06-30 23:23:40 +02:00

Add example of tests with isolated schema

This commit is contained in:
Vasiliy Vasilyuk
2024-03-05 21:09:21 +03:00
parent 05d30f6c09
commit 593bc9715c
9 changed files with 333 additions and 8 deletions

View File

@ -78,6 +78,7 @@ total: (statements) 100.0%
package.
- [Example of integration testing with isolated database for each testcase](https://github.com/xorcare/testing-go-code-with-postgres/blob/main/user_repository_with_isolated_database_test.go).
- [Example of integration testing with transaction cleanup for each testcase](https://github.com/xorcare/testing-go-code-with-postgres/blob/main/user_repository_with_transactional_cleanup_test.go).
- [Example of integration testing with isolated schema for each testcase](https://github.com/xorcare/testing-go-code-with-postgres/blob/main/user_repository_with_isolated_schema_test.go).
- And example
of [GitHub Actions](https://github.com/xorcare/testing-go-code-with-postgres/blob/main/.github/workflows/go.yml)
and [Gitlab CI](https://github.com/xorcare/testing-go-code-with-postgres/blob/main/.gitlab-ci.yml).

9
go.mod
View File

@ -3,6 +3,7 @@ module github.com/xorcare/testing-go-code-with-postgres
go 1.22.0
require (
github.com/golang-migrate/migrate/v4 v4.17.1
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.6.0
github.com/stretchr/testify v1.9.0
@ -10,14 +11,18 @@ require (
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/puddle/v2 v2.2.1 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lib/pq v1.10.9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
golang.org/x/crypto v0.17.0 // indirect
golang.org/x/sync v0.1.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
golang.org/x/crypto v0.20.0 // indirect
golang.org/x/sync v0.5.0 // indirect
golang.org/x/text v0.14.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

53
go.sum
View File

@ -1,9 +1,32 @@
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dhui/dktest v0.4.1 h1:/w+IWuDXVymg3IrRJCHHOkMK10m9aNVMOyD0X12YVTg=
github.com/dhui/dktest v0.4.1/go.mod h1:DdOqcUpL7vgyP4GlF3X3w7HbSlz8cEQzwewPveYEQbA=
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0=
github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4=
github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
@ -16,6 +39,18 @@ github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
@ -25,12 +60,22 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

10
migrations/embed.go Normal file
View File

@ -0,0 +1,10 @@
// Copyright (c) 2024 Vasiliy Vasilyuk. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package migrations
import "embed"
//go:embed *.up.sql
var FS embed.FS

View File

@ -17,6 +17,7 @@ import (
"time"
"unicode"
_ "github.com/golang-migrate/migrate/v4/source/file"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/stretchr/testify/require"
)
@ -37,6 +38,10 @@ func NewWithIsolatedDatabase(t TestingT) *Postgres {
return newPostgres(t, defaultPostgresURL).cloneFromReference()
}
func NewWithIsolatedSchema(t TestingT) *Postgres {
return newPostgres(t, defaultPostgresURL).createSchema(t)
}
func NewWithTransactionalCleanup(t TestingT) interface {
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
@ -108,6 +113,40 @@ func (p *Postgres) DB() *sql.DB {
return p.sqlDB
}
func (p *Postgres) createSchema(t TestingT) *Postgres {
schemaName := newUniqueHumanReadableDatabaseName(p.t)
// Unclear why, but if the scheme contains letters of different case, the
// tests stop working. At the moment I don't quite understand why this
// happens, but converting to lower case fixes the problem.
schemaName = strings.ToLower(schemaName)
ctx, done := context.WithCancel(context.Background())
t.Cleanup(done)
{
sql := fmt.Sprintf(`CREATE SCHEMA "%s";`, schemaName)
_, err := p.DB().ExecContext(ctx, sql)
require.NoError(t, err)
}
t.Cleanup(func() {
sql := fmt.Sprintf(`DROP SCHEMA "%s" CASCADE;`, schemaName)
_, err := p.DB().ExecContext(ctx, sql)
require.NoError(t, err)
})
pgurl := setSearchPath(t, p.URL(), schemaName)
return &Postgres{
t: p.t,
ref: p.ref,
url: pgurl.String(),
}
}
func (p *Postgres) cloneFromReference() *Postgres {
newDBName := newUniqueHumanReadableDatabaseName(p.t)
@ -220,3 +259,14 @@ func open(t TestingT, dataSourceURL string) *sql.DB {
return db
}
func setSearchPath(t TestingT, pgURL string, schemaName string) *url.URL {
pgurl, err := url.Parse(pgURL)
require.NoError(t, err)
query := pgurl.Query()
query.Set("search_path", schemaName)
pgurl.RawQuery = query.Encode()
return pgurl
}

View File

@ -94,6 +94,86 @@ func TestNewPostgres(t *testing.T) {
})
}
func TestNewWithIsolatedSchema(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode")
}
t.Parallel()
t.Run("Successfully connect by URL and get version", func(t *testing.T) {
t.Parallel()
// Arrange
postgres := testingpg.NewWithIsolatedSchema(t)
ctx := context.Background()
dbPool, err := pgxpool.New(ctx, postgres.URL())
require.NoError(t, err)
// Act
var version string
err = dbPool.QueryRow(ctx, "SHOW search_path;").Scan(&version)
// Assert
require.NoError(t, err)
require.NotEmpty(t, version)
t.Log(version)
})
t.Run("Successfully obtained a version using a pre-configured conn", func(t *testing.T) {
t.Parallel()
// Arrange
postgres := testingpg.NewWithIsolatedSchema(t)
ctx := context.Background()
// Act
var version string
err := postgres.DB().QueryRowContext(ctx, "SHOW search_path;").Scan(&version)
// Assert
require.NoError(t, err)
require.NotEmpty(t, version)
t.Log(version)
})
t.Run("Changes are not visible in different instances", func(t *testing.T) {
t.Parallel()
// Arrange
postgres1 := testingpg.NewWithIsolatedSchema(t)
postgres2 := testingpg.NewWithIsolatedSchema(t)
ctx := context.Background()
// Act
const sqlStr = `CREATE TABLE "no_conflict" (id integer PRIMARY KEY)`
_, err1 := postgres1.DB().ExecContext(ctx, sqlStr)
_, err2 := postgres2.DB().ExecContext(ctx, sqlStr)
// Assert
require.NoError(t, err1)
require.NoError(t, err2, "databases must be isolated for each instance")
})
t.Run("URL is different at different instances", func(t *testing.T) {
t.Parallel()
// Arrange
postgres1 := testingpg.NewWithIsolatedSchema(t)
postgres2 := testingpg.NewWithIsolatedSchema(t)
// Act
url1 := postgres1.URL()
url2 := postgres2.URL()
// Assert
require.NotEqual(t, url1, url2)
})
}
func TestNewWithTransactionalCleanup(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode")

View File

@ -34,8 +34,7 @@ func (r *UserRepository) ReadUser(ctx context.Context, userID uuid.UUID) (User,
err := row.Scan(&user.ID, &user.Username, &user.CreatedAt)
if err != nil {
const format = "failed selection of User from database: %v"
const format = "failed selection of User from database: %w"
return User{}, fmt.Errorf(format, err)
}

View File

@ -9,3 +9,4 @@ package testing_go_code_with_postgres_test
// in the following files:
// - user_repository_with_isolated_database_test.go
// - user_repository_with_transactional_cleanup_test.go
// - user_repository_with_isolated_schema_test.go

View File

@ -0,0 +1,134 @@
// Copyright (c) 2023-2024 Vasiliy Vasilyuk. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package testing_go_code_with_postgres_test
import (
"context"
"database/sql"
"errors"
"testing"
"time"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
"github.com/golang-migrate/migrate/v4/source/iofs"
"github.com/xorcare/testing-go-code-with-postgres/migrations"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
rootpkg "github.com/xorcare/testing-go-code-with-postgres"
"github.com/xorcare/testing-go-code-with-postgres/testingpg"
)
func migrateDatabaseSchema(t *testing.T, pg *testingpg.Postgres) {
source, err := iofs.New(migrations.FS, ".")
require.NoError(t, err)
mi, err := migrate.NewWithSourceInstance(
"iofs",
source,
pg.URL(),
)
require.NoError(t, err)
err = mi.Up()
if !errors.Is(err, migrate.ErrNoChange) {
require.NoError(t, err)
}
}
func Test_Schema_UserRepository_CreateUser(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode")
}
t.Parallel()
newFullyFiledUser := func() rootpkg.User {
return rootpkg.User{
ID: uuid.New(),
Username: "gopher",
CreatedAt: time.Now().Truncate(time.Microsecond),
}
}
t.Run("Successfully created a User", func(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode")
}
t.Parallel()
// Arrange
pg := testingpg.NewWithIsolatedSchema(t)
migrateDatabaseSchema(t, pg)
repo := rootpkg.NewUserRepository(pg.DB())
user := newFullyFiledUser()
// Act
err := repo.CreateUser(context.Background(), user)
// Assert
require.NoError(t, err)
gotUser, err := repo.ReadUser(context.Background(), user.ID)
require.NoError(t, err)
require.Equal(t, user, gotUser)
})
t.Run("Cannot create a user with the same ID", func(t *testing.T) {
t.Parallel()
// Arrange
pg := testingpg.NewWithIsolatedSchema(t)
migrateDatabaseSchema(t, pg)
repo := rootpkg.NewUserRepository(pg.DB())
user := newFullyFiledUser()
err := repo.CreateUser(context.Background(), user)
require.NoError(t, err)
// Act
err = repo.CreateUser(context.Background(), user)
// Assert
require.Error(t, err)
require.Contains(t, err.Error(), "duplicate key value violates unique constraint")
})
}
func Test_Schema_UserRepository_ReadUser(t *testing.T) {
if testing.Short() {
t.Skip("skipping test in short mode")
}
t.Parallel()
t.Run("Get an error if the user does not exist", func(t *testing.T) {
t.Parallel()
// Arrange
pg := testingpg.NewWithIsolatedSchema(t)
migrateDatabaseSchema(t, pg)
repo := rootpkg.NewUserRepository(pg.DB())
// Act
_, err := repo.ReadUser(context.Background(), uuid.New())
// Assert
require.ErrorIs(t, err, sql.ErrNoRows)
})
}