Skip to content

Commit 0c9ef89

Browse files
authored
test: Improve integration test speed with persistent test infra #90 (#93)
* test: Test infra with shared testcontainers & persistent test containers * test: Refactor infra to support parallel tests * test: Fix issue finding .env.test at project root dir * test: RabbitMQ infra * refactor: Confirm integration test before starting testinfra * test: Apply testinfra to destination adapter integration tests * test: Refactor internal/mqs integration tests * test: Remove unused testutil code * test: Fix flaky test * docs: How to use test infra & integration test template * chore: Remove no-longer-valid comment
1 parent ea03147 commit 0c9ef89

File tree

19 files changed

+564
-353
lines changed

19 files changed

+564
-353
lines changed

.env.test

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
TEST_CLICKHOUSE_URL="localhost:39000"
2+
TEST_LOCALSTACK_URL="localhost:34566"
3+
TEST_RABBITMQ_URL="localhost:35672"

Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ down/uptrace:
2929
up/portal:
3030
cd internal/portal && npm install && npm run dev
3131

32+
up/test:
33+
docker-compose -f build/test/compose.yml up -d
34+
35+
down/test:
36+
docker-compose -f build/test/compose.yml down
37+
3238
test:
3339
go test $(TEST) $(TESTARGS)
3440

build/test/compose.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
name: "outpost-test"
2+
3+
services:
4+
clickhouse:
5+
image: clickhouse/clickhouse-server:24-alpine
6+
ports:
7+
- 39000:9000
8+
rabbitmq:
9+
image: rabbitmq:3-management
10+
ports:
11+
- 35672:5672
12+
- 45672:15672
13+
aws:
14+
image: localstack/localstack:latest
15+
environment:
16+
- SERVICES=sns,sts,sqs
17+
ports:
18+
- 34566:4566
19+
- 34571:4571

cmd/e2e/configs/basic.go

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ package configs
33
import (
44
"testing"
55

6-
"github.com/hookdeck/outpost/internal/clickhouse"
76
"github.com/hookdeck/outpost/internal/config"
8-
"github.com/hookdeck/outpost/internal/mqs"
7+
"github.com/hookdeck/outpost/internal/util/testinfra"
98
"github.com/hookdeck/outpost/internal/util/testutil"
109
)
1110

@@ -17,27 +16,13 @@ func Basic(t *testing.T) (*config.Config, func(), error) {
1716
}
1817
}
1918

20-
// Testcontainer
21-
chEndpoint, cleanupCH, err := testutil.StartTestContainerClickHouse()
22-
if err != nil {
23-
return nil, cleanup, err
24-
}
25-
cleanupFns = append(cleanupFns, cleanupCH)
26-
27-
awsEndpoint, cleanupAWS, err := testutil.StartTestcontainerLocalstack()
28-
if err != nil {
29-
return nil, cleanup, err
30-
}
31-
cleanupFns = append(cleanupFns, cleanupAWS)
19+
t.Cleanup(testinfra.Start(t))
3220

3321
// Config
3422
redisConfig := testutil.CreateTestRedisConfig(t)
35-
clickHouseConfig := &clickhouse.ClickHouseConfig{
36-
Addr: chEndpoint,
37-
Username: "default",
38-
Password: "",
39-
Database: "default",
40-
}
23+
clickHouseConfig := testinfra.NewClickHouseConfig(t)
24+
deliveryMQConfig := testinfra.NewMQAWSConfig(t, nil)
25+
logMQConfig := testinfra.NewMQAWSConfig(t, nil)
4126

