Skip to content

Commit ea84ecb

Browse files
committed
add test containers for SSH, PostgreSQL, MySQL, MongoDB, and LocalStack
1 parent 532c330 commit ea84ecb

18 files changed

+1257
-8
lines changed

.github/workflows/ci.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
- name: set up go 1.20
1515
uses: actions/setup-go@v4
1616
with:
17-
go-version: "1.20"
17+
go-version: "1.22"
1818
id: go
1919

2020
- name: checkout

LICENSE

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
MIT License
22

3-
Copyright (c) 2023 go packages
3+
Copyright (c) 20235 Umputun
44

55
Permission is hereby granted, free of charge, to any person obtaining a copy
66
of this software and associated documentation files (the "Software"), to deal

README.md

+77
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,83 @@ Package `testutils` provides useful test helpers.
66

77
- `CaptureStdout`, `CaptureSterr` and `CaptureStdoutAndStderr`: capture stdout, stderr or both for testing purposes. All capture functions are not thread-safe if used in parallel tests, and usually it is better to pass a custom io.Writer to the function under test instead.
88

9+
- `containers`: provides test containers for integration testing:
10+
- `SSHTestContainer`: SSH server container for testing SSH connections and operations
11+
- `PostgresTestContainer`: PostgreSQL database container with automatic database creation
12+
- `MySQLTestContainer`: MySQL database container with automatic database creation
13+
- `MongoTestContainer`: MongoDB container with support for multiple versions (5, 6, 7)
14+
- `LocalstackTestContainer`: LocalStack container with S3 service for AWS testing
15+
916
## Install and update
1017

1118
`go get -u github.com/go-pkgz/testutils`
19+
20+
### Example usage:
21+
22+
```go
23+
// PostgreSQL test container
24+
func TestWithPostgres(t *testing.T) {
25+
ctx := context.Background()
26+
pg := containers.NewPostgresTestContainer(ctx, t)
27+
defer pg.Close(ctx)
28+
29+
db, err := sql.Open("postgres", pg.ConnectionString())
30+
require.NoError(t, err)
31+
defer db.Close()
32+
33+
// run your tests with the database
34+
}
35+
36+
// MySQL test container
37+
func TestWithMySQL(t *testing.T) {
38+
ctx := context.Background()
39+
mysql := containers.NewMySQLTestContainer(ctx, t)
40+
defer mysql.Close(ctx)
41+
42+
db, err := sql.Open("mysql", mysql.DSN())
43+
require.NoError(t, err)
44+
defer db.Close()
45+
46+
// run your tests with the database
47+
}
48+
49+
// MongoDB test container
50+
func TestWithMongo(t *testing.T) {
51+
ctx := context.Background()
52+
mongo := containers.NewMongoTestContainer(ctx, t, 7) // version 7
53+
defer mongo.Close(ctx)
54+
55+
coll := mongo.Collection("test_db")
56+
_, err := coll.InsertOne(ctx, bson.M{"test": "value"})
57+
require.NoError(t, err)
58+
}
59+
60+
// SSH test container
61+
func TestWithSSH(t *testing.T) {
62+
ctx := context.Background()
63+
ssh := containers.NewSSHTestContainer(ctx, t)
64+
defer ssh.Close(ctx)
65+
66+
// use ssh.Address() to get host:port
67+
// default user is "test"
68+
sshAddr := ssh.Address()
69+
}
70+
71+
// Localstack (S3) test container
72+
func TestWithS3(t *testing.T) {
73+
ctx := context.Background()
74+
ls := containers.NewLocalstackTestContainer(ctx, t)
75+
defer ls.Close(ctx)
76+
77+
s3Client, bucketName := ls.MakeS3Connection(ctx, t)
78+
79+
// put object example
80+
_, err := s3Client.PutObject(ctx, &s3.PutObjectInput{
81+
Bucket: aws.String(bucketName),
82+
Key: aws.String("test-key"),
83+
Body: strings.NewReader("test content"),
84+
})
85+
require.NoError(t, err)
86+
}
87+
```
88+

capture.go

