Skip to content

Commit 55f8f06

Browse files
committed
Add example of tests with isolated schema
1 parent f445b5e commit 55f8f06

9 files changed

+330
-8
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ total: (statements) 100.0%
8080
[](https://github.com/xorcare/testing-go-code-with-postgres/blob/main/user_repository_with_isolated_database_test.go).
8181
- Example of integration testing with transaction cleanup for each testcase
8282
[](https://github.com/xorcare/testing-go-code-with-postgres/blob/main/user_repository_with_transactional_cleanup_test.go).
83+
- Example of integration testing with isolated schema for each testcase
84+
[](https://github.com/xorcare/testing-go-code-with-postgres/blob/main/user_repository_with_isolated_schema_test.go).
8385
- And example
8486
of [GitHub Actions](https://github.com/xorcare/testing-go-code-with-postgres/blob/main/.github/workflows/go.yml)
8587
and [Gitlab CI](https://github.com/xorcare/testing-go-code-with-postgres/blob/main/.gitlab-ci.yml).

go.mod

+7-2
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,26 @@ module github.com/xorcare/testing-go-code-with-postgres
33
go 1.22.0
44

55
require (
6+
github.com/golang-migrate/migrate/v4 v4.17.1
67
github.com/google/uuid v1.6.0
78
github.com/jackc/pgx/v5 v5.6.0
89
github.com/stretchr/testify v1.9.0
910
)
1011

1112
require (
1213
github.com/davecgh/go-spew v1.1.1 // indirect
14+
github.com/hashicorp/errwrap v1.1.0 // indirect
15+
github.com/hashicorp/go-multierror v1.1.1 // indirect
1316
github.com/jackc/pgpassfile v1.0.0 // indirect
1417
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
1518
github.com/jackc/puddle/v2 v2.2.1 // indirect
1619
github.com/kr/text v0.2.0 // indirect
20+
github.com/lib/pq v1.10.9 // indirect
1721
github.com/pmezard/go-difflib v1.0.0 // indirect
1822
github.com/rogpeppe/go-internal v1.10.0 // indirect
19-
golang.org/x/crypto v0.17.0 // indirect
20-
golang.org/x/sync v0.1.0 // indirect
23+
go.uber.org/atomic v1.7.0 // indirect
24+
golang.org/x/crypto v0.20.0 // indirect
25+
golang.org/x/sync v0.5.0 // indirect
2126
golang.org/x/text v0.14.0 // indirect
2227
gopkg.in/yaml.v3 v3.0.1 // indirect
2328
)

go.sum

+49-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,32 @@
1+
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0=
2+
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
3+
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=
4+
github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM=
15
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
26
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
37
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
48
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
9+
github.com/dhui/dktest v0.4.1 h1:/w+IWuDXVymg3IrRJCHHOkMK10m9aNVMOyD0X12YVTg=
10+
github.com/dhui/dktest v0.4.1/go.mod h1:DdOqcUpL7vgyP4GlF3X3w7HbSlz8cEQzwewPveYEQbA=
11+
github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8=
12+
github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w=
13+
github.com/docker/docker v24.0.9+incompatible h1:HPGzNmwfLZWdxHqK9/II92pyi1EpYKsAqcl4G0Of9v0=
14+
github.com/docker/docker v24.0.9+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
15+
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
16+
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
17+
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
18+
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
19+
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
20+
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
21+
github.com/golang-migrate/migrate/v4 v4.17.1 h1:4zQ6iqL6t6AiItphxJctQb3cFqWiSpMnX7wLTPnnYO4=
22+
github.com/golang-migrate/migrate/v4 v4.17.1/go.mod h1:m8hinFyWBn0SA4QKHuKh175Pm9wjmxj3S2Mia7dbXzM=
523
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
624
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
25+
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
26+
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
27+
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
28+
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
29+
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
730
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
831
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
932
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=
1639
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
1740
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
1841
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
42+
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
43+
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
44+
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
45+
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
46+
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
47+
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
48+
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
49+
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
50+
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
51+
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
52+
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
53+
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
1954
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
2055
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
2156
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
2560
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
2661
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
2762
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
28-
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
29-
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
30-
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
31-
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
63+
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
64+
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
65+
golang.org/x/crypto v0.20.0 h1:jmAMJJZXr5KiCw05dfYK9QnqaqKLYXijU23lsEdcQqg=
66+
golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ=
67+
golang.org/x/mod v0.11.0 h1:bUO06HqtnRcc/7l71XBe4WcqTZ+3AH1J59zWDDwLKgU=
68+
golang.org/x/mod v0.11.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
69+
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
70+
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
71+
golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE=
72+
golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
73+
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
74+
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
3275
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
3376
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
77+
golang.org/x/tools v0.10.0 h1:tvDr/iQoUqNdohiYm0LmmKcBk+q86lb9EprIUFhHHGg=
78+
golang.org/x/tools v0.10.0/go.mod h1:UJwyiVBsOA2uwvK/e5OY3GTpDUJriEd+/YlqAwLPmyM=
3479
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
3580
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
3681
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

migrations/embed.go

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package migrations
2+
3+
import "embed"
4+
5+
//go:embed *.up.sql
6+
var FS embed.FS

testingpg/testingpg.go

+50
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"time"
1818
"unicode"
1919

20+
_ "github.com/golang-migrate/migrate/v4/source/file"
2021
_ "github.com/jackc/pgx/v5/stdlib"
2122
"github.com/stretchr/testify/require"
2223
)
@@ -37,6 +38,10 @@ func NewWithIsolatedDatabase(t TestingT) *Postgres {
3738
return newPostgres(t, defaultPostgresURL).cloneFromReference()
3839
}
3940

41+
func NewWithIsolatedSchema(t TestingT) *Postgres {
42+
return newPostgres(t, defaultPostgresURL).createSchema(t)
43+
}
44+
4045
func NewWithTransactionalCleanup(t TestingT) interface {
4146
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
4247
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
@@ -108,6 +113,40 @@ func (p *Postgres) DB() *sql.DB {
108113
return p.sqlDB
109114
}
110115

116+
func (p *Postgres) createSchema(t TestingT) *Postgres {
117+
schemaName := newUniqueHumanReadableDatabaseName(p.t)
118+
119+
// Unclear why, but if the scheme contains letters of different case, the
120+
// tests stop working. At the moment I don't quite understand why this
121+
// happens, but converting to lower case fixes the problem.
122+
schemaName = strings.ToLower(schemaName)
123+
124+
ctx, done := context.WithCancel(context.Background())
125+
t.Cleanup(done)
126+
127+
{
128+
sql := fmt.Sprintf(`CREATE SCHEMA "%s";`, schemaName)
129+
130+
_, err := p.DB().ExecContext(ctx, sql)
131+
require.NoError(t, err)
132+
}
133+
134+
t.Cleanup(func() {
135+
sql := fmt.Sprintf(`DROP SCHEMA "%s" CASCADE;`, schemaName)
136+
137+
_, err := p.DB().ExecContext(ctx, sql)
138+
require.NoError(t, err)
139+
})
140+
141+
pgurl := setSearchPath(t, p.URL(), schemaName)
142+
143+
return &Postgres{
144+
t: p.t,
145+
ref: p.ref,
146+
url: pgurl.String(),
147+
}
148+
}
149+
111150
func (p *Postgres) cloneFromReference() *Postgres {
112151
newDBName := newUniqueHumanReadableDatabaseName(p.t)
113152

@@ -220,3 +259,14 @@ func open(t TestingT, dataSourceURL string) *sql.DB {
220259

221260
return db
222261
}
262+
263+
func setSearchPath(t TestingT, pgURL string, schemaName string) *url.URL {
264+
pgurl, err := url.Parse(pgURL)
265+
require.NoError(t, err)
266+
267+
query := pgurl.Query()
268+
query.Set("search_path", schemaName)
269+
pgurl.RawQuery = query.Encode()
270+
271+
return pgurl
272+
}

testingpg/testingpg_test.go

+80
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,86 @@ func TestNewPostgres(t *testing.T) {
9494
})
9595
}
9696

97+
func TestNewWithIsolatedSchema(t *testing.T) {
98+
if testing.Short() {
99+
t.Skip("skipping test in short mode")
100+
}
101+
102+
t.Parallel()
103+
104+
t.Run("Successfully connect by URL and get version", func(t *testing.T) {
105+
t.Parallel()
106+
107+
// Arrange
108+
postgres := testingpg.NewWithIsolatedSchema(t)
109+
110+
ctx := context.Background()
111+
dbPool, err := pgxpool.New(ctx, postgres.URL())
112+
require.NoError(t, err)
113+
114+
// Act
115+
var version string
116+
err = dbPool.QueryRow(ctx, "SHOW search_path;").Scan(&version)
117+
118+
// Assert
119+
require.NoError(t, err)
120+
require.NotEmpty(t, version)
121+
t.Log(version)
122+
})
123+
124+
t.Run("Successfully obtained a version using a pre-configured conn", func(t *testing.T) {
125+
t.Parallel()
126+
127+
// Arrange
128+
postgres := testingpg.NewWithIsolatedSchema(t)
129+
ctx := context.Background()
130+
131+
// Act
132+
var version string
133+
err := postgres.DB().QueryRowContext(ctx, "SHOW search_path;").Scan(&version)
134+
135+
// Assert
136+
require.NoError(t, err)
137+
require.NotEmpty(t, version)
138+
139+
t.Log(version)
140+
})
141+
142+
t.Run("Changes are not visible in different instances", func(t *testing.T) {
143+
t.Parallel()
144+
145+
// Arrange
146+
postgres1 := testingpg.NewWithIsolatedSchema(t)
147+
postgres2 := testingpg.NewWithIsolatedSchema(t)
148+
149+
ctx := context.Background()
150+
151+
// Act
152+
const sqlStr = `CREATE TABLE "no_conflict" (id integer PRIMARY KEY)`
153+
_, err1 := postgres1.DB().ExecContext(ctx, sqlStr)
154+
_, err2 := postgres2.DB().ExecContext(ctx, sqlStr)
155+
156+
// Assert
157+
require.NoError(t, err1)
158+
require.NoError(t, err2, "databases must be isolated for each instance")
159+
})
160+
161+
t.Run("URL is different at different instances", func(t *testing.T) {
162+
t.Parallel()
163+
164+
// Arrange
165+
postgres1 := testingpg.NewWithIsolatedSchema(t)
166+
postgres2 := testingpg.NewWithIsolatedSchema(t)
167+
168+
// Act
169+
url1 := postgres1.URL()
170+
url2 := postgres2.URL()
171+
172+
// Assert
173+
require.NotEqual(t, url1, url2)
174+
})
175+
}
176+
97177
func TestNewWithTransactionalCleanup(t *testing.T) {
98178
if testing.Short() {
99179
t.Skip("skipping test in short mode")

user_repository.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,7 @@ func (r *UserRepository) ReadUser(ctx context.Context, userID uuid.UUID) (User,
3434

3535
err := row.Scan(&user.ID, &user.Username, &user.CreatedAt)
3636
if err != nil {
37-
const format = "failed selection of User from database: %v"
38-
37+
const format = "failed selection of User from database: %w"
3938
return User{}, fmt.Errorf(format, err)
4039
}
4140

user_repository_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,4 @@ package testing_go_code_with_postgres_test
99
// in the following files:
1010
// - user_repository_with_isolated_database_test.go
1111
// - user_repository_with_transactional_cleanup_test.go
12+
// - user_repository_with_isolated_schema_test.go

0 commit comments

Comments
 (0)