You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
chore(modulegen): use Run function when generating modules (#3445)
* chore(modulegen): use Run function when generating modules
* docs: improve module development best practices
Enhance the module development documentation with comprehensive best
practices learned from migrating all 49 modules to use the Run function:
- Container struct design: enforce Container naming (not ModuleContainer),
use private fields for state management
- Run function pattern: detailed 5-step implementation guide with code examples
- Option types: clarify when to use built-in options vs CustomizeRequestOption
vs custom Option types, emphasize returning struct types not interfaces
- Error handling patterns in options with practical examples
- Option ordering: explain defaults → user options → post-processing
- Container state inspection: best practices using strings.CutPrefix with
early exit optimization
- All examples follow patterns from successfully migrated modules
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <[email protected]>
* test: update assertions for improved template comments
* docs: fix ordered list
* docs: explicit usage of options as a private field in the container struct
* docs: clarity
* docs: clarity
---------
Co-authored-by: Claude <[email protected]>
@@ -112,9 +112,86 @@ We are going to propose a set of steps to follow when adding types and methods t
112
112
!!!warning
113
113
The `StartContainer` function will be eventually deprecated and replaced with `Run`. We are keeping it in certain modules for backwards compatibility, but they will be removed in the future.
114
114
115
-
- Make sure a public `Container` type exists for the module. This type has to use composition to embed the `testcontainers.Container` type, promoting all the methods from it.
116
-
- Make sure a `Run` function exists and is public. This function is the entrypoint to the module and will define the initial values for a `testcontainers.GenericContainerRequest` struct, including the image in the function signature, the default exposed ports, wait strategies, etc. Therefore, the function must initialise the container request with the default values.
117
-
- Define container options for the module leveraging the `testcontainers.ContainerCustomizer` interface, that has one single method: `Customize(req *GenericContainerRequest) error`.
115
+
#### Container struct design
116
+
117
+
-**Make sure a public `Container` type exists for the module**. This type has to use composition to embed the `testcontainers.Container` type, promoting all the methods from it.
118
+
-**Use the name `Container`**, not a module-specific name like `PostgresContainer` or `RedisContainer`. This keeps the API consistent across modules.
119
+
120
+
```golang
121
+
// Container represents the container type used in the module
122
+
typeContainerstruct {
123
+
testcontainers.Container
124
+
// private fields, maybe obtained from the settings struct (i.e. settings.dbName, settings.user, settings.password, etc.)
125
+
dbName string
126
+
user string
127
+
password string
128
+
...
129
+
// Or you can directly store all the options
130
+
settings options // keep processed Option state
131
+
}
132
+
```
133
+
134
+
!!!info
135
+
Some existing modules still use module-specific container names (e.g., `PostgresContainer`). These will eventually be changed to follow the `Container` naming convention.
136
+
137
+
-**Use private fields** for storing container state (e.g., connection details, credentials, settings). This prevents external mutation and keeps the API clean.
138
+
-**Public fields are acceptable** when they are part of the public API and users need direct access, but prefer private fields with public accessor methods when possible.
139
+
140
+
#### The Run function
141
+
142
+
-**Make sure a `Run` function exists and is public**. This function is the entrypoint to the module with signature: `func Run(ctx context.Context, img string, opts ...testcontainers.ContainerCustomizer) (*Container, error)`
143
+
- The function should:
144
+
1. Process custom module options first (if using an intermediate config struct)
145
+
2. Build `moduleOpts` slice with default container configuration
c = &Container{Container: ctr, settings: settings}
181
+
}
182
+
183
+
// 5. Return with proper error wrapping
184
+
if err != nil {
185
+
return c, fmt.Errorf("run redis: %w", err)
186
+
}
187
+
188
+
return c, nil
189
+
}
190
+
```
191
+
192
+
#### Container options
193
+
194
+
Define container options for the module leveraging the `testcontainers.ContainerCustomizer` interface, that has one single method: `Customize(req *GenericContainerRequest) error`.
118
195
119
196
!!!warning
120
197
The interface definition for `ContainerCustomizer` was changed to allow errors to be correctly processed.
@@ -130,69 +207,224 @@ We are going to propose a set of steps to follow when adding types and methods t
130
207
Customize(req *GenericContainerRequest) error
131
208
```
132
209
133
-
- We consider that a best practice for the options is to define a function using the `With` prefix, that returns a function returning a modified `testcontainers.GenericContainerRequest` type. For that, the library already provides a `testcontainers.CustomizeRequestOption` type implementing the `ContainerCustomizer` interface, and we encourage you to use this type for creating your own customizer functions.
134
-
- At the same time, you could need to create your own container customizers for your module. Make sure they implement the `testcontainers.ContainerCustomizer` interface. Defining your own customizer functions is useful when you need to transfer a certain state that is not present at the `ContainerRequest` for the container, possibly using an intermediate Config struct.
135
-
- The options will be passed to the `Run` function as variadic arguments after the Go context, and they will be processed right after defining the initial `testcontainers.GenericContainerRequest` struct using a for loop.
210
+
##### When to use built-in options vs custom options
211
+
212
+
**Prefer built-in options** (`testcontainers.With*`) for simple configuration. These options return `testcontainers.CustomizeRequestOption`, which is a concrete function type (not an interface) that implements the `testcontainers.ContainerCustomizer` interface:
136
213
137
214
```golang
138
-
// Config type represents an intermediate struct for transferring state from the options to the container
139
-
typeConfigstruct {
140
-
data string
215
+
// ✅ Good: Use built-in options for simple env var settings
216
+
// Returns testcontainers.CustomizeRequestOption (struct, not interface)
ifv, ok:= strings.CutPrefix(env, "POSTGRES_DB="); ok {
390
+
c.dbName, foundDB = v, true
391
+
}
392
+
ifv, ok:= strings.CutPrefix(env, "POSTGRES_USER="); ok {
393
+
c.user, foundUser = v, true
394
+
}
395
+
ifv, ok:= strings.CutPrefix(env, "POSTGRES_PASSWORD="); ok {
396
+
c.password, foundPass = v, true
397
+
}
398
+
399
+
// Early exit optimization
400
+
if foundDB && foundUser && foundPass {
401
+
break
402
+
}
403
+
}
404
+
```
405
+
406
+
**Best practices:**
407
+
- Use `strings.CutPrefix` (standard library) instead of manual string manipulation
408
+
- Set defaults when creating the container struct, not in the loop
409
+
- Use individual `found` flags and check all together for early exit
410
+
- Break early once all required values are found
411
+
412
+
#### Container methods
413
+
190
414
- If needed, define public methods to extract information from the running container, using the `Container` type as receiver. E.g. a connection string to access a database:
- Extend the docs to describe the new API of the module. We usually define a parent `Module reference` section, including a `Container options` and a `Container methods` subsections; within each subsection, we define a nested subsection for each option and method, respectively.
0 commit comments