Skip to content

Commit d9525fa

Browse files
committed
Introduce test helpers and run integration tests separately
This introduces a `test` package which contains some test helpers. It also introduces a convention whereby tests with "Integration" in the name are treated as integration tests and run separately from the rest of the test suite using `-skip` and `-run` arguments to `go test` as needed. Integration tests that access an external shared resource such as Redis often need to be run serially, so providing a common pattern for them them will allow us to run non-integration tests as fast as possible.
1 parent 0fda39f commit d9525fa

File tree

7 files changed

+153
-111
lines changed

7 files changed

+153
-111
lines changed

go.mod

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module github.com/replicate/go
33
go 1.22
44

55
require (
6+
github.com/alicebob/miniredis/v2 v2.33.0
67
github.com/getsentry/sentry-go v0.28.1
78
github.com/go-logr/logr v1.4.2
89
github.com/go-redis/redismock/v9 v9.2.0
@@ -32,6 +33,7 @@ require (
3233
require (
3334
cloud.google.com/go/compute/metadata v0.3.0 // indirect
3435
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.23.0 // indirect
36+
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a // indirect
3537
github.com/beorn7/perks v1.0.1 // indirect
3638
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
3739
github.com/cespare/xxhash/v2 v2.3.0 // indirect
@@ -58,6 +60,7 @@ require (
5860
github.com/prometheus/client_model v0.6.1 // indirect
5961
github.com/prometheus/common v0.53.0 // indirect
6062
github.com/prometheus/procfs v0.15.0 // indirect
63+
github.com/yuin/gopher-lua v1.1.1 // indirect
6164
go.opentelemetry.io/proto/otlp v1.2.0 // indirect
6265
go.uber.org/multierr v1.11.0 // indirect
6366
golang.org/x/exp v0.0.0-20240416160154-fe59bbe5cc7f // indirect

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@ cloud.google.com/go/compute/metadata v0.3.0 h1:Tz+eQXMEqDIKRsmY3cHTL6FVaynIjX2Qx
22
cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
33
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.23.0 h1:yRhWveg9NbJcJYoJL4FoSauT2dxnt4N9MIAJ7tvU/mQ=
44
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.23.0/go.mod h1:p2puVVSKjQ84Qb1gzw2XHLs34WQyHTYFZLaVxypAFYs=
5+
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a h1:HbKu58rmZpUGpz5+4FfNmIU+FmZg2P3Xaj2v2bfNWmk=
6+
github.com/alicebob/gopher-json v0.0.0-20200520072559-a9ecdc9d1d3a/go.mod h1:SGnFV6hVsYE877CKEZ6tDNTjaSXYUk6QqoIK6PrAtcc=
7+
github.com/alicebob/miniredis/v2 v2.33.0 h1:uvTF0EDeu9RLnUEG27Db5I68ESoIxTiXbNUiji6lZrA=
8+
github.com/alicebob/miniredis/v2 v2.33.0/go.mod h1:MhP4a3EU7aENRi9aO+tHfTBZicLqQevyi/DJpoj6mi0=
59
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
610
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
711
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
@@ -120,6 +124,8 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT
120124
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
121125
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0 h1:3UeQBvD0TFrlVjOeLOBz+CPAI8dnbqNSVwUwRrkp7vQ=
122126
github.com/wsxiaoys/terminal v0.0.0-20160513160801-0940f3fc43a0/go.mod h1:IXCdmsXIht47RaVFLEdVnh1t+pgYtTAhQGj73kz+2DM=
127+
github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M=
128+
github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw=
123129
go.opentelemetry.io/contrib/detectors/gcp v1.27.0 h1:eVfDeFAPnMFZUhNdDZ/BbpEmC7/xxDKTSba5NhJH88s=
124130
go.opentelemetry.io/contrib/detectors/gcp v1.27.0/go.mod h1:amd+4uZxqJAUx7zI1JvygUtAc2EVWtQeyz8D+3161SQ=
125131
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.52.0 h1:9l89oX4ba9kHbBol3Xin3leYJ+252h0zszDtBwyKe2A=

lock/lock_test.go

Lines changed: 8 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,15 @@ import (
44
"context"
55
"errors"
66
"fmt"
7-
"os"
87
"sync"
98
"testing"
109
"time"
1110

1211
"github.com/go-redis/redismock/v9"
13-
"github.com/redis/go-redis/v9"
1412
"github.com/stretchr/testify/assert"
1513
"github.com/stretchr/testify/require"
14+
15+
"github.com/replicate/go/test"
1616
)
1717

1818
func TestLockerTryAcquireReturnsLockWhenSetSucceeds(t *testing.T) {
@@ -151,18 +151,9 @@ func TestLockReleaseReturnsRedisErrors(t *testing.T) {
151151
}
152152

153153
func TestLockAcquireIntegration(t *testing.T) {
154-
redisURL := os.Getenv("REDIS_URL")
155-
if redisURL == "" {
156-
t.Skip("REDIS_URL is not set")
157-
}
158-
159-
ctx := context.Background()
160-
161-
opts, err := redis.ParseURL(redisURL)
162-
require.NoError(t, err)
163-
164-
client := redis.NewClient(opts)
165-
locker := Locker{Client: client}
154+
ctx := test.Context(t)
155+
rdb := test.Redis(ctx, t)
156+
locker := Locker{Client: rdb}
166157

167158
require.NoError(t, locker.Prepare(ctx))
168159

@@ -209,18 +200,9 @@ func TestLockAcquireIntegration(t *testing.T) {
209200
}
210201

211202
func TestLockTryAcquireIntegration(t *testing.T) {
212-
redisURL := os.Getenv("REDIS_URL")
213-
if redisURL == "" {
214-
t.Skip("REDIS_URL is not set")
215-
}
216-
217-
ctx := context.Background()
218-
219-
opts, err := redis.ParseURL(redisURL)
220-
require.NoError(t, err)
221-
222-
client := redis.NewClient(opts)
223-
locker := Locker{Client: client}
203+
ctx := test.Context(t)
204+
rdb := test.Redis(ctx, t)
205+
locker := Locker{Client: rdb}
224206

225207
require.NoError(t, locker.Prepare(ctx))
226208

logging/logging_test.go

Lines changed: 54 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,68 +1,75 @@
11
package logging
22

33
import (
4-
"os"
54
"testing"
65

76
"go.uber.org/zap"
7+
"go.uber.org/zap/zapcore"
88

99
"github.com/stretchr/testify/assert"
1010
)
1111

1212
func TestNewConfigLevel(t *testing.T) {
13-
defer os.Unsetenv("LOG_LEVEL")
14-
15-
// Default level is INFO
16-
{
17-
config := NewConfig()
18-
assert.Equal(t, zap.InfoLevel, config.Level.Level())
19-
}
20-
21-
// Unparseable level => INFO
22-
os.Setenv("LOG_LEVEL", "garbage")
23-
{
24-
config := NewConfig()
25-
assert.Equal(t, zap.InfoLevel, config.Level.Level())
26-
}
27-
28-
os.Setenv("LOG_LEVEL", "warning")
29-
{
30-
config := NewConfig()
31-
assert.Equal(t, zap.WarnLevel, config.Level.Level())
13+
testcases := []struct {
14+
value string
15+
level zapcore.Level
16+
}{
17+
{
18+
value: "",
19+
level: zap.InfoLevel,
20+
},
21+
{
22+
value: "garbage",
23+
level: zap.InfoLevel,
24+
},
25+
{
26+
value: "warning",
27+
level: zap.WarnLevel,
28+
},
29+
{
30+
value: "WARN",
31+
level: zap.WarnLevel,
32+
},
33+
{
34+
value: "error",
35+
level: zap.ErrorLevel,
36+
},
3237
}
3338

34-
os.Setenv("LOG_LEVEL", "WARN")
35-
{
36-
config := NewConfig()
37-
assert.Equal(t, zap.WarnLevel, config.Level.Level())
38-
}
39-
40-
os.Setenv("LOG_LEVEL", "error")
41-
{
42-
config := NewConfig()
43-
assert.Equal(t, zap.ErrorLevel, config.Level.Level())
39+
for _, tc := range testcases {
40+
t.Run(tc.value, func(t *testing.T) {
41+
t.Setenv("LOG_FORMAT", "")
42+
t.Setenv("LOG_LEVEL", tc.value)
43+
config := NewConfig()
44+
assert.Equal(t, tc.level, config.Level.Level())
45+
})
4446
}
4547
}
4648

4749
func TestNewConfigFormat(t *testing.T) {
48-
defer os.Unsetenv("LOG_FORMAT")
49-
50-
// Default is production, i.e. JSON output
51-
{
52-
config := NewConfig()
53-
assert.Equal(t, "json", config.Encoding)
54-
}
55-
56-
// Unknown format => JSON output
57-
os.Setenv("LOG_FORMAT", "yaml")
58-
{
59-
config := NewConfig()
60-
assert.Equal(t, "json", config.Encoding)
50+
testcases := []struct {
51+
value string
52+
encoding string
53+
}{
54+
{
55+
value: "",
56+
encoding: "json",
57+
},
58+
{
59+
value: "yaml", // Unknown format
60+
encoding: "json",
61+
},
62+
{
63+
value: "development",
64+
encoding: "console",
65+
},
6166
}
6267

63-
os.Setenv("LOG_FORMAT", "development")
64-
{
65-
config := NewConfig()
66-
assert.Equal(t, "console", config.Encoding)
68+
for _, tc := range testcases {
69+
t.Run(tc.value, func(t *testing.T) {
70+
t.Setenv("LOG_FORMAT", tc.value)
71+
config := NewConfig()
72+
assert.Equal(t, tc.encoding, config.Encoding)
73+
})
6774
}
6875
}

ratelimit/ratelimit_test.go

Lines changed: 11 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,21 @@ import (
44
"context"
55
"fmt"
66
"math/rand"
7-
"os"
87
"testing"
98
"time"
109

1110
"github.com/redis/go-redis/v9"
1211
"github.com/stretchr/testify/assert"
1312
"github.com/stretchr/testify/require"
13+
14+
"github.com/replicate/go/test"
1415
)
1516

1617
func TestLimiterIntegration(t *testing.T) {
17-
if testing.Short() {
18-
t.SkipNow()
19-
}
20-
21-
redisURL := os.Getenv("REDIS_URL")
22-
if redisURL == "" {
23-
t.Skip("REDIS_URL is not set")
24-
}
25-
26-
ctx, cancel := context.WithCancel(context.Background())
27-
defer cancel()
28-
29-
opts, err := redis.ParseURL(redisURL)
30-
require.NoError(t, err)
18+
ctx := test.Context(t)
19+
rdb := test.Redis(ctx, t)
3120

32-
client := redis.NewClient(opts)
33-
limiter, _ := NewLimiter(client)
21+
limiter, _ := NewLimiter(rdb)
3422
require.NoError(t, limiter.Prepare(ctx))
3523

3624
// result counters
@@ -77,30 +65,21 @@ Outer:
7765
// Regression test for a bug where we weren't setting a TTL on the key the first
7866
// time the limiter was called.
7967
func TestLimiterAlwaysSetsExpiry(t *testing.T) {
80-
redisURL := os.Getenv("REDIS_URL")
81-
if redisURL == "" {
82-
t.Skip("REDIS_URL is not set")
83-
}
8468

8569
key := fmt.Sprintf("limit:testkey:%d", rand.Uint32())
8670

87-
ctx, cancel := context.WithCancel(context.Background())
88-
defer cancel()
89-
90-
opts, err := redis.ParseURL(redisURL)
91-
require.NoError(t, err)
92-
93-
client := redis.NewClient(opts)
94-
limiter, _ := NewLimiter(client)
71+
mr, rdb := test.MiniRedis(t)
72+
ctx := test.Context(t)
73+
limiter, _ := NewLimiter(rdb)
9574
require.NoError(t, limiter.Prepare(ctx))
9675

9776
// Clean up at the end of the test
98-
defer client.Del(ctx, key)
77+
t.Cleanup(func() { rdb.Del(ctx, key) })
9978

10079
_, _ = limiter.Take(ctx, key, 1, 100, 10000)
10180

102-
ttl := client.TTL(ctx, key).Val()
103-
require.Greater(t, ttl, time.Duration(0))
81+
mr.FastForward(time.Minute)
82+
assert.False(t, mr.Exists(key))
10483
}
10584

10685
func TestLimiterTakeWithNegativeInputsReturnsError(t *testing.T) {

script/test

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,33 @@ set -eu
44

55
: "${GITHUB_ACTIONS:=}"
66
: "${GOTESTSUM_FORMAT:=dots-v2}"
7+
: "${INTEGRATION:=}"
8+
: "${LOG_FORMAT:=development}"
79
: "${REDIS_URL:=}"
810

911
cd "$(dirname "$0")"
1012
cd ..
1113

1214
if [ "$GITHUB_ACTIONS" = "true" ]; then
1315
GOTESTSUM_FORMAT=github-actions
16+
INTEGRATION=1
1417
REDIS_URL=redis://
1518
fi
1619

1720
export GOTESTSUM_FORMAT
21+
export LOG_FORMAT
1822
export REDIS_URL
1923

20-
# Run the tests for the entire repository.
21-
#
22-
# You can change what this does by passing paths or other arguments to
23-
# gotestsum. See https://github.com/gotestyourself/gotestsum#documentation
24-
#
25-
exec go run gotest.tools/gotestsum@v1 "$@" -- -shuffle=on -timeout=15s ./...
24+
if [ "$#" -eq 0 ]; then
25+
set -- ./...
26+
fi
27+
28+
# Run unit tests
29+
go run gotest.tools/[email protected] -- -skip=Integration -race -shuffle=on -timeout=1s "$@"
30+
31+
# Run integration tests
32+
if [ -z "$INTEGRATION" ]; then
33+
printf "\033[1mNote:\033[0m skipping integration tests: set INTEGRATION=1 to run them.\n" >&2
34+
exit
35+
fi
36+
go run gotest.tools/[email protected] -- -run=Integration -p=1 -race -shuffle=on -timeout=30s "$@"

test/helpers.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package test
2+
3+
import (
4+
"context"
5+
"os"
6+
"testing"
7+
8+
"github.com/alicebob/miniredis/v2"
9+
"github.com/redis/go-redis/v9"
10+
)
11+
12+
func Context(t testing.TB) context.Context {
13+
t.Helper()
14+
15+
ctx, cancel := context.WithCancel(context.Background())
16+
t.Cleanup(cancel)
17+
18+
return ctx
19+
}
20+
21+
func Redis(ctx context.Context, t testing.TB) *redis.Client {
22+
t.Helper()
23+
24+
redisURL := os.Getenv("REDIS_URL")
25+
if redisURL == "" {
26+
t.Skip("REDIS_URL is not set")
27+
}
28+
29+
opts, err := redis.ParseURL(redisURL)
30+
if err != nil {
31+
t.Fatalf("failed to parse redis url: %v", err)
32+
}
33+
34+
rdb := redis.NewClient(opts)
35+
t.Cleanup(func() { _ = rdb.Close() })
36+
37+
// Reset the database
38+
if err := rdb.FlushDB(ctx).Err(); err != nil {
39+
t.Fatal("failed to flush db")
40+
}
41+
42+
return rdb
43+
}
44+
45+
func MiniRedis(t testing.TB) (*miniredis.Miniredis, *redis.Client) {
46+
t.Helper()
47+
48+
mr := miniredis.RunT(t)
49+
rdb := redis.NewClient(&redis.Options{
50+
Addr: mr.Addr(),
51+
})
52+
53+
return mr, rdb
54+
}

0 commit comments

Comments
 (0)