+5-5
Original file line numberDiff line numberDiff line change
@@ -45,20 +45,20 @@ func CaptureStdoutAndStderr(t *testing.T, f func()) (o, e string) {
4545
var wg sync.WaitGroup
4646
wg.Add(2)
4747

48-
go func() {
48+
go func() { //nolint
4949
var buf bytes.Buffer
5050
wg.Done()
5151
if _, err := io.Copy(&buf, rOut); err != nil {
52-
t.Fatal(err)
52+
t.Fatal(err) //nolint
5353
}
5454
outCh <- buf.String()
5555
}()
5656

57-
go func() {
57+
go func() { //nolint
5858
var buf bytes.Buffer
5959
wg.Done()
6060
if _, err := io.Copy(&buf, rErr); err != nil {
61-
t.Fatal(err)
61+
t.Fatal(err) //nolint
6262
}
6363
errCh <- buf.String()
6464
}()
@@ -88,7 +88,7 @@ func capture(t *testing.T, out *os.File, f func()) string {
8888

8989
f()
9090

91-
w.Close()
91+
_ = w.Close()
9292

9393
var buf bytes.Buffer
9494
_, err = io.Copy(&buf, r)

containers/localstack.go

+85
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
package containers
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"sync/atomic"
7+
"testing"
8+
"time"
9+
10+
"github.com/aws/aws-sdk-go-v2/aws"
11+
"github.com/aws/aws-sdk-go-v2/config"
12+
"github.com/aws/aws-sdk-go-v2/credentials"
13+
"github.com/aws/aws-sdk-go-v2/service/s3"
14+
"github.com/stretchr/testify/require"
15+
"github.com/testcontainers/testcontainers-go"
16+
"github.com/testcontainers/testcontainers-go/wait"
17+
)
18+
19+
// LocalstackTestContainer is a wrapper around a testcontainers.Container that provides an S3 endpoint
20+
type LocalstackTestContainer struct {
21+
Container testcontainers.Container
22+
Endpoint string
23+
counter atomic.Int64
24+
}
25+
26+
// NewLocalstackTestContainer creates a new Localstack test container and returns a LocalstackTestContainer instance
27+
func NewLocalstackTestContainer(ctx context.Context, t *testing.T) *LocalstackTestContainer {
28+
req := testcontainers.ContainerRequest{
29+
Image: "localstack/localstack:3.0.0",
30+
ExposedPorts: []string{"4566/tcp"},
31+
Env: map[string]string{
32+
"SERVICES": "s3",
33+
"DEFAULT_REGION": "us-east-1",
34+
"AWS_ACCESS_KEY_ID": "test",
35+
"AWS_SECRET_ACCESS_KEY": "test",
36+
},
37+
WaitingFor: wait.ForAll(
38+
wait.ForListeningPort("4566/tcp"),
39+
wait.ForLog("Ready."),
40+
).WithDeadline(time.Minute),
41+
}
42+
43+
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
44+
ContainerRequest: req,
45+
Started: true,
46+
})
47+
require.NoError(t, err)
48+
49+
host, err := container.Host(ctx)
50+
require.NoError(t, err)
51+
52+
port, err := container.MappedPort(ctx, "4566")
53+
require.NoError(t, err)
54+
55+
endpoint := fmt.Sprintf("http://%s:%s", host, port.Port())
56+
return &LocalstackTestContainer{
57+
Container: container,
58+
Endpoint: endpoint,
59+
}
60+
}
61+
62+
// MakeS3Connection creates a new S3 connection using the test container endpoint and returns the connection and a bucket name
63+
func (lc *LocalstackTestContainer) MakeS3Connection(ctx context.Context, t *testing.T) (client *s3.Client, bucketName string) {
64+
cfg, err := config.LoadDefaultConfig(ctx,
65+
config.WithRegion("us-east-1"),
66+
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider("test", "test", "")),
67+
)
68+
require.NoError(t, err)
69+
70+
client = s3.NewFromConfig(cfg, func(o *s3.Options) {
71+
o.BaseEndpoint = aws.String(lc.Endpoint)
72+
o.UsePathStyle = true
73+
})
74+
75+
bucketName = fmt.Sprintf("test-bucket-%d-%d", time.Now().UnixNano(), lc.counter.Add(1))
76+
_, err = client.CreateBucket(ctx, &s3.CreateBucketInput{Bucket: aws.String(bucketName)})
77+
require.NoError(t, err)
78+
79+
return client, bucketName
80+
}
81+
82+
// Close terminates the container
83+
func (lc *LocalstackTestContainer) Close(ctx context.Context) error {
84+
return lc.Container.Terminate(ctx)
85+
}

containers/localstack_test.go

