diff --git a/.claude/skills/testcontainers-guides-migrator/SKILL.md b/.claude/skills/testcontainers-guides-migrator/SKILL.md new file mode 100644 index 000000000000..45c13a0aa176 --- /dev/null +++ b/.claude/skills/testcontainers-guides-migrator/SKILL.md @@ -0,0 +1,315 @@ +--- +name: testcontainers-guide-migrator +description: > + Migrate a Testcontainers guide from testcontainers.com into the Docker docs site (docs.docker.com). + Converts AsciiDoc to Hugo Markdown, updates code to the latest Testcontainers API, splits into + chapters with stepper navigation, verifies code compiles and tests pass, and validates against + Docker docs style rules. Use when asked to migrate a testcontainers guide, add a TC guide, or + port content from testcontainers.com to Docker docs. +--- + +# Migrate a Testcontainers Guide + +You are migrating guides from https://testcontainers.com/guides/ into the Docker docs Hugo site. +Each guide lives in its own GitHub repo under `testcontainers/tc-guide-*`, written in AsciiDoc. +The source repos are listed in the testcontainers-site build.sh: +https://github.com/testcontainers/testcontainers-site/blob/main/build.sh#L23-L45 + +## Inputs + +The user provides one or more guides to migrate. Resolve these from the inventory below: + +- **REPO_NAME**: GitHub repo (e.g. `tc-guide-getting-started-with-testcontainers-for-java`) +- **SLUG**: guide slug inside `guide/` dir (e.g. `getting-started-with-testcontainers-for-java`) +- **LANG**: language identifier (go, java, dotnet, nodejs, python) +- **GUIDE_ID**: short kebab-case name (e.g. `getting-started`) + +## Guide inventory + +These are the 21 guides from testcontainers.com/guides/ and their source repos: + +| # | Title | Repo | Lang | GUIDE_ID | +|---|-------|------|------|----------| +| 1 | Introduction to Testcontainers | tc-guide-introducing-testcontainers | (none) | introducing | +| 2 | Getting started for Java | tc-guide-getting-started-with-testcontainers-for-java | java | getting-started | +| 3 | Testing Spring Boot REST API | tc-guide-testing-spring-boot-rest-api | java | spring-boot-rest-api | +| 4 | Testcontainers lifecycle (JUnit 5) | tc-guide-testcontainers-lifecycle | java | lifecycle | +| 5 | Configuration of services in container | tc-guide-configuration-of-services-running-in-container | java | service-configuration | +| 6 | Replace H2 with real database | tc-guide-replace-h2-with-real-database-for-testing | java | replace-h2 | +| 7 | Testing ASP.NET Core web app | tc-guide-testing-aspnet-core | dotnet | aspnet-core | +| 8 | Testing Spring Boot Kafka Listener | tc-guide-testing-spring-boot-kafka-listener | java | spring-boot-kafka | +| 9 | REST API integrations with MockServer | tc-guide-testing-rest-api-integrations-using-mockserver | java | mockserver | +| 10 | Getting started for .NET | tc-guide-getting-started-with-testcontainers-for-dotnet | dotnet | getting-started | +| 11 | AWS integrations with LocalStack | tc-guide-testing-aws-service-integrations-using-localstack | java | aws-localstack | +| 12 | Testcontainers in Quarkus apps | tc-guide-testcontainers-in-quarkus-applications | java | quarkus | +| 13 | Getting started for Go | tc-guide-getting-started-with-testcontainers-for-go | go | getting-started | +| 14 | jOOQ and Flyway with Testcontainers | tc-guide-working-with-jooq-flyway-using-testcontainers | java | jooq-flyway | +| 15 | Getting started for Node.js | tc-guide-getting-started-with-testcontainers-for-nodejs | nodejs | getting-started | +| 16 | REST API integrations with WireMock | tc-guide-testing-rest-api-integrations-using-wiremock | java | wiremock | +| 17 | Local dev with Testcontainers Desktop | tc-guide-simple-local-development-with-testcontainers-desktop | java | local-dev-desktop | +| 18 | Micronaut REST API with WireMock | tc-guide-testing-rest-api-integrations-in-micronaut-apps-using-wiremock | java | micronaut-wiremock | +| 19 | Micronaut Kafka Listener | tc-guide-testing-micronaut-kafka-listener | java | micronaut-kafka | +| 20 | Getting started for Python | tc-guide-getting-started-with-testcontainers-for-python | python | getting-started | +| 21 | Keycloak with Spring Boot | tc-guide-securing-spring-boot-microservice-using-keycloak-and-testcontainers | java | keycloak-spring-boot | + +Already migrated: **#13 (Go getting-started)**, **#20 (Python getting-started)** + +## Step 0: Pre-flight + +1. Confirm `testing-with-docker` tag exists in `data/tags.yaml`. If not, add: + ```yaml + testing-with-docker: + title: Testing with Docker + ``` +2. Check if new terms need adding to `_vale/config/vocabularies/Docker/accept.txt`. +3. Read `STYLE.md` and `COMPONENTS.md` to refresh on Docker docs conventions. + +## Step 1: Clone the guide repo + +Clone the guide repo to a temporary directory. This gives you all source files locally — no HTTP calls needed. + +```bash +git clone --depth 1 https://github.com/testcontainers/{REPO_NAME}.git /{REPO_NAME} +``` + +Where `` is a temporary directory on your system (e.g. the output of `mktemp -d`). + +The repo structure is: +- `/{REPO_NAME}/guide/{SLUG}/index.adoc` — the AsciiDoc guide source +- `/{REPO_NAME}/src/` — application source code (referenced by `include::` directives) +- `/{REPO_NAME}/testdata/` — test data files (SQL scripts, configs, etc.) +- `/{REPO_NAME}/pom.xml` or `go.mod` — build config + +1. Read `guide/{SLUG}/index.adoc` to get the guide content. +2. Find all `include::{codebase}/path/to/file[]` directives. The `{codebase}` attribute points to a remote URL, but since you have the repo cloned, read the files directly from disk instead (e.g. `include::{codebase}/src/main/java/Foo.java[]` → read `/{REPO_NAME}/src/main/java/Foo.java`). +3. If includes have `[lines="X..Y"]`, extract only those lines from the local file. +4. Note the `[source,lang]` block preceding each include — that determines the code fence language. + +This cloned repo also serves as the base for Step 6 (code verification) — you can run the tests directly in it to confirm they pass before updating the code to the latest API. + +## Step 2: Convert AsciiDoc to Markdown + +| AsciiDoc | Markdown | +|---|---| +| `== Heading` | `## Heading` | +| `=== Heading` | `### Heading` | +| `*bold*` (AsciiDoc bold) | `**bold**` | +| `https://url[Link text]` | `[Link text](url)` | +| `[source,lang]\n----\ncode\n----` | `` ```lang\ncode\n``` `` | +| `[source,shell]` with `$` prompts | `` ```console `` | +| `[NOTE]\ntext` or `====\n[NOTE]\n...\n====` | `> [!NOTE]\n> text` | +| `[TIP]\ntext` | `> [!TIP]\n> text` | +| `:toc:`, `:toclevels:`, `:codebase:` | Remove entirely | +| `include::{codebase}/path[]` | Replace with fetched code in a code fence | +| YAML front matter (date, draft, repo) | Remove; transform to Docker docs format | + +## Step 3: Apply Docker docs style rules + +These are mandatory (from STYLE.md and AGENTS.md): + +- **No "we"**: "We are going to create" → "Create" or "Start by creating" +- **No "let us" / "let's"**: → imperative voice or "You can..." +- **No hedge words**: remove "simply", "easily", "just", "seamlessly" +- **No meta-commentary**: remove "it's worth noting", "it's important to understand" +- **No "allows you to" / "enables you to"**: → "lets you" or rephrase +- **No "click"**: → "select" +- **No bold for emphasis or product names**: only bold UI elements +- **No time-relative language**: remove "currently", "new", "recently", "now" +- **No exclamations**: remove "Voila!!!" etc. +- Use `console` language hint for interactive shell blocks with `$` prompts +- Use contractions: "it's", "you're", "don't" + +## Step 4: Update code to latest Testcontainers API + +Research the latest API version for the target language before writing code. + +**Best practices reference**: The Testcontainers team maintains Claude skills with up-to-date API patterns and best practices for each language at https://github.com/testcontainers/claude-skills/ — check the relevant language skill (testcontainers-go, testcontainers-node, testcontainers-dotnet) for current API signatures, cleanup patterns, wait strategies, and anti-patterns to avoid. + +For each language, check the cloned repo's existing code, then update to the latest API. Key patterns per language: + +**Go** (testcontainers-go v0.41.0): +- `postgres.RunContainer(ctx, opts...)` → `postgres.Run(ctx, "image", opts...)` +- `testcontainers.WithImage(...)` → image is now the 2nd positional param to `Run()` +- Manual `WithWaitStrategy(wait.ForLog(...))` → `postgres.BasicWaitStrategies()` +- `t.Cleanup(func() { ctr.Terminate(ctx) })` → `testcontainers.CleanupContainer(t, ctr)` +- `if err != nil { log.Fatal(err) }` → `require.NoError(t, err)` (use testify require/assert) +- Helper functions should accept `t *testing.T` as first param, call `t.Helper()` +- No `TearDownSuite()` needed if `CleanupContainer` is registered in the helper +- Go version prerequisite: 1.25+ + +**Java** (testcontainers-java): +- Check the latest BOM version at https://java.testcontainers.org/ +- Use `@Testcontainers` and `@Container` annotations for JUnit 5 lifecycle +- Prefer module-specific containers (e.g. `PostgreSQLContainer`) over `GenericContainer` +- Use `@DynamicPropertySource` for Spring Boot integration + +**.NET** (testcontainers-dotnet): +- Check the latest NuGet package version +- Use `IAsyncLifetime` for container lifecycle in xUnit +- Use builder pattern: `new PostgreSqlBuilder().Build()` + +**Node.js** (testcontainers-node): +- Check the latest npm version +- Use module-specific packages (e.g. `@testcontainers/postgresql`) +- Use `GenericContainer` for services without a dedicated module + +**Python** (testcontainers-python): +- Check the latest PyPI version +- Use context managers (`with PostgresContainer() as postgres:`) +- Use module-specific containers when available + +For all languages: consult the corresponding Testcontainers skill at https://github.com/testcontainers/claude-skills/ for current best practices and anti-patterns. + +## Step 5: Create guide directory structure + +Directory: `content/guides/testcontainers-{LANG}-{GUIDE_ID}/` + +Each guide is its own top-level entry under `/guides/`. Do NOT nest guides inside a shared parent section — otherwise they won't appear individually in the tag/language filters on the guides listing page. + +### _index.md (landing page) + +```yaml +--- +title: {Full guide title} +linkTitle: {Short title for guides listing} +description: {One-line description} +keywords: testcontainers, {lang}, testing, {technologies used} +summary: | + {2-3 line summary for the guides listing card} +toc_min: 1 +toc_max: 2 +tags: [testing-with-docker] +languages: [{lang}] +params: + time: {estimated} minutes +--- + + +``` + +Content: what you'll learn (bulleted list), prerequisites, and a NOTE linking to `https://testcontainers.com/getting-started/` for newcomers. + +### Sub-pages (chapters) + +Split the guide into logical chapters. Each sub-page: + +```yaml +--- +title: {Chapter title} +linkTitle: {Short title for stepper} +description: {One-line description} +weight: {10, 20, 30, ...} +--- +``` + +**No `tags`, `languages`, or `params` on sub-pages** — only on `_index.md`. + +Typical chapter breakdown: +| Weight | File | Content | +|--------|------|---------| +| 10 | `create-project.md` | Project setup, dependencies, business logic | +| 20 | `write-tests.md` | First test using testcontainers | +| 30 | `test-suites.md` | Reusing containers, test helpers, suites | +| 40 | `run-tests.md` | Running tests, summary, further reading | + +Adapt the split to the guide's content — some guides may need fewer or more chapters. + +## Step 6: Verify code compiles and tests pass + +This is CRITICAL. The code in the guide MUST compile and all tests MUST pass. Do not skip this step. + +### 6a: Use the cloned repo as the verification project + +The repo you cloned in Step 1 (`/{REPO_NAME}`) already contains a working project with all source files, build config, and tests. Use it as the starting point: + +```bash +cd /{REPO_NAME} +``` + +First, verify the **original** code compiles and tests pass before you change anything. This confirms a good baseline. + +### 6b: Update the code in the cloned repo + +After confirming the original works, apply the API updates (from Step 4) directly in the cloned repo's source files. This is the same code you're putting in the guide — keep them in sync. + +### 6c: Update dependencies and compile + +Run compilation inside a container for reproducibility — no need to install the language toolchain on the host. Use the appropriate language Docker image, mounting the cloned repo: + +```bash +docker run --rm -v "/{REPO_NAME}":/app -w /app sh -c "" +``` + +Pick the right image for the language (e.g. `golang:1.25-alpine`, `maven:3-eclipse-temurin-21`, `gradle:jdk21`, `mcr.microsoft.com/dotnet/sdk:9.0`, `node:22-alpine`, `python:3.13-alpine`). Update dependencies to the latest Testcontainers version and compile. + +If compilation fails, fix the code and update the guide markdown to match. + +### 6d: Run tests in a container with Docker socket mounted + +Run tests in the same kind of container, but **mount the Docker socket** so Testcontainers can create sibling containers: + +```bash +docker run --rm \ + -v "/{REPO_NAME}":/app \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -w /app \ + sh -c "" +``` + +The key is `-v /var/run/docker.sock:/var/run/docker.sock` — this lets Testcontainers inside the container talk to the host's Docker daemon and create sibling containers. + +### 6e: Fix until green + +If any test fails, debug and fix the code in both the temporary project AND the guide markdown. Re-run until all tests pass. Do not proceed until verified. + +## Step 7: Update cross-references + +1. **`content/manuals/testcontainers.md`**: Add a bullet under the `## Guides` section: + ```markdown + - [Guide title](/guides/testcontainers-{LANG}-{GUIDE_ID}/) + ``` +2. **Do NOT update** `content/guides/testcontainers-cloud/_index.md` — keep its external links. +3. Link to `https://testcontainers.com/getting-started/` for the Testcontainers overview. +4. Use internal paths for already-migrated guides; keep `testcontainers.com` links for unmigrated ones. + +## Step 8: Validate + +1. `npx prettier --write content/guides/testcontainers-{LANG}-{GUIDE_ID}/` +2. `npx prettier --write content/manuals/testcontainers.md` +3. `docker buildx bake lint` — must pass +4. `docker buildx bake vale` — check `tmp/vale.out` for errors in new files + - Spelling errors for tech terms: add to `_vale/config/vocabularies/Docker/accept.txt` +5. Verify in local dev server (`HUGO_PORT=1314 docker compose watch`): + - Guide appears when filtering by its language + - Guide appears when filtering by `Testing with Docker` tag + - Stepper navigation works across chapters + - All links resolve (no 404s) +6. Verify all external URLs return 200: + ```bash + curl -s -o /dev/null -w "%{http_code}" -L "{url}" + ``` + +## Step 9: Commit + +One commit per guide. Message format: +``` +feat(guides): add testcontainers {lang} {guide-id} guide + +Migrated from https://github.com/testcontainers/{REPO_NAME} +Updated to testcontainers-{lang} v{version} API. +``` + +## Special cases + +- **introducing-testcontainers**: Language-agnostic, conceptual. May overlap with `content/manuals/testcontainers.md`. Review for deduplication before migrating. +- **local-dev-testcontainers-desktop**: About Testcontainers Desktop (now part of Docker Desktop). May need significant rewriting rather than mechanical migration. +- **Java guides**: Many share the same language. Each still gets its own `testcontainers-java-{GUIDE_ID}` directory. + +## Reference: completed migration (Go getting-started) + +Use `content/guides/testcontainers-go-getting-started/` as the reference implementation: +- `_index.md` — landing page with frontmatter, prerequisites, learning objectives +- `create-project.md` (weight: 10) — project setup and business logic +- `write-tests.md` (weight: 20) — first test with testcontainers-go +- `test-suites.md` (weight: 30) — container reuse with testify suites +- `run-tests.md` (weight: 40) — running tests, summary, further reading diff --git a/_vale/config/vocabularies/Docker/accept.txt b/_vale/config/vocabularies/Docker/accept.txt index 6bd48433dca8..44da687bf63a 100644 --- a/_vale/config/vocabularies/Docker/accept.txt +++ b/_vale/config/vocabularies/Docker/accept.txt @@ -167,11 +167,14 @@ Paketo PAT perl pgAdmin +pgx PKG plaintext plist pluggable Postgres +psycopg +pytest PowerShell Python Qualcomm diff --git a/content/guides/testcontainers-go-getting-started/_index.md b/content/guides/testcontainers-go-getting-started/_index.md new file mode 100644 index 000000000000..5ec598979d08 --- /dev/null +++ b/content/guides/testcontainers-go-getting-started/_index.md @@ -0,0 +1,36 @@ +--- +title: Getting started with Testcontainers for Go +linkTitle: Testcontainers for Go +description: Learn how to use Testcontainers for Go to test database interactions with a real PostgreSQL instance. +keywords: testcontainers, go, golang, testing, postgresql, integration testing +summary: | + Learn how to create a Go application and test database interactions + using Testcontainers for Go with a real PostgreSQL instance. +toc_min: 1 +toc_max: 2 +tags: [testing-with-docker] +languages: [go] +params: + time: 20 minutes +--- + + + +In this guide, you will learn how to: + +- Create a Go application with modules support +- Implement a Repository to manage customer data in a PostgreSQL database using the pgx driver +- Write integration tests using testcontainers-go +- Reuse containers across multiple tests using test suites + +## Prerequisites + +- Go 1.25+ +- Your preferred IDE (VS Code, GoLand) +- A Docker environment supported by Testcontainers. For details, see + the [testcontainers-go system requirements](https://golang.testcontainers.org/system_requirements/). + +> [!NOTE] +> If you're new to Testcontainers, visit the +> [Testcontainers overview](https://testcontainers.com/getting-started/) to learn more about +> Testcontainers and the benefits of using it. diff --git a/content/guides/testcontainers-go-getting-started/create-project.md b/content/guides/testcontainers-go-getting-started/create-project.md new file mode 100644 index 000000000000..2fa3579e2923 --- /dev/null +++ b/content/guides/testcontainers-go-getting-started/create-project.md @@ -0,0 +1,103 @@ +--- +title: Create the Go project +linkTitle: Create the project +description: Set up a Go project with a PostgreSQL-backed repository. +weight: 10 +--- + +## Initialize the project + +Start by creating a Go project. + +```console +$ mkdir testcontainers-go-demo +$ cd testcontainers-go-demo +$ go mod init github.com/testcontainers/testcontainers-go-demo +``` + +This guide uses the [jackc/pgx](https://github.com/jackc/pgx) PostgreSQL +driver to interact with the Postgres database and the testcontainers-go +[Postgres module](https://golang.testcontainers.org/modules/postgres/) to +spin up a Postgres Docker instance for testing. It also uses +[testify](https://github.com/stretchr/testify) for running multiple tests +as a suite and for writing assertions. + +Install these dependencies: + +```console +$ go get github.com/jackc/pgx/v5 +$ go get github.com/testcontainers/testcontainers-go +$ go get github.com/testcontainers/testcontainers-go/modules/postgres +$ go get github.com/stretchr/testify +``` + +## Create Customer struct + +Create a `types.go` file in the `customer` package and define the `Customer` +struct to model the customer details: + +```go +package customer + +type Customer struct { + Id int + Name string + Email string +} +``` + +## Create Repository + +Next, create `customer/repo.go`, define the `Repository` struct, and add +methods to create a customer and get a customer by email: + +```go +package customer + +import ( + "context" + "fmt" + "os" + + "github.com/jackc/pgx/v5" +) + +type Repository struct { + conn *pgx.Conn +} + +func NewRepository(ctx context.Context, connStr string) (*Repository, error) { + conn, err := pgx.Connect(ctx, connStr) + if err != nil { + _, _ = fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err) + return nil, err + } + return &Repository{ + conn: conn, + }, nil +} + +func (r Repository) CreateCustomer(ctx context.Context, customer Customer) (Customer, error) { + err := r.conn.QueryRow(ctx, + "INSERT INTO customers (name, email) VALUES ($1, $2) RETURNING id", + customer.Name, customer.Email).Scan(&customer.Id) + return customer, err +} + +func (r Repository) GetCustomerByEmail(ctx context.Context, email string) (Customer, error) { + var customer Customer + query := "SELECT id, name, email FROM customers WHERE email = $1" + err := r.conn.QueryRow(ctx, query, email). + Scan(&customer.Id, &customer.Name, &customer.Email) + if err != nil { + return Customer{}, err + } + return customer, nil +} +``` + +Here's what the code does: + +- `Repository` holds a `*pgx.Conn` for performing database operations. +- `NewRepository(connStr)` takes a database connection string and initializes a `Repository`. +- `CreateCustomer()` and `GetCustomerByEmail()` are methods on the `Repository` receiver that insert and query customer records. diff --git a/content/guides/testcontainers-go-getting-started/run-tests.md b/content/guides/testcontainers-go-getting-started/run-tests.md new file mode 100644 index 000000000000..49c1f128ddc8 --- /dev/null +++ b/content/guides/testcontainers-go-getting-started/run-tests.md @@ -0,0 +1,36 @@ +--- +title: Run tests and next steps +linkTitle: Run tests +description: Run your Testcontainers-based integration tests and explore next steps. +weight: 40 +--- + +## Run the tests + +Run all the tests using `go test ./...`. Optionally add the `-v` flag for +verbose output: + +```console +$ go test -v ./... +``` + +You should see two Postgres Docker containers start automatically: one for the +suite and its two tests, and another for the initial standalone test. All tests +should pass. After the tests finish, the containers are stopped and removed +automatically. + +## Summary + +The Testcontainers for Go library helps you write integration tests by using +the same type of database (Postgres) that you use in production, instead of +mocks. Because you aren't using mocks and instead talk to real services, you're +free to refactor code and still verify that the application works as expected. + +To learn more about Testcontainers, visit the +[Testcontainers overview](https://testcontainers.com/getting-started/). + +## Further reading + +- [Testcontainers for Go documentation](https://golang.testcontainers.org/) +- [Testcontainers for Go quickstart](https://golang.testcontainers.org/quickstart/) +- [Testcontainers Postgres module for Go](https://golang.testcontainers.org/modules/postgres/) diff --git a/content/guides/testcontainers-go-getting-started/test-suites.md b/content/guides/testcontainers-go-getting-started/test-suites.md new file mode 100644 index 000000000000..67741ad65d66 --- /dev/null +++ b/content/guides/testcontainers-go-getting-started/test-suites.md @@ -0,0 +1,144 @@ +--- +title: Reuse containers with test suites +linkTitle: Test suites +description: Share a single Postgres container across multiple tests using testify suites. +weight: 30 +--- + +In the previous section, you saw how to spin up a Postgres Docker container +for a single test. But often you have multiple tests in a single file, and you +may want to reuse the same Postgres Docker container for all of them. + +You can use the [testify suite](https://pkg.go.dev/github.com/stretchr/testify/suite) +package to implement common test setup and teardown actions. + +## Extract container setup + +First, extract the `PostgresContainer` creation logic into a separate file +called `testhelpers/containers.go`: + +```go +package testhelpers + +import ( + "context" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/postgres" +) + +type PostgresContainer struct { + *postgres.PostgresContainer + ConnectionString string +} + +func CreatePostgresContainer(t *testing.T, ctx context.Context) *PostgresContainer { + t.Helper() + + ctr, err := postgres.Run(ctx, + "postgres:16-alpine", + postgres.WithInitScripts(filepath.Join("..", "testdata", "init-db.sql")), + postgres.WithDatabase("test-db"), + postgres.WithUsername("postgres"), + postgres.WithPassword("postgres"), + postgres.BasicWaitStrategies(), + ) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + connStr, err := ctr.ConnectionString(ctx, "sslmode=disable") + require.NoError(t, err) + + return &PostgresContainer{ + PostgresContainer: ctr, + ConnectionString: connStr, + } +} +``` + +In `containers.go`, `PostgresContainer` extends the testcontainers-go +`PostgresContainer` to provide easy access to `ConnectionString`. The +`CreatePostgresContainer()` function accepts `*testing.T` as its first +parameter, calls `t.Helper()` so that test failures point to the caller, +and uses `testcontainers.CleanupContainer()` to register automatic cleanup. + +## Write the test suite + +Create `customer/repo_suite_test.go` and implement tests for creating +a customer and getting a customer by email using the testify suite package: + +```go +package customer + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + "github.com/testcontainers/testcontainers-go-demo/testhelpers" +) + +type CustomerRepoTestSuite struct { + suite.Suite + pgContainer *testhelpers.PostgresContainer + repository *Repository + ctx context.Context +} + +func (suite *CustomerRepoTestSuite) SetupSuite() { + suite.ctx = context.Background() + suite.pgContainer = testhelpers.CreatePostgresContainer(suite.T(), suite.ctx) + + repository, err := NewRepository(suite.ctx, suite.pgContainer.ConnectionString) + require.NoError(suite.T(), err) + suite.repository = repository +} + +func (suite *CustomerRepoTestSuite) TestCreateCustomer() { + t := suite.T() + + customer, err := suite.repository.CreateCustomer(suite.ctx, Customer{ + Name: "Henry", + Email: "henry@gmail.com", + }) + require.NoError(t, err) + assert.NotNil(t, customer.Id) +} + +func (suite *CustomerRepoTestSuite) TestGetCustomerByEmail() { + t := suite.T() + + customer, err := suite.repository.GetCustomerByEmail(suite.ctx, "john@gmail.com") + require.NoError(t, err) + assert.Equal(t, "John", customer.Name) + assert.Equal(t, "john@gmail.com", customer.Email) +} + +func TestCustomerRepoTestSuite(t *testing.T) { + suite.Run(t, new(CustomerRepoTestSuite)) +} +``` + +Here's what the code does: + +- `CustomerRepoTestSuite` extends `suite.Suite` and includes fields shared + across multiple tests. +- `SetupSuite()` runs once before all tests. It calls + `CreatePostgresContainer(suite.T(), ...)` which handles cleanup registration + automatically via `CleanupContainer`, so no `TearDownSuite()` is needed. +- `TestCreateCustomer()` uses `require.NoError()` for the create operation + (fail immediately if it errors) and `assert.NotNil()` for the ID check. +- `TestGetCustomerByEmail()` uses `require.NoError()` then asserts on the + returned values. +- `TestCustomerRepoTestSuite(t *testing.T)` runs the test suite when you + execute `go test`. + +> [!TIP] +> For the purpose of this guide, the tests don't reset data in the database. +> In practice, it's a good idea to reset the database to a known state before +> running each test. diff --git a/content/guides/testcontainers-go-getting-started/write-tests.md b/content/guides/testcontainers-go-getting-started/write-tests.md new file mode 100644 index 000000000000..893bf73c2310 --- /dev/null +++ b/content/guides/testcontainers-go-getting-started/write-tests.md @@ -0,0 +1,107 @@ +--- +title: Write tests with Testcontainers +linkTitle: Write tests +description: Write your first integration test using testcontainers-go and PostgreSQL. +weight: 20 +--- + +You have the `Repository` implementation ready, but for testing you need a +PostgreSQL database. You can use testcontainers-go to spin up a Postgres +database in a Docker container and run your tests against that database. + +## Set up the test database + +In real applications you might use a database migration tool, but for this +guide, use a script to initialize the database. + +Create a `testdata/init-db.sql` file to create the `CUSTOMERS` table and +insert sample data: + +```sql +CREATE TABLE IF NOT EXISTS customers (id serial, name varchar(255), email varchar(255)); + +INSERT INTO customers(name, email) VALUES ('John', 'john@gmail.com'); +``` + +## Understand the testcontainers-go API + +The testcontainers-go library provides the generic `Container` abstraction +that can run any containerized service. To further simplify, testcontainers-go +provides technology-specific modules that reduce boilerplate and provide a +functional options pattern to construct the container instance. + +For example, `PostgresContainer` provides `WithDatabase()`, +`WithUsername()`, `WithPassword()`, and other functions to set various +properties of Postgres containers. + +## Write the test + +Create the `customer/repo_test.go` file and implement the test: + +```go +package customer + +import ( + "context" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/testcontainers/testcontainers-go" + "github.com/testcontainers/testcontainers-go/modules/postgres" +) + +func TestCustomerRepository(t *testing.T) { + ctx := context.Background() + + ctr, err := postgres.Run(ctx, + "postgres:16-alpine", + postgres.WithInitScripts(filepath.Join("..", "testdata", "init-db.sql")), + postgres.WithDatabase("test-db"), + postgres.WithUsername("postgres"), + postgres.WithPassword("postgres"), + postgres.BasicWaitStrategies(), + ) + testcontainers.CleanupContainer(t, ctr) + require.NoError(t, err) + + connStr, err := ctr.ConnectionString(ctx, "sslmode=disable") + require.NoError(t, err) + + customerRepo, err := NewRepository(ctx, connStr) + require.NoError(t, err) + + c, err := customerRepo.CreateCustomer(ctx, Customer{ + Name: "Henry", + Email: "henry@gmail.com", + }) + assert.NoError(t, err) + assert.NotNil(t, c) + + customer, err := customerRepo.GetCustomerByEmail(ctx, "henry@gmail.com") + assert.NoError(t, err) + assert.NotNil(t, customer) + assert.Equal(t, "Henry", customer.Name) + assert.Equal(t, "henry@gmail.com", customer.Email) +} +``` + +Here's what the test does: + +- Calls `postgres.Run()` with the `postgres:16-alpine` Docker image as the + first argument. This is the v0.41.0 API — the image is a required positional + parameter instead of an option. +- Configures initialization scripts using `WithInitScripts(...)` so that the + `CUSTOMERS` table is created and sample data is inserted after the database + starts. +- Uses `postgres.BasicWaitStrategies()` which combines waiting for the Postgres + log message and for the port to be ready. This replaces manual wait strategy + configuration. +- Calls `testcontainers.CleanupContainer(t, ctr)` right after `postgres.Run()`. + This registers automatic cleanup with the test framework, replacing the manual + `t.Cleanup` and `Terminate` pattern. +- Obtains the database `ConnectionString` from the container and initializes a + `Repository`. +- Creates a customer with the email `henry@gmail.com` and verifies that the + customer exists in the database. diff --git a/content/guides/testcontainers-python-getting-started/_index.md b/content/guides/testcontainers-python-getting-started/_index.md new file mode 100644 index 000000000000..6a00efa89fcc --- /dev/null +++ b/content/guides/testcontainers-python-getting-started/_index.md @@ -0,0 +1,35 @@ +--- +title: Getting started with Testcontainers for Python +linkTitle: Testcontainers for Python +description: Learn how to use Testcontainers for Python to test database interactions with a real PostgreSQL instance. +keywords: testcontainers, python, testing, postgresql, integration testing, pytest +summary: | + Learn how to create a Python application and test database interactions + using Testcontainers for Python with a real PostgreSQL instance. +toc_min: 1 +toc_max: 2 +tags: [testing-with-docker] +languages: [python] +params: + time: 15 minutes +--- + + + +In this guide, you will learn how to: + +- Create a Python application that uses PostgreSQL to store customer data +- Use `psycopg` to interact with the database +- Write integration tests using `testcontainers-python` and `pytest` +- Manage container lifecycle with pytest fixtures + +## Prerequisites + +- Python 3.10+ +- pip +- A Docker environment supported by Testcontainers + +> [!NOTE] +> If you're new to Testcontainers, visit the +> [Testcontainers overview](https://testcontainers.com/getting-started/) to learn more about +> Testcontainers and the benefits of using it. diff --git a/content/guides/testcontainers-python-getting-started/create-project.md b/content/guides/testcontainers-python-getting-started/create-project.md new file mode 100644 index 000000000000..88ab26be5f5e --- /dev/null +++ b/content/guides/testcontainers-python-getting-started/create-project.md @@ -0,0 +1,130 @@ +--- +title: Create the Python project +linkTitle: Create the project +description: Set up a Python project with a PostgreSQL-backed customer service. +weight: 10 +--- + +## Initialize the project + +Start by creating a Python project with a virtual environment: + +```console +$ mkdir tc-python-demo +$ cd tc-python-demo +$ python3 -m venv venv +$ source venv/bin/activate +``` + +This guide uses [psycopg3](https://www.psycopg.org/psycopg3/) to interact +with the Postgres database, [pytest](https://pytest.org/) for testing, and +[testcontainers-python](https://testcontainers-python.readthedocs.io/) for +running a PostgreSQL database in a container. + +Install the dependencies: + +```console +$ pip install psycopg pytest testcontainers[postgres] +$ pip freeze > requirements.txt +``` + +The `pip freeze` command generates a `requirements.txt` file so that others +can install the same package versions using `pip install -r requirements.txt`. + +## Create the database helper + +Create a `db/connection.py` file with a function to get a database connection: + +```python +import os + +import psycopg + + +def get_connection(): + host = os.getenv("DB_HOST", "localhost") + port = os.getenv("DB_PORT", "5432") + username = os.getenv("DB_USERNAME", "postgres") + password = os.getenv("DB_PASSWORD", "postgres") + database = os.getenv("DB_NAME", "postgres") + return psycopg.connect(f"host={host} dbname={database} user={username} password={password} port={port}") +``` + +Instead of hard-coding the database connection parameters, the function uses +environment variables. This makes it possible to run the application in +different environments without changing code. + +## Create the business logic + +Create a `customers/customers.py` file and define the `Customer` class: + +```python +class Customer: + def __init__(self, cust_id, name, email): + self.id = cust_id + self.name = name + self.email = email + + def __str__(self): + return f"Customer({self.id}, {self.name}, {self.email})" +``` + +Add a `create_table()` function to create the `customers` table: + +```python +from db.connection import get_connection + + +def create_table(): + with get_connection() as conn: + with conn.cursor() as cur: + cur.execute(""" + CREATE TABLE customers ( + id serial PRIMARY KEY, + name varchar not null, + email varchar not null unique) + """) + conn.commit() +``` + +The function obtains a database connection using `get_connection()` and creates +the `customers` table. The `with` statement automatically closes the connection +when done. + +Add the remaining CRUD functions: + +```python +def create_customer(name, email): + with get_connection() as conn: + with conn.cursor() as cur: + cur.execute( + "INSERT INTO customers (name, email) VALUES (%s, %s)", (name, email)) + conn.commit() + + +def get_all_customers() -> list[Customer]: + with get_connection() as conn: + with conn.cursor() as cur: + cur.execute("SELECT * FROM customers") + return [Customer(cid, name, email) for cid, name, email in cur] + + +def get_customer_by_email(email) -> Customer: + with get_connection() as conn: + with conn.cursor() as cur: + cur.execute("SELECT id, name, email FROM customers WHERE email = %s", (email,)) + (cid, name, email) = cur.fetchone() + return Customer(cid, name, email) + + +def delete_all_customers(): + with get_connection() as conn: + with conn.cursor() as cur: + cur.execute("DELETE FROM customers") + conn.commit() +``` + +> [!NOTE] +> To keep it straightforward for this guide, each function creates a new +> connection. In a real-world application, use a connection pool to reuse +> connections. diff --git a/content/guides/testcontainers-python-getting-started/run-tests.md b/content/guides/testcontainers-python-getting-started/run-tests.md new file mode 100644 index 000000000000..47943db6b142 --- /dev/null +++ b/content/guides/testcontainers-python-getting-started/run-tests.md @@ -0,0 +1,51 @@ +--- +title: Run tests and next steps +linkTitle: Run tests +description: Run your Testcontainers-based integration tests and explore next steps. +weight: 30 +--- + +## Run the tests + +Run the tests using pytest: + +```console +$ pytest -v +``` + +You should see output similar to: + +```text +============================= test session starts ============================== +platform linux -- Python 3.13.x, pytest-9.x.x +collected 2 items + +tests/test_customers.py::test_get_all_customers PASSED [ 50%] +tests/test_customers.py::test_get_customer_by_email PASSED [100%] + +============================== 2 passed in 1.90s =============================== +``` + +The tests run against a real PostgreSQL database instead of mocks, which gives +more confidence in the implementation. + +## Summary + +The Testcontainers for Python library helps you write integration tests using the +same type of database (Postgres) that you use in production, instead of mocks. +Because you aren't using mocks and instead talk to real services, you're free +to refactor code and still verify that the application works as expected. + +In addition to PostgreSQL, Testcontainers for Python provides modules for many +SQL databases, NoSQL databases, messaging queues, and more. You can use +Testcontainers to run any containerized dependency for your tests. + +To learn more about Testcontainers, visit the +[Testcontainers overview](https://testcontainers.com/getting-started/). + +## Further reading + +- [testcontainers-python documentation](https://testcontainers-python.readthedocs.io/) +- [Getting started with Testcontainers for Go](/guides/testcontainers-go-getting-started/) +- [Getting started with Testcontainers for Java](https://testcontainers.com/guides/getting-started-with-testcontainers-for-java/) +- [Getting started with Testcontainers for Node.js](https://testcontainers.com/guides/getting-started-with-testcontainers-for-nodejs/) diff --git a/content/guides/testcontainers-python-getting-started/write-tests.md b/content/guides/testcontainers-python-getting-started/write-tests.md new file mode 100644 index 000000000000..9fec5e556a5a --- /dev/null +++ b/content/guides/testcontainers-python-getting-started/write-tests.md @@ -0,0 +1,104 @@ +--- +title: Write tests with Testcontainers +linkTitle: Write tests +description: Write integration tests using testcontainers-python and pytest with a real PostgreSQL database. +weight: 20 +--- + +You'll create a PostgreSQL container using Testcontainers and use it for all +the tests. Before each test, you'll delete all customer records so that tests +run with a clean database. + +## Set up pytest fixtures + +This guide uses [pytest fixtures](https://pytest.org/en/stable/how-to/fixtures.html) +for setup and teardown logic. A recommended approach is to use +[finalizers](https://pytest.org/en/stable/how-to/fixtures.html#adding-finalizers-directly) +to guarantee cleanup runs even if setup fails: + +```python +@pytest.fixture +def setup(request): + # setup code + + def cleanup(): + # teardown code + + request.addfinalizer(cleanup) + return some_value +``` + +## Create the test file + +Create a `tests/__init__.py` file with empty content to enable pytest +[auto-discovery](https://pytest.org/explanation/goodpractices.html#test-discovery). + +Then create `tests/test_customers.py` with the fixtures: + +```python +import os +import pytest +from testcontainers.postgres import PostgresContainer + +from customers import customers + +postgres = PostgresContainer("postgres:16-alpine") + + +@pytest.fixture(scope="module", autouse=True) +def setup(request): + postgres.start() + + def remove_container(): + postgres.stop() + + request.addfinalizer(remove_container) + os.environ["DB_CONN"] = postgres.get_connection_url() + os.environ["DB_HOST"] = postgres.get_container_host_ip() + os.environ["DB_PORT"] = str(postgres.get_exposed_port(5432)) + os.environ["DB_USERNAME"] = postgres.username + os.environ["DB_PASSWORD"] = postgres.password + os.environ["DB_NAME"] = postgres.dbname + customers.create_table() + + +@pytest.fixture(scope="function", autouse=True) +def setup_data(): + customers.delete_all_customers() +``` + +Here's what the fixtures do: + +- The `setup` fixture has `scope="module"`, so it runs once for all tests in + the file. It starts a PostgreSQL container, sets environment variables with + the connection details, and creates the `customers` table. A cleanup + function removes the container after all tests complete. +- The `setup_data` fixture has `scope="function"`, so it runs before every + test. It deletes all records to give each test a clean database. + +## Write the tests + +Add the test functions to the same file: + +```python +def test_get_all_customers(): + customers.create_customer("Siva", "siva@gmail.com") + customers.create_customer("James", "james@gmail.com") + customers_list = customers.get_all_customers() + assert len(customers_list) == 2 + + +def test_get_customer_by_email(): + customers.create_customer("John", "john@gmail.com") + customer = customers.get_customer_by_email("john@gmail.com") + assert customer.name == "John" + assert customer.email == "john@gmail.com" +``` + +- `test_get_all_customers()` inserts two customer records, fetches all + customers, and asserts the count. +- `test_get_customer_by_email()` inserts a customer, fetches it by email, and + asserts the details. + +Because `setup_data` deletes all records before each test, the tests can run in +any order. diff --git a/content/manuals/testcontainers.md b/content/manuals/testcontainers.md index 0ed99d9e98cc..9d991119eb88 100644 --- a/content/manuals/testcontainers.md +++ b/content/manuals/testcontainers.md @@ -2,29 +2,30 @@ title: Testcontainers weight: 40 description: Learn how to use Testcontainers to run containers programmatically in your preferred programming language. -keywords: docker APIs, docker, testcontainers documentation, testcontainers, testcontainers oss, testcontainers oss documentation, +keywords: + docker APIs, docker, testcontainers documentation, testcontainers, testcontainers oss, testcontainers oss documentation, docker compose, docker-compose, java, golang, go params: sidebar: group: Open source intro: -- title: What is Testcontainers? - description: Learn about what Testcontainers does and its key benefits - icon: feature_search - link: https://testcontainers.com/getting-started/#what-is-testcontainers -- title: The Testcontainers workflow - description: Understand the Testcontainers workflow - icon: explore - link: https://testcontainers.com/getting-started/#testcontainers-workflow + - title: What is Testcontainers? + description: Learn about what Testcontainers does and its key benefits + icon: feature_search + link: https://testcontainers.com/getting-started/#what-is-testcontainers + - title: The Testcontainers workflow + description: Understand the Testcontainers workflow + icon: explore + link: https://testcontainers.com/getting-started/#testcontainers-workflow quickstart: -- title: Testcontainers for Go - description: A Go package that makes it simple to create and clean up container-based dependencies for automated integration/smoke tests. - icon: /icons/go.svg - link: https://golang.testcontainers.org/quickstart/ -- title: Testcontainers for Java - description: A Java library that supports JUnit tests, providing lightweight, throwaway instances of anything that can run in a Docker container. - icon: /icons/java.svg - link: https://java.testcontainers.org/ + - title: Testcontainers for Go + description: A Go package that makes it simple to create and clean up container-based dependencies for automated integration/smoke tests. + icon: /icons/go.svg + link: https://golang.testcontainers.org/quickstart/ + - title: Testcontainers for Java + description: A Java library that supports JUnit tests, providing lightweight, throwaway instances of anything that can run in a Docker container. + icon: /icons/java.svg + link: https://java.testcontainers.org/ --- Testcontainers is a set of open source libraries that provides easy and lightweight APIs for bootstrapping local development and test dependencies with real services wrapped in Docker containers. @@ -32,6 +33,14 @@ Using Testcontainers, you can write tests that depend on the same services you u {{< grid items=intro >}} +## Guides + +Explore hands-on Testcontainers guides to learn how to use Testcontainers +with different languages and popular frameworks: + +- [Getting started with Testcontainers for Go](/guides/testcontainers-go-getting-started/) +- [Getting started with Testcontainers for Python](/guides/testcontainers-python-getting-started/) + ## Quickstart ### Supported languages @@ -54,6 +63,6 @@ However, these are not actively tested in the main development workflow, so not and additional manual configuration might be necessary. If you have further questions about configuration details for your setup or whether it supports running Testcontainers-based tests, - contact the Testcontainers team and other users from the Testcontainers community on [Slack](https://slack.testcontainers.org/). +contact the Testcontainers team and other users from the Testcontainers community on [Slack](https://slack.testcontainers.org/). - {{< grid items=quickstart >}} +{{< grid items=quickstart >}} diff --git a/data/tags.yaml b/data/tags.yaml index 79d766f0cadf..cb5bccf7d9d6 100644 --- a/data/tags.yaml +++ b/data/tags.yaml @@ -36,5 +36,7 @@ release-notes: title: Release notes secrets: title: Secrets +testing-with-docker: + title: Testing with Docker troubleshooting: title: Troubleshooting