Skip to content

Commit 4e07c6b

Browse files
committed
Refactor tests to use shared MongoDB and CouchDB containers, simplify container setup logic, and consolidate state management methods.
1 parent d408fb8 commit 4e07c6b

File tree

6 files changed

+502
-185
lines changed

6 files changed

+502
-185
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Build and Test
22

33
env:
44
GO_LATEST: '1.24.8'
5-
DOCKER_COMPOSE_VERSION: 'v2.39.2'
5+
DOCKER_COMPOSE_VERSION: 'v2.39.4'
66

77
on:
88
push:

app_test.go

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,6 @@ import (
1010
"golang-simple-notes/model"
1111
"golang-simple-notes/storage"
1212

13-
"github.com/testcontainers/testcontainers-go"
14-
"github.com/testcontainers/testcontainers-go/modules/mongodb"
1513
"go.mongodb.org/mongo-driver/mongo"
1614
"go.mongodb.org/mongo-driver/mongo/options"
1715
)
@@ -47,18 +45,16 @@ func TestApp_Initialize(t *testing.T) {
4745
func TestApp_InitializeWithCouchDB(t *testing.T) {
4846
ctx := context.Background()
4947

50-
// Start CouchDB container
51-
couchContainer, couchURL, err := startCouchDBContainer(ctx)
52-
if err != nil {
53-
t.Fatalf("Failed to start CouchDB container: %v", err)
48+
// Use the shared CouchDB container
49+
couchURL := sharedCouchURL
50+
if couchURL == "" {
51+
t.Skip("Shared CouchDB container not available")
5452
}
53+
5554
defer func() {
5655
// Clean up: delete the notes database
5756
req, _ := http.NewRequest(http.MethodDelete, couchURL+"/notes", nil)
5857
http.DefaultClient.Do(req)
59-
if err := couchContainer.Terminate(ctx); err != nil {
60-
t.Fatalf("Failed to terminate CouchDB container: %v", err)
61-
}
6258
}()
6359

6460
// Create app with CouchDB config
@@ -71,7 +67,7 @@ func TestApp_InitializeWithCouchDB(t *testing.T) {
7167
}
7268

7369
app := NewApp(config)
74-
err = app.Initialize(ctx)
70+
err := app.Initialize(ctx)
7571
if err != nil {
7672
t.Fatalf("Failed to initialize app: %v", err)
7773
}
@@ -118,30 +114,21 @@ func TestApp_InitializeWithCouchDB(t *testing.T) {
118114
func TestApp_InitializeWithMongoDB(t *testing.T) {
119115
ctx := context.Background()
120116

121-
// Start MongoDB container
122-
mongoContainer, err := mongodb.RunContainer(ctx, testcontainers.WithImage("mongo:7.0.23-jammy"))
123-
if err != nil {
124-
t.Fatalf("Failed to start MongoDB container: %v", err)
117+
// Use the shared MongoDB container
118+
mongoURI := sharedMongoURI
119+
if mongoURI == "" {
120+
t.Skip("Shared MongoDB container not available")
125121
}
122+
126123
defer func() {
127124
// Clean up: drop the notes collection
128-
mongoURI, _ := mongoContainer.ConnectionString(ctx)
129125
client, err := mongo.Connect(ctx, options.Client().ApplyURI(mongoURI))
130126
if err == nil {
131127
_ = client.Database("notes").Collection("notes").Drop(ctx)
132128
_ = client.Disconnect(ctx)
133129
}
134-
if err := mongoContainer.Terminate(ctx); err != nil {
135-
t.Fatalf("Failed to terminate MongoDB container: %v", err)
136-
}
137130
}()
138131

139-
// Get MongoDB connection details
140-
mongoURI, err := mongoContainer.ConnectionString(ctx)
141-
if err != nil {
142-
t.Fatalf("Failed to get MongoDB connection string: %v", err)
143-
}
144-
145132
// Create app with MongoDB config
146133
config := &Config{
147134
StorageType: "mongodb",
@@ -153,7 +140,7 @@ func TestApp_InitializeWithMongoDB(t *testing.T) {
153140
}
154141

155142
app := NewApp(config)
156-
err = app.Initialize(ctx)
143+
err := app.Initialize(ctx)
157144
if err != nil {
158145
t.Fatalf("Failed to initialize app: %v", err)
159146
}

main_test.go

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,235 @@ package main
22

33
import (
44
"context"
5+
"encoding/json"
56
"fmt"
7+
"log"
8+
"os"
9+
"path/filepath"
10+
"testing"
11+
"time"
612

713
"golang-simple-notes/model"
814
"golang-simple-notes/storage"
915

1016
"github.com/testcontainers/testcontainers-go"
17+
"github.com/testcontainers/testcontainers-go/modules/mongodb"
1118
"github.com/testcontainers/testcontainers-go/wait"
1219
)
1320

21+
var (
22+
// Shared container instances for all tests in the main package
23+
sharedMongoContainer testcontainers.Container
24+
sharedMongoURI string
25+
sharedCouchContainer testcontainers.Container
26+
sharedCouchURL string
27+
shouldCleanupContainers bool
28+
)
29+
30+
// containerState holds the connection details for shared containers
31+
type containerState struct {
32+
MongoURI string `json:"mongo_uri"`
33+
CouchURL string `json:"couch_url"`
34+
OwnerPID int `json:"owner_pid"`
35+
}
36+
37+
// getStateFilePath returns the path to the shared state file
38+
func getStateFilePath() string {
39+
return filepath.Join(os.TempDir(), "testcontainers-shared-state.json")
40+
}
41+
42+
// getLockFilePath returns the path to the lock file
43+
func getLockFilePath() string {
44+
return filepath.Join(os.TempDir(), "testcontainers-shared-state.lock")
45+
}
46+
47+
// acquireLock attempts to acquire a file-based lock
48+
func acquireLock() (*os.File, error) {
49+
lockFile := getLockFilePath()
50+
// Try to create lock file exclusively
51+
for i := 0; i < 50; i++ {
52+
f, err := os.OpenFile(lockFile, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
53+
if err == nil {
54+
// Write our PID to the lock file
55+
fmt.Fprintf(f, "%d", os.Getpid())
56+
return f, nil
57+
}
58+
// Lock file exists, wait and retry
59+
time.Sleep(100 * time.Millisecond)
60+
}
61+
return nil, fmt.Errorf("failed to acquire lock after 5 seconds")
62+
}
63+
64+
// releaseLock releases the file-based lock
65+
func releaseLock(f *os.File) {
66+
if f != nil {
67+
f.Close()
68+
os.Remove(getLockFilePath())
69+
}
70+
}
71+
72+
// loadContainerState loads the container state from file
73+
func loadContainerState() (*containerState, error) {
74+
stateFile := getStateFilePath()
75+
data, err := os.ReadFile(stateFile)
76+
if err != nil {
77+
return nil, err
78+
}
79+
80+
var state containerState
81+
if err := json.Unmarshal(data, &state); err != nil {
82+
return nil, err
83+
}
84+
85+
return &state, nil
86+
}
87+
88+
// saveContainerState saves the container state to file
89+
func saveContainerState(state *containerState) error {
90+
stateFile := getStateFilePath()
91+
data, err := json.Marshal(state)
92+
if err != nil {
93+
return err
94+
}
95+
96+
return os.WriteFile(stateFile, data, 0644)
97+
}
98+
99+
// TestMain sets up shared test containers for the main package
100+
func TestMain(m *testing.M) {
101+
ctx := context.Background()
102+
103+
// Acquire lock to prevent race condition with other packages
104+
lock, err := acquireLock()
105+
if err != nil {
106+
log.Printf("Warning: Failed to acquire lock: %v. Tests may fail.", err)
107+
os.Exit(1)
108+
}
109+
110+
// Try to load existing container state
111+
state, err := loadContainerState()
112+
if err == nil && state != nil {
113+
// Reuse existing containers from another package
114+
log.Printf("Reusing existing MongoDB container at %s", state.MongoURI)
115+
log.Printf("Reusing existing CouchDB container at %s", state.CouchURL)
116+
sharedMongoURI = state.MongoURI
117+
sharedCouchURL = state.CouchURL
118+
shouldCleanupContainers = false
119+
releaseLock(lock)
120+
} else {
121+
// Start new containers and save state
122+
shouldCleanupContainers = true
123+
124+
// Start shared MongoDB container
125+
if err := startSharedMongoDBContainer(ctx); err != nil {
126+
log.Printf("Warning: Failed to start shared MongoDB container: %v. MongoDB tests may fail.", err)
127+
}
128+
129+
// Start shared CouchDB container
130+
if err := startSharedCouchDBContainer(ctx); err != nil {
131+
log.Printf("Warning: Failed to start shared CouchDB container: %v. CouchDB tests may fail.", err)
132+
}
133+
134+
// Save container state for other packages to reuse
135+
if sharedMongoURI != "" && sharedCouchURL != "" {
136+
state := &containerState{
137+
MongoURI: sharedMongoURI,
138+
CouchURL: sharedCouchURL,
139+
OwnerPID: os.Getpid(),
140+
}
141+
if err := saveContainerState(state); err != nil {
142+
log.Printf("Warning: Failed to save container state: %v", err)
143+
}
144+
}
145+
146+
releaseLock(lock)
147+
}
148+
149+
// Run tests
150+
code := m.Run()
151+
152+
// Cleanup containers only if we started them
153+
if shouldCleanupContainers {
154+
cleanupCtx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
155+
defer cancel()
156+
157+
if sharedMongoContainer != nil {
158+
if err := sharedMongoContainer.Terminate(cleanupCtx); err != nil {
159+
log.Printf("Failed to terminate MongoDB container: %v", err)
160+
}
161+
}
162+
163+
if sharedCouchContainer != nil {
164+
if err := sharedCouchContainer.Terminate(cleanupCtx); err != nil {
165+
log.Printf("Failed to terminate CouchDB container: %v", err)
166+
}
167+
}
168+
169+
// Clean up state file
170+
os.Remove(getStateFilePath())
171+
}
172+
173+
os.Exit(code)
174+
}
175+
176+
func startSharedMongoDBContainer(ctx context.Context) error {
177+
container, err := mongodb.RunContainer(ctx, testcontainers.WithImage("mongo:7.0.23-jammy"))
178+
if err != nil {
179+
return fmt.Errorf("failed to start MongoDB container: %w", err)
180+
}
181+
182+
sharedMongoContainer = container
183+
184+
// Get connection string
185+
mongoURI, err := container.ConnectionString(ctx)
186+
if err != nil {
187+
return fmt.Errorf("failed to get MongoDB connection string: %w", err)
188+
}
189+
190+
sharedMongoURI = mongoURI
191+
192+
log.Printf("Started shared MongoDB container at %s", sharedMongoURI)
193+
return nil
194+
}
195+
196+
func startSharedCouchDBContainer(ctx context.Context) error {
197+
req := testcontainers.ContainerRequest{
198+
Image: "couchdb:3.4.3",
199+
ExposedPorts: []string{"5984/tcp"},
200+
WaitingFor: wait.ForListeningPort("5984/tcp"),
201+
Env: map[string]string{
202+
"COUCHDB_USER": "admin",
203+
"COUCHDB_PASSWORD": "password",
204+
},
205+
}
206+
207+
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
208+
ContainerRequest: req,
209+
Started: true,
210+
})
211+
if err != nil {
212+
return fmt.Errorf("failed to start CouchDB container: %w", err)
213+
}
214+
215+
sharedCouchContainer = container
216+
217+
// Get connection details
218+
host, err := container.Host(ctx)
219+
if err != nil {
220+
return fmt.Errorf("failed to get CouchDB container host: %w", err)
221+
}
222+
223+
port, err := container.MappedPort(ctx, "5984")
224+
if err != nil {
225+
return fmt.Errorf("failed to get CouchDB container port: %w", err)
226+
}
227+
228+
sharedCouchURL = fmt.Sprintf("http://admin:password@%s:%s", host, port.Port())
229+
230+
log.Printf("Started shared CouchDB container at %s", sharedCouchURL)
231+
return nil
232+
}
233+
14234
// MockStorage is a simple implementation of NoteStorage for testing
15235
type MockStorage struct {
16236
notes []*model.Note

0 commit comments

Comments
 (0)