+111
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package containers
2+
3+
import (
4+
"context"
5+
"io"
6+
"strings"
7+
"testing"
8+
9+
"github.com/aws/aws-sdk-go-v2/aws"
10+
"github.com/aws/aws-sdk-go-v2/service/s3"
11+
"github.com/stretchr/testify/assert"
12+
"github.com/stretchr/testify/require"
13+
)
14+
15+
func TestLocalstackTestContainer(t *testing.T) {
16+
ctx := context.Background()
17+
18+
t.Run("create and cleanup container", func(t *testing.T) {
19+
ls := NewLocalstackTestContainer(ctx, t)
20+
defer func() { require.NoError(t, ls.Close(ctx)) }()
21+
22+
assert.NotEmpty(t, ls.Endpoint)
23+
assert.Contains(t, ls.Endpoint, "http://")
24+
})
25+
26+
t.Run("make s3 connection", func(t *testing.T) {
27+
ls := NewLocalstackTestContainer(ctx, t)
28+
defer func() { require.NoError(t, ls.Close(ctx)) }()
29+
30+
client, bucketName := ls.MakeS3Connection(ctx, t)
31+
32+
// verify we can use the connection
33+
buckets, err := client.ListBuckets(ctx, &s3.ListBucketsInput{})
34+
require.NoError(t, err)
35+
require.Len(t, buckets.Buckets, 1)
36+
assert.Equal(t, bucketName, *buckets.Buckets[0].Name)
37+
})
38+
39+
t.Run("s3 operations", func(t *testing.T) {
40+
ls := NewLocalstackTestContainer(ctx, t)
41+
defer func() { require.NoError(t, ls.Close(ctx)) }()
42+
43+
client, bucketName := ls.MakeS3Connection(ctx, t)
44+
45+
// test put object
46+
_, err := client.PutObject(ctx, &s3.PutObjectInput{
47+
Bucket: aws.String(bucketName),
48+
Key: aws.String("test-key"),
49+
Body: strings.NewReader("test content"),
50+
})
51+
require.NoError(t, err)
52+
53+
// test get object
54+
result, err := client.GetObject(ctx, &s3.GetObjectInput{
55+
Bucket: aws.String(bucketName),
56+
Key: aws.String("test-key"),
57+
})
58+
require.NoError(t, err)
59+
60+
content, err := io.ReadAll(result.Body)
61+
require.NoError(t, err)
62+
assert.Equal(t, "test content", string(content))
63+
})
64+
65+
t.Run("multiple connections to same container", func(t *testing.T) {
66+
ls := NewLocalstackTestContainer(ctx, t)
67+
defer func() { require.NoError(t, ls.Close(ctx)) }()
68+
69+
client1, bucket1 := ls.MakeS3Connection(ctx, t)
70+
client2, bucket2 := ls.MakeS3Connection(ctx, t)
71+
72+
// test operations with first client
73+
_, err := client1.PutObject(ctx, &s3.PutObjectInput{
74+
Bucket: aws.String(bucket1),
75+
Key: aws.String("test1"),
76+
Body: strings.NewReader("content1"),
77+
})
78+
require.NoError(t, err)
79+
80+
// test operations with second client
81+
_, err = client2.PutObject(ctx, &s3.PutObjectInput{
82+
Bucket: aws.String(bucket2),
83+
Key: aws.String("test2"),
84+
Body: strings.NewReader("content2"),
85+
})
86+
require.NoError(t, err)
87+
88+
// verify buckets are different
89+
assert.NotEqual(t, bucket1, bucket2)
90+
91+
// verify first client can access its content
92+
result1, err := client1.GetObject(ctx, &s3.GetObjectInput{
93+
Bucket: aws.String(bucket1),
94+
Key: aws.String("test1"),
95+
})
96+
require.NoError(t, err)
97+
content1, err := io.ReadAll(result1.Body)
98+
require.NoError(t, err)
99+
assert.Equal(t, "content1", string(content1))
100+
101+
// verify second client can access its content
102+
result2, err := client2.GetObject(ctx, &s3.GetObjectInput{
103+
Bucket: aws.String(bucket2),
104+
Key: aws.String("test2"),
105+
})
106+
require.NoError(t, err)
107+
content2, err := io.ReadAll(result2.Body)
108+
require.NoError(t, err)
109+
assert.Equal(t, "content2", string(content2))
110+
})
111+
}

0 commit comments

Comments
 (0)