Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,11 @@ updates:

# Testing.

- directory: "/testing/testcontainers/go-pgx"
package-ecosystem: "gomod"
schedule:
interval: "daily"

- directory: "/testing/testcontainers/java"
package-ecosystem: "gradle"
schedule:
Expand Down
60 changes: 60 additions & 0 deletions .github/workflows/testing-testcontainers-go.yml
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 ./...
85 changes: 85 additions & 0 deletions testing/testcontainers/go-pgx/README.md
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.
Comment thread
florinutz marked this conversation as resolved.

## 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/
105 changes: 105 additions & 0 deletions testing/testcontainers/go-pgx/cratedb.go
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
}
49 changes: 49 additions & 0 deletions testing/testcontainers/go-pgx/function_scope_test.go
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)
}
64 changes: 64 additions & 0 deletions testing/testcontainers/go-pgx/go.mod
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
)
Loading
Loading