4227
return &config.Config{
4328
Hostname: "outpost",
@@ -49,11 +34,11 @@ func Basic(t *testing.T) (*config.Config, func(), error) {
4934
PortalProxyURL: "",
5035
Topics: testutil.TestTopics,
5136
Redis: redisConfig,
52-
ClickHouse: clickHouseConfig,
37+
ClickHouse: &clickHouseConfig,
5338
OpenTelemetry: nil,
5439
PublishQueueConfig: nil,
55-
DeliveryQueueConfig: &mqs.QueueConfig{AWSSQS: &mqs.AWSSQSConfig{Endpoint: awsEndpoint, Region: "us-east-1", ServiceAccountCredentials: "test:test:", Topic: "delivery"}},
56-
LogQueueConfig: &mqs.QueueConfig{AWSSQS: &mqs.AWSSQSConfig{Endpoint: awsEndpoint, Region: "us-east-1", ServiceAccountCredentials: "test:test:", Topic: "log"}},
40+
DeliveryQueueConfig: &deliveryMQConfig,
41+
LogQueueConfig: &logMQConfig,
5742
PublishMaxConcurrency: 3,
5843
DeliveryMaxConcurrency: 3,
5944
LogMaxConcurrency: 3,

docs/contributing/test.md

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ $ TESTARGS='-v -run "TestJWT"'' make test
3939
# go test $(go list ./...) -v -run "TestJWT"
4040
```
4141
42-
Keep in mind you can't use `-run "Test..."` along with `make test/integration` as the integration test already specify integration tests with `-run` option. However, since you're already specifying which test to run, I assume this is a non-issue.
42+
Keep in mind you can't use `-run "Test..."` along with `make test/integration` as the integration test already specify integration tests with `-run` option. However, since you're already specifying which test to run, we assume this is a non-issue.
4343
4444
## Coverage
4545
@@ -62,3 +62,47 @@ Running the coverage test command above will generate the `coverage.out` file. Y
6262
$ make test/coverage/html
6363
# go tool cover -html=coverage.out
6464
```
65+
66+
## Integration & E2E Tests
67+
68+
When running integration & e2e tests, we often times require some test infrastructure such as ClickHouse, LocalStack, RabbitMQ, etc. We use [Testcontainers](https://testcontainers.com/) for that. It usually takes a few seconds (10s or so) to spawn the necessary containers. To improve the feedback loop, you can run a persistent test infrastructure and skip spawning testcontainers.
69+
70+
To run the test infrastructure:
71+
72+
```sh
73+
$ make up/test
74+
75+
## to take the test infra down
76+
# $ make down/test
77+
```
78+
79+
It will run a Docker compose stack called `outpost-test` which runs the necessary services at ports ":30000 + port". For example, ClickHouse usually runs on port `:9000`, so in the test infra it will run on port `:39000`.
80+
81+
From here, you can provide env variable `TESTINFRA=1` to tell the test suite to use these services instead of spawning testcontainers.
82+
83+
```sh
84+
$ TESTINFRA=1 make test
85+
```
86+
87+
Tip: You can `$ export TESTINFRA=1` to use the test infra for the whole terminal session.
88+
89+
### Integration Test Template
90+
91+
Here's a short template for how you can write integration tests that require an external test infra:
92+
93+
```golang
94+
// Integration test should always start with "TestIntegration...() {}"
95+
func TestIntegrationMyIntegrationTest(t *testing.T) {
96+
t.Parallel()
97+
98+
// call testinfra.Start(t) to signal that you require the test infra.
99+
// This helps the test runner properly terminate resources at the end.
100+
t.Cleanup(testinfra.Start(t))
101+
102+
// use whichever infra you need
103+
chConfig := testinfra.NewClickHouseConfig(t)
104+
awsMQConfig := testinfra.NewMQAWSConfig(t, attributesMap)
105+
rabbitmqConfig := testinfra.NewMQRabbitMQConfig(t)
106+
// ...
107+
}
108+
```

internal/destinationadapter/adapters/aws/aws_test.go

Lines changed: 13 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,17 @@ package aws_test
33
import (
44
"context"
55
"encoding/json"
6-
"errors"
76
"log"
87
"testing"
98
"time"
109

11-
"github.com/aws/aws-sdk-go-v2/aws"
12-
"github.com/aws/aws-sdk-go-v2/config"
13-
"github.com/aws/aws-sdk-go-v2/credentials"
1410
"github.com/aws/aws-sdk-go-v2/service/sqs"
1511
"github.com/aws/aws-sdk-go-v2/service/sqs/types"
16-
"github.com/aws/smithy-go"
1712
"github.com/google/uuid"
1813
"github.com/hookdeck/outpost/internal/destinationadapter/adapters"
1914
awsadapter "github.com/hookdeck/outpost/internal/destinationadapter/adapters/aws"
20-
"github.com/hookdeck/outpost/internal/util/testutil"
15+
"github.com/hookdeck/outpost/internal/util/awsutil"
16+
"github.com/hookdeck/outpost/internal/util/testinfra"
2117
"github.com/stretchr/testify/assert"
2218
"github.com/stretchr/testify/require"
2319
)
@@ -111,41 +107,21 @@ func TestAWSDestination_Validate(t *testing.T) {
111107
}
112108

113109
func TestIntegrationAWSDestination_Publish(t *testing.T) {
114-
if testing.Short() {
115-
t.Skip("skipping integration test")
116-
}
117-
118110
t.Parallel()
111+
t.Cleanup(testinfra.Start(t))
119112

120-
// Setup SQS
121-
awsEndpoint, terminate, err := testutil.StartTestcontainerLocalstack()
122-
require.Nil(t, err)
123-
defer terminate()
124-
125-
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
126-
defer cancel()
127-
queueName := "destination_sqs_queue"
128-
awsRegion := "eu-central-1"
129-
130-
sdkConfig, err := config.LoadDefaultConfig(ctx,
131-
config.WithRegion(awsRegion),
132-
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider("test", "test", "")),
133-
)
134-
require.Nil(t, err)
135-
sqsClient := sqs.NewFromConfig(sdkConfig, func(o *sqs.Options) {
136-
o.BaseEndpoint = aws.String(awsEndpoint)
137-
})
138-
queueURL, err := ensureQueue(ctx, sqsClient, queueName)
139-
require.Nil(t, err)
140-
141-
// Setup Destination & Event
113+
mq := testinfra.NewMQAWSConfig(t, nil)
114+
sqsClient, err := awsutil.SQSClientFromConfig(context.Background(), mq.AWSSQS)
115+
require.NoError(t, err)
116+
queueURL, err := awsutil.EnsureQueue(context.Background(), sqsClient, mq.AWSSQS.Topic, nil)
117+
require.NoError(t, err)
142118
awsdestination := awsadapter.New()
143119

144120
destination := adapters.DestinationAdapterValue{
145121
ID: uuid.New().String(),
146122
Type: "aws",
147123
Config: map[string]string{
148-
"endpoint": awsEndpoint,
124+
"endpoint": mq.AWSSQS.Endpoint,
149125
"queue_url": queueURL,
150126
},
151127
Credentials: map[string]string{
@@ -158,6 +134,9 @@ func TestIntegrationAWSDestination_Publish(t *testing.T) {
158134
errchan := make(chan error)
159135
msgchan := make(chan *types.Message)
160136

137+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
138+
defer cancel()
139+
161140
go func() {
162141
for {
163142
out, err := sqsClient.ReceiveMessage(ctx, &sqs.ReceiveMessageInput{
@@ -212,8 +191,7 @@ func TestIntegrationAWSDestination_Publish(t *testing.T) {
212191
"mykey": "myvaluee",
213192
},
214193
}
215-
err = awsdestination.Publish(context.Background(), destination, event)
216-
require.Nil(t, err)
194+
require.NoError(t, awsdestination.Publish(context.Background(), destination, event))
217195

218196
// Assert
219197
log.Println("waiting for msg...")
@@ -237,29 +215,3 @@ func TestIntegrationAWSDestination_Publish(t *testing.T) {
237215
assert.Equal(t, "anothermetadatavalue", *msg.MessageAttributes["another_metadata"].StringValue)
238216
}
239217
}
240-
241-
func ensureQueue(ctx context.Context, sqsClient *sqs.Client, queueName string) (string, error) {
242-
queue, err := sqsClient.GetQueueUrl(ctx, &sqs.GetQueueUrlInput{
243-
QueueName: aws.String(queueName),
244-
})
245-
if err != nil {
246-
var apiErr smithy.APIError
247-
if errors.As(err, &apiErr) {
248-
switch apiErr.(type) {
249-
case *types.QueueDoesNotExist:
250-
log.Println("Queue does not exist, creating...")
251-
createdQueue, err := sqsClient.CreateQueue(ctx, &sqs.CreateQueueInput{
252-
QueueName: aws.String(queueName),
253-
})
254-
if err != nil {
255-
return "", err
256-
}
257-
return *createdQueue.QueueUrl, nil
258-
default:
259-
return "", err
260-
}
261-
}
262-
return "", err
263-
}
264-
return *queue.QueueUrl, nil
265-
}

internal/destinationadapter/adapters/rabbitmq/rabbitmq_test.go

Lines changed: 17 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/google/uuid"
1111
"github.com/hookdeck/outpost/internal/destinationadapter/adapters"
1212
"github.com/hookdeck/outpost/internal/destinationadapter/adapters/rabbitmq"
13+
"github.com/hookdeck/outpost/internal/util/testinfra"
1314
"github.com/hookdeck/outpost/internal/util/testutil"
1415
"github.com/rabbitmq/amqp091-go"
1516
"github.com/stretchr/testify/assert"
@@ -123,34 +124,22 @@ func TestRabbitMQDestination_Publish(t *testing.T) {
123124
}
124125

125126
func TestIntegrationRabbitMQDestination_Publish(t *testing.T) {
126-
if testing.Short() {
127-
t.Skip("skipping integration test")
128-
}
129-
130127
t.Parallel()
128+
t.Cleanup(testinfra.Start(t))
131129

132-
rabbitmqURL, terminate, err := testutil.StartTestcontainerRabbitMQ()
133-
require.Nil(t, err)
134-
defer terminate()
135-
136-
RABBIT_SERVER_URL := rabbitmqURL
137-
const (
138-
RABBIT_EXCHANGE = "destination_exchange"
139-
RABBIT_QUEUE = "destination_queue_test"
140-
)
141-
130+
mq := testinfra.NewMQRabbitMQConfig(t)
142131
rabbitmqDestination := rabbitmq.New()
143132

144133
destination := adapters.DestinationAdapterValue{
145134
ID: uuid.New().String(),
146135
Type: "rabbitmq",
147136
Config: map[string]string{
148-
"server_url": testutil.ExtractRabbitURL(RABBIT_SERVER_URL),
149-
"exchange": RABBIT_EXCHANGE,
137+
"server_url": testutil.ExtractRabbitURL(mq.RabbitMQ.ServerURL),
138+
"exchange": mq.RabbitMQ.Exchange,
150139
},
151140
Credentials: map[string]string{
152-
"username": testutil.ExtractRabbitUsername(RABBIT_SERVER_URL),
153-
"password": testutil.ExtractRabbitPassword(RABBIT_SERVER_URL),
141+
"username": testutil.ExtractRabbitUsername(mq.RabbitMQ.ServerURL),
142+
"password": testutil.ExtractRabbitPassword(mq.RabbitMQ.ServerURL),
154143
},
155144
}
156145

@@ -174,44 +163,19 @@ func TestIntegrationRabbitMQDestination_Publish(t *testing.T) {
174163
cancelChan := make(chan bool)
175164
msgChan := make(chan *amqp091.Delivery)
176165
go func() {
177-
conn, _ := amqp091.Dial(RABBIT_SERVER_URL)
166+
conn, _ := amqp091.Dial(mq.RabbitMQ.ServerURL)
178167
defer conn.Close()
179168
ch, _ := conn.Channel()
180169
defer ch.Close()
181170

182-
ch.ExchangeDeclare(
183-
RABBIT_EXCHANGE, // name
184-
"topic", // type
185-
true, // durable
186-
false, // auto-deleted
187-
false, // internal
188-
false, // no-wait
189-
nil, // arguments
190-
)
191-
q, _ := ch.QueueDeclare(
192-
RABBIT_QUEUE, // name
193-
false, // durable
194-
false, // delete when unused
195-
true, // exclusive
196-
false, // no-wait
197-
nil, // arguments
198-
)
199-
ch.QueueBind(
200-
q.Name, // queue name
201-
"", // routing key
202-
RABBIT_EXCHANGE, // exchange
203-
false,
204-
nil,
205-
)
206-
207171
msgs, _ := ch.Consume(
208-
RABBIT_QUEUE, // queue
209-
"", // consumer
210-
true, // auto-ack
211-
false, // exclusive
212-
false, // no-local
213-
false, // no-wait
214-
nil, // args
172+
mq.RabbitMQ.Queue, // queue
173+
"", // consumer
174+
true, // auto-ack
175+
false, // exclusive
176+
false, // no-local
177+
false, // no-wait
178+
nil, // args
215179
)
216180

217181
log.Println("ready to receive messages")
@@ -229,8 +193,7 @@ func TestIntegrationRabbitMQDestination_Publish(t *testing.T) {
229193

230194
<-readyChan
231195
log.Println("publishing message")
232-
err = rabbitmqDestination.Publish(context.Background(), destination, event)
233-
assert.Nil(t, err)
196+
assert.NoError(t, rabbitmqDestination.Publish(context.Background(), destination, event))
234197

235198
func() {
236199
time.Sleep(time.Second / 2)
@@ -244,10 +207,7 @@ func TestIntegrationRabbitMQDestination_Publish(t *testing.T) {
244207
}
245208
log.Println("message received", msg)
246209
body := make(map[string]interface{})
247-
err = json.Unmarshal(msg.Body, &body)
248-
if err != nil {
249-
t.Fatal(err)
250-
}
210+
require.NoError(t, json.Unmarshal(msg.Body, &body))
251211
assert.Equal(t, event.Data, body)
252212
// metadata
253213
assert.Equal(t, "metadatavalue", msg.Headers["my_metadata"])

0 commit comments

Comments
 (0)