Skip to content

Commit b42963a

Browse files
authored
Refactor test infrastructure to use centralized container management (#245)
1 parent 8f48139 commit b42963a

File tree

8 files changed

+493
-374
lines changed

8 files changed

+493
-374
lines changed

daemon/test_daemon.go

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import (
4343
"github.com/bsv-blockchain/teranode/stores/blob/options"
4444
"github.com/bsv-blockchain/teranode/stores/utxo"
4545
"github.com/bsv-blockchain/teranode/stores/utxo/fields"
46+
"github.com/bsv-blockchain/teranode/test/utils/containers"
4647
"github.com/bsv-blockchain/teranode/test/utils/transactions"
4748
"github.com/bsv-blockchain/teranode/test/utils/wait"
4849
"github.com/bsv-blockchain/teranode/ulogger"
@@ -77,6 +78,7 @@ type TestDaemon struct {
7778
UtxoStore utxo.Store
7879
P2PClient p2p.ClientI
7980
composeDependencies tc.ComposeStack
81+
containerManager *containers.ContainerManager
8082
ctxCancel context.CancelFunc
8183
d *Daemon
8284
privKey *bec.PrivateKey
@@ -95,6 +97,9 @@ type TestOptions struct {
9597
SkipRemoveDataDir bool
9698
StartDaemonDependencies bool
9799
FSMState blockchain.FSMStateType
100+
// UTXOStoreType specifies which UTXO store backend to use ("aerospike", "postgres")
101+
// If empty, defaults to "aerospike"
102+
UTXOStoreType string
98103
}
99104

100105
// JSONError represents a JSON error response from the RPC server.
@@ -314,6 +319,28 @@ func NewTestDaemon(t *testing.T, opts TestOptions) *TestDaemon {
314319
opts.SettingsOverrideFunc(appSettings)
315320
}
316321

322+
// Initialize container manager for UTXO store if UTXOStoreType is specified
323+
var containerManager *containers.ContainerManager
324+
if opts.UTXOStoreType != "" {
325+
containerManager, err = containers.NewContainerManager(containers.UTXOStoreType(opts.UTXOStoreType))
326+
require.NoError(t, err, "Failed to create container manager")
327+
328+
utxoStoreURL, err := containerManager.Initialize(ctx)
329+
require.NoError(t, err, "Failed to initialize container")
330+
331+
// Register cleanup immediately to prevent resource leak if daemon initialization fails
332+
t.Cleanup(func() {
333+
if containerManager != nil {
334+
_ = containerManager.Cleanup()
335+
}
336+
})
337+
338+
// Override the UTXO store URL in settings
339+
appSettings.UtxoStore.UtxoStore = utxoStoreURL
340+
341+
t.Logf("Initialized %s container with URL: %s", opts.UTXOStoreType, utxoStoreURL.String())
342+
}
343+
317344
readyCh := make(chan struct{})
318345

319346
var (
@@ -481,6 +508,7 @@ func NewTestDaemon(t *testing.T, opts TestOptions) *TestDaemon {
481508
UtxoStore: utxoStore,
482509
P2PClient: p2pClient,
483510
composeDependencies: composeDependencies,
511+
containerManager: containerManager,
484512
ctxCancel: cancel,
485513
d: d,
486514
privKey: pk,
@@ -506,6 +534,13 @@ func (td *TestDaemon) Stop(t *testing.T, skipTracerShutdown ...bool) {
506534
// Cleanup daemon stores to reset singletons
507535
td.d.daemonStores.Cleanup()
508536

537+
// Cleanup container manager if it exists
538+
if td.containerManager != nil {
539+
if err := td.containerManager.Cleanup(); err != nil {
540+
t.Logf("Warning: Failed to cleanup container manager: %v", err)
541+
}
542+
}
543+
509544
WaitForPortsFree(t, td.Ctx, td.Settings)
510545

511546
// cleanup remaining listeners that were never used

test/.claude-context/teranode-test-guide.md

Lines changed: 86 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -72,151 +72,152 @@ func TestMyScenario(t *testing.T) {
7272
### IMPORTANT: Database Backend Requirements
7373

7474
All tests MUST be tested with multiple database backends to ensure compatibility:
75-
- **SQLite** (in-memory, for fast tests)
75+
7676
- **PostgreSQL** (using testcontainers)
7777
- **Aerospike** (using testcontainers)
7878

79-
### Database Backend Test Pattern
79+
### Unified Container Management (RECOMMENDED)
80+
81+
**NEW: As of 2025, use the unified `UTXOStoreType` option in TestDaemon for automatic container management.**
82+
83+
This approach eliminates boilerplate container initialization code and ensures consistent setup across all tests.
84+
85+
#### Simple Pattern - Single Backend Test
8086

8187
```go
8288
package smoke
8389

8490
import (
85-
"os"
8691
"testing"
87-
8892
"github.com/bsv-blockchain/teranode/daemon"
89-
"github.com/bsv-blockchain/teranode/test/utils/aerospike"
90-
"github.com/bsv-blockchain/teranode/test/utils/postgres"
9193
"github.com/stretchr/testify/require"
9294
)
9395

94-
func init() {
95-
os.Setenv("SETTINGS_CONTEXT", "test")
96-
}
97-
98-
// Test with SQLite (in-memory)
99-
func TestMyFeatureSQLite(t *testing.T) {
100-
utxoStore := "sqlite:///test"
96+
func TestMyFeature(t *testing.T) {
97+
SharedTestLock.Lock()
98+
defer SharedTestLock.Unlock()
10199

102-
t.Run("scenario1", func(t *testing.T) {
103-
testScenario1(t, utxoStore)
104-
})
105-
t.Run("scenario2", func(t *testing.T) {
106-
testScenario2(t, utxoStore)
100+
// Container automatically initialized and cleaned up
101+
td := daemon.NewTestDaemon(t, daemon.TestOptions{
102+
EnableRPC: true,
103+
EnableValidator: true,
104+
SettingsContext: "dev.system.test",
105+
UTXOStoreType: "aerospike", // "aerospike", "postgres"
107106
})
108-
}
107+
defer td.Stop(t)
109108

110-
// Test with PostgreSQL
111-
func TestMyFeaturePostgres(t *testing.T) {
112-
// Setup PostgreSQL container
113-
utxoStore, teardown, err := postgres.SetupTestPostgresContainer()
109+
err := td.BlockchainClient.Run(td.Ctx, "test")
114110
require.NoError(t, err)
115111

116-
defer func() {
117-
_ = teardown()
118-
}()
112+
// Test implementation...
113+
}
114+
```
115+
116+
#### Multi-Backend Pattern - Testing All Backends
119117

118+
```go
119+
package smoke
120+
121+
import (
122+
"testing"
123+
"github.com/bsv-blockchain/teranode/daemon"
124+
"github.com/stretchr/testify/require"
125+
)
126+
127+
// Test with Aerospike (default, recommended for most tests)
128+
func TestMyFeatureAerospike(t *testing.T) {
120129
t.Run("scenario1", func(t *testing.T) {
121-
testScenario1(t, utxoStore)
130+
testScenario1(t, "aerospike")
122131
})
123132
t.Run("scenario2", func(t *testing.T) {
124-
testScenario2(t, utxoStore)
133+
testScenario2(t, "aerospike")
125134
})
126135
}
127136

128-
// Test with Aerospike
129-
func TestMyFeatureAerospike(t *testing.T) {
130-
// Setup Aerospike container
131-
utxoStore, teardown, err := aerospike.InitAerospikeContainer()
132-
require.NoError(t, err)
133-
134-
t.Cleanup(func() {
135-
_ = teardown()
136-
})
137-
137+
// Test with PostgreSQL
138+
func TestMyFeaturePostgres(t *testing.T) {
138139
t.Run("scenario1", func(t *testing.T) {
139-
testScenario1(t, utxoStore)
140+
testScenario1(t, "postgres")
140141
})
141142
t.Run("scenario2", func(t *testing.T) {
142-
testScenario2(t, utxoStore)
143+
testScenario2(t, "postgres")
143144
})
144145
}
145146

146147
// Shared test implementation
147-
func testScenario1(t *testing.T, utxoStore string) {
148+
func testScenario1(t *testing.T, storeType string) {
148149
SharedTestLock.Lock()
149150
defer SharedTestLock.Unlock()
150151

151152
td := daemon.NewTestDaemon(t, daemon.TestOptions{
152153
EnableRPC: true,
153154
EnableValidator: true,
154155
SettingsContext: "dev.system.test",
155-
SettingsOverrideFunc: func(s *settings.Settings) {
156-
url, err := url.Parse(utxoStore)
157-
require.NoError(t, err)
158-
s.UtxoStore.UtxoStore = url
159-
},
156+
UTXOStoreType: storeType, // Automatic container management
160157
})
161158
defer td.Stop(t)
162159

160+
err := td.BlockchainClient.Run(td.Ctx, "test")
161+
require.NoError(t, err)
162+
163163
// Test implementation...
164164
}
165165
```
166166

167-
### Database URL Formats
167+
#### Available Store Types
168168

169-
- **SQLite**: `"sqlite:///test"` (in-memory database)
170-
- **PostgreSQL**: Connection string returned by `SetupTestPostgresContainer()`
171-
- **Aerospike**: URL returned by `InitAerospikeContainer()` (e.g., `"aerospike://host:port/namespace?set=test&expiration=10m"`)
169+
- `"aerospike"` - Aerospike container (production-like, recommended)
170+
- `"postgres"` - PostgreSQL container (production-like)
171+
- `""` (empty) - No automatic container (uses default settings)
172172

173-
### Helper Functions for Database Setup
173+
#### Benefits of Unified Approach
174174

175-
#### PostgreSQL Container Setup
175+
**No boilerplate** - No manual container initialization
176+
**Automatic cleanup** - Containers cleaned up with `td.Stop(t)`
177+
**Consistent setup** - Same initialization across all tests
178+
**Type-safe** - Compile-time validation of store types
179+
**Less code** - ~10 lines reduced per test
176180

177-
```go
178-
import "github.com/bsv-blockchain/teranode/test/utils/postgres"
181+
### Legacy Pattern (Manual Container Setup)
179182

180-
utxoStore, teardown, err := postgres.SetupTestPostgresContainer()
181-
require.NoError(t, err)
182-
defer func() {
183-
_ = teardown()
184-
}()
185-
```
183+
**NOTE: This pattern is deprecated. Use `UTXOStoreType` option instead.**
184+
185+
If you encounter existing tests using manual container setup, they should be refactored to use the unified approach above.
186186

187-
#### Aerospike Container Setup
187+
<details>
188+
<summary>Click to see legacy pattern (for reference only)</summary>
188189

189190
```go
190-
import "github.com/bsv-blockchain/teranode/test/utils/aerospike"
191+
// OLD PATTERN - Do not use for new tests
192+
func TestMyFeatureAerospike(t *testing.T) {
193+
// Manual container setup (deprecated)
194+
utxoStore, teardown, err := aerospike.InitAerospikeContainer()
195+
require.NoError(t, err)
196+
t.Cleanup(func() {
197+
_ = teardown()
198+
})
191199

192-
utxoStore, teardown, err := aerospike.InitAerospikeContainer()
193-
require.NoError(t, err)
194-
t.Cleanup(func() {
195-
_ = teardown()
196-
})
200+
td := daemon.NewTestDaemon(t, daemon.TestOptions{
201+
SettingsContext: "dev.system.test",
202+
SettingsOverrideFunc: func(s *settings.Settings) {
203+
url, err := url.Parse(utxoStore)
204+
require.NoError(t, err)
205+
s.UtxoStore.UtxoStore = url
206+
},
207+
})
208+
defer td.Stop(t)
209+
// ...
210+
}
197211
```
198212

199-
### Configuring TestDaemon with Custom UTXO Store
200-
201-
```go
202-
td := daemon.NewTestDaemon(t, daemon.TestOptions{
203-
SettingsContext: "dev.system.test",
204-
SettingsOverrideFunc: func(s *settings.Settings) {
205-
// Parse the UTXO store URL
206-
url, err := url.Parse(utxoStore)
207-
require.NoError(t, err)
208-
// Override the UTXO store setting
209-
s.UtxoStore.UtxoStore = url
210-
},
211-
})
212-
```
213+
</details>
213214

214215
### Best Practices for Multi-Database Testing
215216

216-
1. **Always test all three backends** - SQLite for speed, PostgreSQL and Aerospike for production parity
217-
2. **Use shared test functions** - Write test logic once, parameterize with `utxoStore`
218-
3. **Handle cleanup properly** - Always defer teardown functions for containers
219-
4. **Set SETTINGS_CONTEXT** - Add `os.Setenv("SETTINGS_CONTEXT", "test")` in init()
217+
1. **Use UTXOStoreType option** - Always prefer the unified container management approach
218+
2. **Test with two store types** - Aerospike and PostgreSQL
219+
3. **Use shared test functions** - Write test logic once, parameterize with `storeType`
220+
4. **Aerospike is default** - Most tests should use Aerospike as it's closest to production
220221
5. **Use subtests** - Organize scenarios with `t.Run()` for better test output
221222
6. **Consider test isolation** - Each database backend should be independent
222223

0 commit comments

Comments
 (0)