Skip to content

Commit fa8f486

Browse files
aeneasrclaude
andauthored
refactor: replace Managed* wrappers with interfaces (#655)
* refactor: replace Managed* wrappers with interfaces Replace the concrete ManagedResource, ManagedPool, and ManagedNetwork wrapper structs with Go interfaces. The *T variants now return the restricted interface (Resource, Pool, Network) while the non-T variants return the closable interface (ClosableResource, ClosablePool, ClosableNetwork). Key changes: - Define Resource/ClosableResource, Network/ClosableNetwork, Pool/ClosablePool interfaces - Unexport Pool struct → pool, Network struct → dockerNetwork - Remove ~115 lines of manual delegation boilerplate - ConnectToNetwork/DisconnectFromNetwork/GetIPInNetwork now accept the Network interface directly, removing the ManagedNetwork.Network() escape hatch - Add NewResource constructor for external unit tests - Convert examples/cleanup/main.go to TestExplicitCleanup test Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * docs: fix three inaccuracies in UPGRADE.md - var pool *dockertest.Pool → var pool dockertest.ClosablePool (pointer-to-interface is meaningless; NewPool returns ClosablePool) - CloseT example: RunT returns Resource (no CloseT); use Run to get ClosableResource when explicit teardown is needed - Registry function signatures: add return types (error, bool, slice) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * chore: apply formatting Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c7ecf5c commit fa8f486

30 files changed

Lines changed: 1158 additions & 797 deletions

README.md

Lines changed: 27 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -239,14 +239,10 @@ resource := pool.RunT(t, "postgres",
239239

240240
### Container reuse
241241

242-
> [!WARNING]
243-
>
244-
> Do not use `resource.Cleanup(t)` on reused containers. Because reused
245-
> containers are shared across tests, cleaning up one reference will remove the
246-
> container for all other tests that depend on it. Only use `pool.Close(ctx)` in
247-
> `TestMain` to clean up reused containers after all tests have finished.
248-
249-
Containers are automatically reused based on `repository:tag`:
242+
Containers are automatically reused based on `repository:tag`. Reuse is
243+
reference-counted: each `Run`/`RunT` call increments the ref count, and each
244+
`Close`/cleanup decrements it. The container is only removed from Docker when
245+
the last reference is released.
250246

251247
```go
252248
// First test creates container
@@ -402,39 +398,38 @@ net, err := pool.CreateNetwork(ctx, "my-network", &dockertest.NetworkCreateOptio
402398

403399
### Cleanup
404400

405-
> [!WARNING]
406-
>
407-
> Do not use `resource.Cleanup(t)` or `resource.CloseT(t)` on **reused**
408-
> containers. Because reused containers are shared across tests, cleaning up one
409-
> reference removes the container for all other tests. Use `pool.Close(ctx)` in
410-
> `TestMain` to clean up reused containers after all tests finish.
411-
412-
Use `Cleanup(t)` for **non-reused** containers — it registers `t.Cleanup` and
413-
logs errors without failing the test:
401+
**`NewPoolT` + `RunT` (recommended):** Cleanup is fully automatic. `RunT`
402+
registers cleanup via `t.Cleanup`, and the pool is closed when the test
403+
finishes. Nothing to do.
414404

415405
```go
416-
resource := pool.RunT(t, "postgres",
417-
dockertest.WithTag("14"),
418-
dockertest.WithoutReuse(),
419-
)
420-
resource.Cleanup(t) // removed when t finishes
406+
func TestDB(t *testing.T) {
407+
pool := dockertest.NewPoolT(t, "")
408+
resource := pool.RunT(t, "postgres", dockertest.WithTag("14"))
409+
// Use resource... cleanup happens automatically when t finishes.
410+
}
421411
```
422412

423-
Use `CloseT(t)` when you need **immediate** cleanup and want a hard failure on
424-
error (calls `t.Fatalf`):
413+
**`NewPool` + `Run`:** Call `resource.Close(ctx)` to release individual
414+
containers, or `pool.Close(ctx)` to release everything:
425415

426416
```go
427-
resource.CloseT(t) // removes now, fails test on error
428-
```
429-
430-
Use `Close(ctx)` in non-test code for manual cleanup:
417+
ctx := context.Background()
418+
pool, err := dockertest.NewPool(ctx, "")
419+
if err != nil {
420+
panic(err)
421+
}
422+
defer pool.Close(ctx) // releases all tracked containers and networks
431423

432-
```go
433-
err := resource.Close(ctx)
424+
resource, err := pool.Run(ctx, "postgres", dockertest.WithTag("14"))
425+
if err != nil {
426+
panic(err)
427+
}
428+
defer resource.Close(ctx) // or let pool.Close handle it
434429
```
435430

436-
Use `pool.Close(ctx)` to clean up **all** resources (reused and non-reused),
437-
typically in `TestMain`:
431+
**Advanced: shared pool in `TestMain`:** Use this when you need a single pool
432+
shared across all tests in a package:
438433

439434
```go
440435
func TestMain(m *testing.M) {

UPGRADE.md

Lines changed: 17 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,11 @@ intended due to Docker API limitations.
5757
v4 offers two pool creation patterns for tests. **Choose A or B per package, do
5858
not mix them** — mixing causes double-close or resource leaks.
5959

60-
**Option A: `NewPoolT` (recommended for most tests)**
60+
**Option A: `NewPoolT` (recommended for most tests — no `TestMain` needed)**
6161

6262
`NewPoolT` registers cleanup automatically via `t.Cleanup`. All tracked
63-
containers and networks are removed when the test finishes. No `TestMain`
64-
needed.
63+
containers and networks are removed when the test finishes. Self-contained: no
64+
`TestMain` needed.
6565

6666
```go
6767
// v4 — cleanup is automatic
@@ -70,14 +70,14 @@ pool := dockertest.NewPoolT(t, "",
7070
)
7171
```
7272

73-
**Option B: `NewPool` + `TestMain` (for shared pools across tests)**
73+
**Option B: `NewPool` + `TestMain` (for shared pools — advanced)**
7474

7575
Use this when you want a single pool shared across all tests in a package. You
7676
must call `pool.Close(ctx)` explicitly.
7777

7878
```go
7979
// v4 — shared pool, manual cleanup
80-
var pool *dockertest.Pool
80+
var pool dockertest.ClosablePool
8181

8282
func TestMain(m *testing.M) {
8383
ctx := context.Background()
@@ -254,8 +254,8 @@ Available sentinel errors: `ErrImagePullFailed`, `ErrContainerCreateFailed`,
254254
- `pool.Purge(resource)` → automatic via `NewPoolT`, or `pool.Close(ctx)` in
255255
`TestMain`
256256
- `pool.Retry(fn)``pool.Retry(ctx, timeout, fn)`
257-
- For non-reused containers needing per-test cleanup: `WithoutReuse()` +
258-
`resource.Cleanup(t)`
257+
- For non-reused containers: use `WithoutReuse()` (cleanup is automatic with
258+
`RunT`, or call `resource.Close(ctx)` with `Run`)
259259
- See [Breaking Changes](#breaking-changes) for full patterns.
260260

261261
4. **Test:** Run `go test ./...` to verify the migration.
@@ -315,14 +315,6 @@ cache := pool.RunT(t, "redis", dockertest.WithTag("7"))
315315
> containers that share an image but differ in configuration (see
316316
> [Custom Reuse ID](#custom-reuse-id-different-configs) below).
317317
318-
> [!WARNING]
319-
>
320-
> Do not use `resource.Cleanup(t)` on reused containers. Because reused
321-
> containers are shared across tests, cleaning up one reference will remove the
322-
> container for all other tests that depend on it. Only use `pool.Close(ctx)`
323-
> (automatic with `NewPoolT`) to clean up reused containers after all tests have
324-
> finished.
325-
326318
v4 automatically reuses containers with the same `repo:tag` across tests. Each
327319
`NewPoolT` call creates a separate pool, but containers are still shared because
328320
default pools use a common reuse scope:
@@ -346,7 +338,7 @@ resource := pool.RunT(t, "postgres",
346338
dockertest.WithTag("14"),
347339
dockertest.WithoutReuse(),
348340
)
349-
resource.Cleanup(t) // Safe — this container is not shared
341+
// Cleanup is automatic via RunT
350342
```
351343

352344
### Custom Reuse ID (Different Configs)
@@ -437,9 +429,11 @@ v4 maintains a global in-memory registry for container reuse. You typically do
437429
not need these functions directly — `Pool.Run` and `Pool.RunT` use them
438430
automatically. They are useful for custom cleanup or inspection:
439431

440-
- `Register(reuseID, resource)` — stores a resource (idempotent; keeps existing)
441-
- `Get(reuseID)` — retrieves a resource by reuse ID
442-
- `GetAll()` — returns all registered resources
432+
- `Register(reuseID string, r ClosableResource) error` — stores a resource
433+
(idempotent; keeps existing)
434+
- `Get(reuseID string) (ClosableResource, bool)` — retrieves a resource by reuse
435+
ID
436+
- `GetAll() []ClosableResource` — returns all registered resources
443437
- `ResetRegistry()` — clears the registry (does **not** stop containers)
444438

445439
### Immediate Cleanup with `CloseT`
@@ -449,7 +443,10 @@ and calls `t.Fatalf` on error. Use this when you need teardown at a specific
449443
point rather than relying on pool-scoped cleanup:
450444

451445
```go
452-
resource := pool.RunT(t, "postgres", dockertest.WithTag("14"), dockertest.WithoutReuse())
446+
resource, err := pool.Run(t.Context(), "postgres", dockertest.WithTag("14"), dockertest.WithoutReuse())
447+
if err != nil {
448+
t.Fatal(err)
449+
}
453450
// ... use resource ...
454451
resource.CloseT(t) // immediate removal
455452
```

build.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ type BuildOptions struct {
7474
// panic(err)
7575
// }
7676
// defer resource.Close(ctx)
77-
func (p *Pool) BuildAndRun(ctx context.Context, name string, buildOpts *BuildOptions, runOpts ...RunOption) (*Resource, error) {
77+
func (p *pool) BuildAndRun(ctx context.Context, name string, buildOpts *BuildOptions, runOpts ...RunOption) (ClosableResource, error) {
7878
if buildOpts == nil {
7979
return nil, fmt.Errorf("buildOpts cannot be nil")
8080
}
@@ -160,14 +160,18 @@ func (p *Pool) BuildAndRun(ctx context.Context, name string, buildOpts *BuildOpt
160160
}
161161

162162
// BuildAndRunT is a test helper that uses t.Context() and calls t.Fatalf on error.
163-
func (p *Pool) BuildAndRunT(t TestingTB, name string, buildOpts *BuildOptions, runOpts ...RunOption) *Resource {
163+
// The returned ManagedResource does not expose Close, CloseT, or Cleanup;
164+
// the resource is automatically cleaned up when the test finishes.
165+
func (p *pool) BuildAndRunT(t TestingTB, name string, buildOpts *BuildOptions, runOpts ...RunOption) Resource {
164166
t.Helper()
165167

166168
r, err := p.BuildAndRun(t.Context(), name, buildOpts, runOpts...)
167169
if err != nil {
168170
t.Fatalf("BuildAndRunT failed: %v", err)
169171
}
170172

173+
r.Cleanup(t)
174+
171175
return r
172176
}
173177

build_test.go

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -54,8 +54,6 @@ CMD ["sleep", "300"]
5454
t.Fatal("Resource has empty container ID")
5555
}
5656

57-
// Cleanup
58-
r.CloseT(t)
5957
}
6058

6159
func TestBuildAndRunWithBuildArgs(t *testing.T) {
@@ -93,7 +91,6 @@ CMD ["sh", "-c", "echo TEST_ENV=$TEST_ENV && sleep 300"]
9391
}
9492

9593
r := pool.BuildAndRunT(t, "test-build-args", buildOpts)
96-
t.Cleanup(func() { r.Close(t.Context()) })
9794

9895
// Verify build arg was applied by checking container env via logs
9996
var logs string
@@ -146,7 +143,6 @@ CMD ["sh", "-c", "echo $TEST_VAR && sleep 300"]
146143
r := pool.BuildAndRunT(t, "test-build-env", buildOpts,
147144
dockertest.WithEnv([]string{"TEST_VAR=hello"}),
148145
)
149-
t.Cleanup(func() { r.Close(t.Context()) })
150146

151147
// Verify env var took effect via logs
152148
var logs string
@@ -213,8 +209,6 @@ func TestBuildAndRunWithBuildContext(t *testing.T) {
213209
t.Fatalf("Expected logs to contain 'Hello, World!', got: %s (error: %v)", logs, err)
214210
}
215211

216-
// Cleanup
217-
r.CloseT(t)
218212
}
219213

220214
func TestBuildAndRunWithTaggedName(t *testing.T) {
@@ -244,11 +238,9 @@ CMD ["sleep", "300"]
244238
ContextDir: tmpDir,
245239
})
246240

247-
if r.Container.Config.Image != imageRef {
248-
t.Fatalf("container image = %q, want %q", r.Container.Config.Image, imageRef)
241+
if r.Container().Config.Image != imageRef {
242+
t.Fatalf("container image = %q, want %q", r.Container().Config.Image, imageRef)
249243
}
250-
251-
r.CloseT(t)
252244
}
253245

254246
func TestRunUsesLocalImageWithoutPull(t *testing.T) {
@@ -275,15 +267,13 @@ CMD ["sleep", "300"]
275267
// If Run attempts a pull, this registry will fail DNS resolution.
276268
repository := "example.invalid/dockertest-local-skip-pull"
277269

278-
first := pool.BuildAndRunT(t, repository, &dockertest.BuildOptions{
270+
pool.BuildAndRunT(t, repository, &dockertest.BuildOptions{
279271
Dockerfile: "Dockerfile",
280272
ContextDir: tmpDir,
281273
}, dockertest.WithoutReuse())
282-
first.CloseT(t)
283274

284-
second := pool.RunT(t, repository,
275+
pool.RunT(t, repository,
285276
dockertest.WithTag("latest"),
286277
dockertest.WithoutReuse(),
287278
)
288-
second.CloseT(t)
289279
}

0 commit comments

Comments
 (0)