-
Notifications
You must be signed in to change notification settings - Fork 12
Testing: Testcontainers for Go #1782
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+774
−0
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| name: Testcontainers for Go | ||
|
|
||
| on: | ||
| pull_request: | ||
| branches: ~ | ||
| paths: | ||
| - '.github/workflows/testing-testcontainers-go.yml' | ||
| - 'testing/testcontainers/go**' | ||
| push: | ||
| branches: [ main ] | ||
| paths: | ||
| - '.github/workflows/testing-testcontainers-go.yml' | ||
| - 'testing/testcontainers/go**' | ||
|
|
||
| # Allow job to be triggered manually. | ||
| workflow_dispatch: | ||
|
|
||
| # Run job each night after CrateDB nightly has been published. | ||
| schedule: | ||
| - cron: '0 3 * * *' | ||
|
|
||
| # Cancel in-progress jobs when pushing to the same branch. | ||
| concurrency: | ||
| cancel-in-progress: true | ||
| group: ${{ github.workflow }}-${{ github.ref }} | ||
|
|
||
| jobs: | ||
| test: | ||
| name: " | ||
| Go: ${{ matrix.go-version }} | ||
| CrateDB: ${{ matrix.cratedb-version }} | ||
| on ${{ matrix.os }}" | ||
| runs-on: ${{ matrix.os }} | ||
| strategy: | ||
| fail-fast: false | ||
| matrix: | ||
| os: [ 'ubuntu-latest' ] | ||
| cratedb-version: [ 'nightly' ] | ||
| go-version: | ||
| - '1.26.x' | ||
|
|
||
| # Note: No `services: cratedb` block is needed here. Unlike the | ||
| # `by-language/go-pgx` example, Testcontainers starts CrateDB itself, using | ||
| # the Docker engine available on the runner. | ||
| env: | ||
| CRATEDB_VERSION: ${{ matrix.cratedb-version }} | ||
|
|
||
| steps: | ||
|
|
||
| - name: Acquire sources | ||
| uses: actions/checkout@v6 | ||
|
|
||
| - name: Install Go | ||
| uses: actions/setup-go@v6 | ||
| with: | ||
| go-version: ${{ matrix.go-version }} | ||
|
|
||
| - name: Validate testing/testcontainers/go-pgx | ||
| working-directory: testing/testcontainers/go-pgx | ||
| run: go test -v ./... |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,85 @@ | ||
| # Testcontainers for Go | ||
|
|
||
| *How to run integration tests of Go applications with CrateDB.* | ||
|
|
||
| ## About | ||
|
|
||
| [Testcontainers for Go] provides lightweight, throwaway instances of databases | ||
| (and anything else that runs in a container) for integration testing. These | ||
| examples spin up a single-node [CrateDB] and talk to it over the PostgreSQL | ||
| wire protocol using [pgx], the canonical PostgreSQL driver for Go. | ||
|
|
||
| Testcontainers for Go has **no dedicated CrateDB module**, so the helper in | ||
| [`cratedb.go`](cratedb.go) drives the core `GenericContainer` with the | ||
| [CrateDB OCI image], a single-node command line, and an HTTP wait strategy on | ||
| port 4200. | ||
|
|
||
| ## What's inside | ||
|
|
||
| - [`cratedb.go`](cratedb.go) — `RunCrateDB(ctx)` plus `ConnectionString` / | ||
| `HTTPEndpoint` helpers, and `CRATEDB_VERSION`-driven image selection. | ||
| - [`shared_test.go`](shared_test.go) — **shared container**: one CrateDB started | ||
| once per package via `TestMain` and reused by all tests. | ||
| - [`function_scope_test.go`](function_scope_test.go) — **per-test container**: | ||
| each test gets its own throwaway CrateDB, torn down via `t.Cleanup`. | ||
| - [`types_test.go`](types_test.go) — **advanced CrateDB types**: `ARRAY`, | ||
| `OBJECT(DYNAMIC)`, `GEO_POINT`, and `TIMESTAMP WITH TIME ZONE`. | ||
| - [`http_test.go`](http_test.go) — **HTTP endpoint**: query CrateDB's native | ||
| `/_sql` REST interface on port 4200, complementing the pgx wire-protocol | ||
| tests. | ||
|
|
||
| ## Container scope | ||
|
|
||
| Starting a container is expensive (a few seconds each), so the examples show two | ||
| strategies for managing CrateDB's lifecycle in a test suite: | ||
|
|
||
| - **Shared, package-scoped** ([`shared_test.go`](shared_test.go)) — one CrateDB | ||
| is started once for the whole package in `TestMain` and reused, through a | ||
| single `sharedConn`, by every test that doesn't need isolation. This is the | ||
| default: it keeps the suite fast. The trade-off is that tests share state, so | ||
| they must not depend on a clean database or leak data that confuses one | ||
| another. | ||
| - **Per-test** ([`function_scope_test.go`](function_scope_test.go)) — each test | ||
| starts its own throwaway CrateDB and tears it down via `t.Cleanup`. Reach for | ||
| this only when a test genuinely needs a pristine instance — for example when it | ||
| changes cluster-wide settings or wants to pin a different CrateDB version — and | ||
| accept the extra startup cost per test. | ||
|
|
||
| As a rule of thumb: prefer the shared container, and isolate a single test with | ||
| its own container only when sharing would make it flaky. | ||
|
|
||
| ## Usage | ||
|
|
||
| 1. Make sure Go (1.25+) and a Docker engine are available — Testcontainers starts | ||
| CrateDB itself, so no CrateDB needs to be running beforehand. | ||
|
|
||
| 2. Run the tests: | ||
|
|
||
| ```shell | ||
| # Run all tests. | ||
| go test -v ./... | ||
|
|
||
| # Run an individual test. | ||
| go test -v -run TestAdvancedTypes ./... | ||
|
|
||
| # Select the CrateDB version (image tag) to test against. | ||
| # (unset) / nightly -> crate/crate:nightly | ||
| # 6.2 / latest / ... -> crate:<tag> | ||
| export CRATEDB_VERSION=6.2 | ||
| go test -v ./... | ||
|
|
||
| # Keep containers around after the run for debugging. | ||
| export TESTCONTAINERS_RYUK_DISABLED=true | ||
| ``` | ||
|
|
||
| 3. From the repository root, the example also runs through the shared test | ||
| runner: | ||
|
|
||
| ```shell | ||
| ngr test testing/testcontainers/go-pgx | ||
| ``` | ||
|
|
||
| [CrateDB]: https://github.com/crate/crate | ||
| [CrateDB OCI image]: https://hub.docker.com/_/crate | ||
| [pgx]: https://github.com/jackc/pgx | ||
| [Testcontainers for Go]: https://golang.testcontainers.org/ | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,105 @@ | ||
| // Package cratedb provides a small helper for running a single-node CrateDB | ||
| // instance with Testcontainers for Go, for use in integration tests. | ||
| // | ||
| // Testcontainers for Go has no dedicated CrateDB module, so this wraps the | ||
| // core GenericContainer with the CrateDB OCI image, a single-node command | ||
| // line, and an HTTP wait strategy. | ||
| package cratedb | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "os" | ||
| "time" | ||
|
|
||
| "github.com/testcontainers/testcontainers-go" | ||
| "github.com/testcontainers/testcontainers-go/wait" | ||
| ) | ||
|
|
||
| const ( | ||
| // httpPort is CrateDB's HTTP endpoint (admin UI, REST/crash). | ||
| httpPort = "4200/tcp" | ||
| // pgPort is CrateDB's PostgreSQL wire-protocol endpoint. | ||
| pgPort = "5432/tcp" | ||
|
|
||
| defaultUser = "crate" | ||
| defaultDatabase = "doc" | ||
| ) | ||
|
|
||
| // imageFromEnv returns the CrateDB OCI image to run, honoring the | ||
| // CRATEDB_VERSION environment variable so a single test suite can be run | ||
| // against a matrix of CrateDB versions. | ||
| func imageFromEnv() string { | ||
| return imageFromLabel(os.Getenv("CRATEDB_VERSION")) | ||
| } | ||
|
|
||
| func imageFromLabel(label string) string { | ||
| switch label { | ||
| case "", "nightly": | ||
| return "crate/crate:nightly" | ||
| default: | ||
| return "crate:" + label | ||
| } | ||
| } | ||
|
|
||
| // CrateDBContainer is a running, single-node CrateDB testcontainer. | ||
| type CrateDBContainer struct { | ||
| testcontainers.Container | ||
| } | ||
|
|
||
| // RunCrateDB starts a single-node CrateDB container and blocks until its HTTP | ||
| // endpoint (port 4200) answers with HTTP 200. Always Terminate the returned | ||
| // container when done (see TestMain and the per-test examples). | ||
| func RunCrateDB(ctx context.Context) (*CrateDBContainer, error) { | ||
| req := testcontainers.ContainerRequest{ | ||
| Image: imageFromEnv(), | ||
| ExposedPorts: []string{httpPort, pgPort}, | ||
| Cmd: []string{"-Cdiscovery.type=single-node"}, | ||
| WaitingFor: wait.ForHTTP("/"). | ||
| WithPort(httpPort). | ||
| WithStatusCodeMatcher(func(status int) bool { return status == 200 }). | ||
| WithStartupTimeout(180 * time.Second), | ||
| } | ||
|
|
||
| container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{ | ||
| ContainerRequest: req, | ||
| Started: true, | ||
| }) | ||
| if err != nil { | ||
| return nil, fmt.Errorf("start cratedb container: %w", err) | ||
| } | ||
|
|
||
| return &CrateDBContainer{Container: container}, nil | ||
| } | ||
|
|
||
| // ConnectionString returns a pgx/libpq-compatible URL for CrateDB's | ||
| // PostgreSQL wire-protocol endpoint (port 5432). | ||
| func (c *CrateDBContainer) ConnectionString(ctx context.Context) (string, error) { | ||
| host, err := c.Host(ctx) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| port, err := c.MappedPort(ctx, pgPort) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| return fmt.Sprintf("postgres://%s@%s:%s/%s", defaultUser, host, port.Port(), defaultDatabase), nil | ||
| } | ||
|
|
||
| // HTTPEndpoint returns the base URL of CrateDB's HTTP endpoint (port 4200), | ||
| // e.g. for the REST `/_sql` API or the admin UI. | ||
| func (c *CrateDBContainer) HTTPEndpoint(ctx context.Context) (string, error) { | ||
| host, err := c.Host(ctx) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| port, err := c.MappedPort(ctx, httpPort) | ||
| if err != nil { | ||
| return "", err | ||
| } | ||
|
|
||
| return fmt.Sprintf("http://%s:%s", host, port.Port()), nil | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| package cratedb | ||
|
|
||
| import ( | ||
| "context" | ||
| "testing" | ||
|
|
||
| "github.com/jackc/pgx/v5" | ||
| ) | ||
|
|
||
| // Per-test container. | ||
| // | ||
| // Each invocation gets its own throwaway CrateDB, started inside the test and | ||
| // removed via t.Cleanup. Prefer the shared container (see TestMain) for speed; | ||
| // reach for a per-test container only when a test needs perfect isolation | ||
| // (e.g. cluster-wide settings, different CrateDB versions in one run). | ||
| func TestFunctionScope(t *testing.T) { | ||
| ctx := context.Background() | ||
|
|
||
| container, err := RunCrateDB(ctx) | ||
| if err != nil { | ||
| t.Fatalf("start container: %v", err) | ||
| } | ||
|
|
||
| t.Cleanup(func() { | ||
| if err := container.Terminate(ctx); err != nil { | ||
| t.Errorf("terminate container: %v", err) | ||
| } | ||
| }) | ||
|
|
||
| dsn, err := container.ConnectionString(ctx) | ||
| if err != nil { | ||
| t.Fatalf("connection string: %v", err) | ||
| } | ||
|
|
||
| conn, err := pgx.Connect(ctx, dsn) | ||
| if err != nil { | ||
| t.Fatalf("connect: %v", err) | ||
| } | ||
| defer conn.Close(ctx) | ||
|
|
||
| var clusterName string | ||
| if err := conn.QueryRow(ctx, "SELECT name FROM sys.cluster").Scan(&clusterName); err != nil { | ||
| t.Fatalf("query sys.cluster: %v", err) | ||
| } | ||
| if clusterName == "" { | ||
| t.Fatal("empty cluster name") | ||
| } | ||
| t.Logf("cluster name: %s", clusterName) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| module github.com/cratedb-examples/testing/testcontainers/go-pgx | ||
|
|
||
| go 1.26.0 | ||
|
|
||
| require ( | ||
| github.com/jackc/pgx/v5 v5.10.0 | ||
| github.com/testcontainers/testcontainers-go v0.42.0 | ||
| ) | ||
|
|
||
| require ( | ||
| dario.cat/mergo v1.0.2 // indirect | ||
| github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect | ||
| github.com/Microsoft/go-winio v0.6.2 // indirect | ||
| github.com/cenkalti/backoff/v4 v4.3.0 // indirect | ||
| github.com/cespare/xxhash/v2 v2.3.0 // indirect | ||
| github.com/containerd/errdefs v1.0.0 // indirect | ||
| github.com/containerd/errdefs/pkg v0.3.0 // indirect | ||
| github.com/containerd/log v0.1.0 // indirect | ||
| github.com/containerd/platforms v0.2.1 // indirect | ||
| github.com/cpuguy83/dockercfg v0.3.2 // indirect | ||
| github.com/davecgh/go-spew v1.1.1 // indirect | ||
| github.com/distribution/reference v0.6.0 // indirect | ||
| github.com/docker/go-connections v0.7.0 // indirect | ||
| github.com/docker/go-units v0.5.0 // indirect | ||
| github.com/ebitengine/purego v0.10.1 // indirect | ||
| github.com/felixge/httpsnoop v1.0.4 // indirect | ||
| github.com/go-logr/logr v1.4.3 // indirect | ||
| github.com/go-logr/stdr v1.2.2 // indirect | ||
| github.com/go-ole/go-ole v1.3.0 // indirect | ||
| github.com/google/uuid v1.6.0 // indirect | ||
| github.com/jackc/pgpassfile v1.0.0 // indirect | ||
| github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect | ||
| github.com/klauspost/compress v1.18.6 // indirect | ||
| github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect | ||
| github.com/magiconair/properties v1.8.10 // indirect | ||
| github.com/moby/docker-image-spec v1.3.1 // indirect | ||
| github.com/moby/go-archive v0.2.0 // indirect | ||
| github.com/moby/moby/api v1.54.2 // indirect | ||
| github.com/moby/moby/client v0.4.1 // indirect | ||
| github.com/moby/patternmatcher v0.6.1 // indirect | ||
| github.com/moby/sys/sequential v0.7.0 // indirect | ||
| github.com/moby/sys/user v0.4.0 // indirect | ||
| github.com/moby/sys/userns v0.1.0 // indirect | ||
| github.com/moby/term v0.5.2 // indirect | ||
| github.com/opencontainers/go-digest v1.0.0 // indirect | ||
| github.com/opencontainers/image-spec v1.1.1 // indirect | ||
| github.com/pmezard/go-difflib v1.0.0 // indirect | ||
| github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect | ||
| github.com/shirou/gopsutil/v4 v4.26.5 // indirect | ||
| github.com/sirupsen/logrus v1.9.4 // indirect | ||
| github.com/stretchr/testify v1.11.1 // indirect | ||
| github.com/tklauser/go-sysconf v0.4.0 // indirect | ||
| github.com/tklauser/numcpus v0.12.0 // indirect | ||
| github.com/yusufpapurcu/wmi v1.2.4 // indirect | ||
| go.opentelemetry.io/auto/sdk v1.2.1 // indirect | ||
| go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 // indirect | ||
| go.opentelemetry.io/otel v1.44.0 // indirect | ||
| go.opentelemetry.io/otel/metric v1.44.0 // indirect | ||
| go.opentelemetry.io/otel/trace v1.44.0 // indirect | ||
| golang.org/x/crypto v0.53.0 // indirect | ||
| golang.org/x/sys v0.46.0 // indirect | ||
| golang.org/x/text v0.38.0 // indirect | ||
| gopkg.in/yaml.v3 v3.0.1 // indirect | ||
| ) |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.