Skip to content

Commit 1d37d62

Browse files
committed
Add example of tests with transactional cleanup
1 parent cd479ac commit 1d37d62

6 files changed

+222
-11
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ total: (statements) 100.0%
7878
package.
7979
- Example of integration testing with isolated database for each testcase
8080
[](https://github.com/xorcare/testing-go-code-with-postgres/blob/main/user_repository_with_isolated_database_test.go).
81+
- Example of integration testing with transaction cleanup for each testcase
82+
[](https://github.com/xorcare/testing-go-code-with-postgres/blob/main/user_repository_with_transactional_cleanup_test.go).
8183
- And example
8284
of [GitHub Actions](https://github.com/xorcare/testing-go-code-with-postgres/blob/main/.github/workflows/go.yml)
8385
and [Gitlab CI](https://github.com/xorcare/testing-go-code-with-postgres/blob/main/.gitlab-ci.yml).

docker-compose.yml

+24-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ services:
88
POSTGRES_DB: postgres
99
POSTGRES_PASSWORD: postgres
1010
POSTGRES_USER: postgres
11-
POSTGRES_MULTIPLE_DATABASES: reference
11+
POSTGRES_MULTIPLE_DATABASES: reference, transaction
1212
healthcheck:
1313
test: pg_isready --username "postgres" --dbname "reference"
1414
interval: 1s
@@ -21,7 +21,7 @@ services:
2121
tmpfs:
2222
- /var/lib/postgresql/data:rw # Necessary to speed up integration tests.
2323

24-
migrate:
24+
migrate-reference:
2525
image: migrate/migrate:v4.16.2
2626
command: >
2727
-source 'file:///migrations'
@@ -31,3 +31,25 @@ services:
3131
condition: service_healthy
3232
volumes:
3333
- ./migrations:/migrations:ro
34+
35+
migrate-transaction:
36+
image: migrate/migrate:v4.16.2
37+
command: >
38+
-source 'file:///migrations'
39+
-database 'postgresql://postgres:postgres@postgres:5432/transaction?sslmode=disable' up
40+
depends_on:
41+
postgres:
42+
condition: service_healthy
43+
volumes:
44+
- ./migrations:/migrations:ro
45+
46+
migrate:
47+
image: postgres:15.3-alpine3.18
48+
command: echo 'All migrations have been successfully applied!'
49+
depends_on:
50+
postgres:
51+
condition: service_healthy
52+
migrate-reference:
53+
condition: service_completed_successfully
54+
migrate-transaction:
55+
condition: service_completed_successfully

testingpg/testingpg.go

+45-6
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,36 @@ type TestingT interface {
3131
Failed() bool
3232
}
3333

34+
const defaultPostgresURL = "postgresql://postgres:postgres@localhost:32260/postgres?sslmode=disable"
35+
3436
func NewWithIsolatedDatabase(t TestingT) *Postgres {
35-
return newPostgres(t).cloneFromReference()
37+
return newPostgres(t, defaultPostgresURL).cloneFromReference()
38+
}
39+
40+
func NewWithTransactionalCleanup(t TestingT) interface {
41+
ExecContext(ctx context.Context, query string, args ...any) (sql.Result, error)
42+
QueryRowContext(ctx context.Context, query string, args ...any) *sql.Row
43+
} {
44+
// databaseName a separate database is used for transactional cleanup.
45+
const databaseName = "transaction"
46+
47+
postgres := newPostgres(t, defaultPostgresURL)
48+
postgres = postgres.replaceDBName(databaseName)
49+
50+
ctx, done := context.WithCancel(context.Background())
51+
t.Cleanup(done)
52+
53+
tx, err := postgres.DB().BeginTx(ctx, &sql.TxOptions{
54+
Isolation: sql.LevelRepeatableRead,
55+
ReadOnly: false,
56+
})
57+
require.NoError(t, err)
58+
59+
t.Cleanup(func() {
60+
require.NoError(t, tx.Rollback())
61+
})
62+
63+
return tx
3664
}
3765

3866
type Postgres struct {
@@ -45,10 +73,10 @@ type Postgres struct {
4573
sqlDBOnce sync.Once
4674
}
4775

48-
func newPostgres(t TestingT) *Postgres {
76+
func newPostgres(t TestingT, defaultPostgresURL string) *Postgres {
4977
urlStr := os.Getenv("TESTING_DB_URL")
5078
if urlStr == "" {
51-
urlStr = "postgresql://postgres:postgres@localhost:32260/postgres?sslmode=disable"
79+
urlStr = defaultPostgresURL
5280

5381
const format = "env TESTING_DB_URL is empty, used default value: %s"
5482

@@ -105,11 +133,22 @@ func (p *Postgres) cloneFromReference() *Postgres {
105133
require.NoError(p.t, err)
106134
})
107135

136+
return p.replaceDBName(newDBName)
137+
}
138+
139+
func (p *Postgres) replaceDBName(newDBName string) *Postgres {
140+
o := p.clone()
141+
o.url = replaceDBName(p.t, p.URL(), newDBName)
142+
143+
return o
144+
}
145+
146+
func (p *Postgres) clone() *Postgres {
108147
return &Postgres{
109148
t: p.t,
110149

111-
url: replaceDBName(p.t, p.URL(), newDBName),
112-
ref: newDBName,
150+
url: p.url,
151+
ref: p.ref,
113152
}
114153
}
115154

@@ -176,7 +215,7 @@ func open(t TestingT, dataSourceURL string) *sql.DB {
176215

177216
// Automatically close connection after the test is completed.
178217
t.Cleanup(func() {
179-
db.Close()
218+
require.NoError(t, db.Close())
180219
})
181220

182221
return db

testingpg/testingpg_test.go

+54-3
Original file line numberDiff line numberDiff line change
@@ -69,9 +69,9 @@ func TestNewPostgres(t *testing.T) {
6969
ctx := context.Background()
7070

7171
// Act
72-
const sql = `CREATE TABLE "no_conflict" (id integer PRIMARY KEY)`
73-
_, err1 := postgres1.DB().ExecContext(ctx, sql)
74-
_, err2 := postgres2.DB().ExecContext(ctx, sql)
72+
const sqlStr = `CREATE TABLE "no_conflict" (id integer PRIMARY KEY)`
73+
_, err1 := postgres1.DB().ExecContext(ctx, sqlStr)
74+
_, err2 := postgres2.DB().ExecContext(ctx, sqlStr)
7575

7676
// Assert
7777
require.NoError(t, err1)
@@ -93,3 +93,54 @@ func TestNewPostgres(t *testing.T) {
9393
require.NotEqual(t, url1, url2)
9494
})
9595
}
96+
97+
func TestNewWithTransactionalCleanup(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 obtained a version", func(t *testing.T) {
105+
t.Parallel()
106+
107+
// Arrange
108+
tx := testingpg.NewWithTransactionalCleanup(t)
109+
ctx := context.Background()
110+
111+
// Act
112+
var version string
113+
err := tx.QueryRowContext(ctx, "SELECT version();").Scan(&version)
114+
115+
// Assert
116+
require.NoError(t, err)
117+
require.NotEmpty(t, version)
118+
119+
t.Log(version)
120+
})
121+
122+
t.Run("Changes are not visible in different instances", func(t *testing.T) {
123+
t.Parallel()
124+
125+
// Arrange
126+
ctx := context.Background()
127+
const sqlStr = `CREATE TABLE "no_conflict" (id integer PRIMARY KEY)`
128+
129+
t.Run("Arrange", func(t *testing.T) {
130+
tx := testingpg.NewWithTransactionalCleanup(t)
131+
_, err := tx.ExecContext(ctx, sqlStr)
132+
require.NoError(t, err)
133+
})
134+
135+
var err error
136+
137+
// Act
138+
t.Run("Act", func(t *testing.T) {
139+
tx := testingpg.NewWithTransactionalCleanup(t)
140+
_, err = tx.ExecContext(ctx, sqlStr)
141+
})
142+
143+
// Assert
144+
require.NoError(t, err, "side effects must be isolated for each instance")
145+
})
146+
}

user_repository_test.go

+1
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ package testing_go_code_with_postgres_test
44
// database cleanup, this file is empty. The specific tests can be found
55
// in the following files:
66
// - user_repository_with_isolated_database_test.go
7+
// - user_repository_with_transactional_cleanup_test.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright (c) 2023 Vasiliy Vasilyuk. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package testing_go_code_with_postgres_test
6+
7+
import (
8+
"context"
9+
"testing"
10+
"time"
11+
12+
"github.com/google/uuid"
13+
"github.com/stretchr/testify/require"
14+
15+
rootpkg "github.com/xorcare/testing-go-code-with-postgres"
16+
"github.com/xorcare/testing-go-code-with-postgres/testingpg"
17+
)
18+
19+
func Test_Transactional_UserRepository_CreateUser(t *testing.T) {
20+
if testing.Short() {
21+
t.Skip("skipping test in short mode")
22+
}
23+
24+
t.Parallel()
25+
26+
newFullyFiledUser := func() rootpkg.User {
27+
return rootpkg.User{
28+
ID: uuid.New(),
29+
Username: "gopher",
30+
CreatedAt: time.Now().Truncate(time.Microsecond),
31+
}
32+
}
33+
34+
t.Run("Successfully created a User", func(t *testing.T) {
35+
t.Parallel()
36+
37+
// Arrange
38+
db := testingpg.NewWithTransactionalCleanup(t)
39+
repo := rootpkg.NewUserRepository(db)
40+
41+
user := newFullyFiledUser()
42+
43+
// Act
44+
err := repo.CreateUser(context.Background(), user)
45+
46+
// Assert
47+
require.NoError(t, err)
48+
49+
gotUser, err := repo.ReadUser(context.Background(), user.ID)
50+
require.NoError(t, err)
51+
52+
require.Equal(t, user, gotUser)
53+
})
54+
55+
t.Run("Cannot create a user with the same ID", func(t *testing.T) {
56+
t.Parallel()
57+
58+
// Arrange
59+
db := testingpg.NewWithTransactionalCleanup(t)
60+
repo := rootpkg.NewUserRepository(db)
61+
62+
user := newFullyFiledUser()
63+
64+
err := repo.CreateUser(context.Background(), user)
65+
require.NoError(t, err)
66+
67+
// Act
68+
err = repo.CreateUser(context.Background(), user)
69+
70+
// Assert
71+
require.Error(t, err)
72+
require.Contains(t, err.Error(), "duplicate key value violates unique constraint")
73+
})
74+
}
75+
76+
func Test_Transactional_UserRepository_ReadUser(t *testing.T) {
77+
if testing.Short() {
78+
t.Skip("skipping test in short mode")
79+
}
80+
81+
t.Parallel()
82+
83+
t.Run("Get an error if the user does not exist", func(t *testing.T) {
84+
t.Parallel()
85+
86+
// Arrange
87+
db := testingpg.NewWithTransactionalCleanup(t)
88+
repo := rootpkg.NewUserRepository(db)
89+
90+
// Act
91+
_, err := repo.ReadUser(context.Background(), uuid.New())
92+
93+
// Assert
94+
require.Error(t, err)
95+
})
96+
}

0 commit comments

Comments
 (0)