Skip to main content

pytest-container-structure-test

PyPI - Version PyPI - Python Version pre-commit.ci status


Run container-structure-test configs from pytest, with every test in your YAML config reported as an individual pytest test — right alongside your regular Python tests.

$ pytest -v
structure.yaml::command:os-release PASSED                    [ 25%]
structure.yaml::command:gunicorn-installed PASSED            [ 50%]
structure.yaml::file-existence:app-dir PASSED                [ 75%]
test_app.py::test_healthcheck PASSED                         [100%]

Table of Contents

Installation

pip install pytest-container-structure-test

The plugin invokes the container-structure-test binary from your PATH. Any install route works:

  • upstream releases / brew (e.g. brew install container-structure-test)
  • pip install container-structure-test — the PyPI wheel that ships the binary as a console script in your environment
  • or point PYTEST_CONTAINER_STRUCTURE_TEST_BINARY at a specific binary

A running Docker daemon is required to execute the tests (not to collect them).

Usage

Declare your config files and the image each one targets in one place, using the container_structure_tests ini option (in pyproject.toml, pytest.ini, tox.ini, or setup.cfg). Each entry has the form <path/to/config.yaml>=<image>:

# pyproject.toml
[tool.pytest.ini_options]
container_structure_tests = [
  "tests/structure/web.yaml=myorg/web:${WEB_VERSION:-latest}",
  "tests/structure/db.yaml=${DB_IMAGE}",
  "tests/structure/cli.yaml=myorg/cli:latest",
]

Paths are relative to the pytest rootdir and must live under a directory pytest collects (typically tests/).

The image value supports environment-variable expansion, so the image name or tag can come from CI:

  • $VAR or ${VAR} — expands from the environment; referencing an unset variable is a collection error.
  • ${VAR:-default} — uses default when VAR is unset or empty, so runs work locally without exports.

Values are expanded when the config file is collected, so a conftest.py can compute them first — see Computing image values in Python.

Write your config files exactly as container-structure-test expects — nothing custom:

# tests/structure/web.yaml
schemaVersion: 2.0.0
commandTests:
  - name: gunicorn-installed
    command: gunicorn
    args: ["--version"]
fileExistenceTests:
  - name: app-dir
    path: /app
    shouldExist: true
metadataTest:
  exposedPorts: ["8000"]

Then just run pytest. Every entry in commandTests, fileExistenceTests, fileContentTests, and licenseTests — plus the metadataTest block — becomes its own pytest test with its own pass/fail, and failures include the errors, stdout, and stderr reported by the tool:

$ pytest -v tests/
tests/structure/web.yaml::command:gunicorn-installed PASSED
tests/structure/web.yaml::file-existence:app-dir FAILED
tests/structure/web.yaml::metadata PASSED
tests/test_app.py::test_healthcheck PASSED

=================================== FAILURES ===================================
______________________ structure/web.yaml::file-existence:app-dir _____________
File Existence Test: app-dir: FAIL
error: Expected file /app to exist but it does not

Test matrix: multiple images, configs, and platforms

When one image per config isn't enough — you want the same config against several images or architectures, or you need other container-structure-test test flags — declare suites in a plugin-owned table in pyproject.toml:

[[tool.pytest-container-structure-test.suites]]
configs   = ["tests/structure/web.yaml"]
image     = "myorg/web:${WEB_VERSION:-latest}"
platforms = ["linux/amd64", "linux/arm64"]
pull      = true

[[tool.pytest-container-structure-test.suites]]
configs    = ["tests/structure/base.yaml", "tests/structure/db.yaml"]
images     = ["${DB_IMAGE}", "myorg/db:edge"]
driver     = "docker"
extra_args = ["--save"]

Each suite expands to the cross product configs × images × platforms, and every combination is one container-structure-test invocation. When a config file runs in more than one combination, the differing dimensions show up as a suffix on each test's node ID:

tests/structure/web.yaml::command:gunicorn-installed[linux/amd64] PASSED
tests/structure/web.yaml::command:gunicorn-installed[linux/arm64] PASSED
tests/structure/db.yaml::command:psql-installed[myorg/db:edge] FAILED

Fields per suite:

Field Maps to Notes
config / configs --config one required; paths relative to rootdir
image / images --image one required; env-var expansion applies
platform / platforms --platform optional; omitted → host default
pull --pull boolean
driver --driver e.g. docker, tar, host
metadata --metadata path relative to rootdir; env-var expansion applies
extra_args passed verbatim any other flag, e.g. ["--save", "--runtime", "runsc"]

Unknown keys are rejected with a clear error (typo protection). extra_args may not include the flags the plugin itself manages (--config, --image, --platform, --output, --test-report, --no-color, --quiet) — overriding those would break result mapping.

[!NOTE] With the classic Docker image store, a tag holds one platform at a time — pulling linux/amd64 replaces a local linux/arm64 image under the same tag. When testing multiple platforms, set pull = true so each run fetches its own variant, and enable Docker's containerd image store if you want multi-platform tags cached side by side.

The simple container_structure_tests ini option keeps working and can be combined with suites; each of its entries is just a suite of one config, one image, and default flags.

Pipeline overrides

The config declares the full intended matrix; command-line flags adjust it per invocation, so the same config works locally and in CI:

# arch-limited pipeline runner with a fresh image cache:
pytest --cst-platform=linux/amd64 --cst-pull

# local run right after `docker build` — don't let a registry pull clobber the local tag:
pytest --cst-no-pull
  • --cst-platform PLATFORM (repeatable) overrides the platform of every configured run, collapsing any declared platform matrix to the given value(s); runs that declared no platform get it injected.
  • --cst-pull / --cst-no-pull force pulling on or off for every run (mutually exclusive; default is whatever each suite configured).

Flags apply to all configured runs. A repo can bake defaults with addopts in [tool.pytest.ini_options]. To see the exact binary invocations for debugging, run with --log-cli-level=DEBUG.

Computing image values in Python

Sometimes the image can't be written as a literal or a plain ${VAR} — the digest lives in a build-output file, the tag is a registry env var joined to a VERSION file, or the value has to be pulled out of some JSON. Compute it in a conftest.py, export it to the environment, and reference it like any other variable:

# conftest.py  (at the pytest rootdir)
import json
import os
from pathlib import Path

_root = Path(__file__).parent
_digest = json.loads(_root.joinpath("build-meta.json").read_text())["digest"]
os.environ["APP_IMAGE"] = f"{os.environ['CI_REGISTRY'].rstrip('/')}/app@{_digest}"
# pyproject.toml
[[tool.pytest-container-structure-test.suites]]
config    = "tests/structure/app.yaml"
image     = "${APP_IMAGE}"
platforms = ["linux/amd64", "linux/arm64"]

The same works with the ini option: container_structure_tests = ["tests/structure/app.yaml=${APP_IMAGE}"].

This works because the plugin resolves image strings while it collects each config file — after every conftest has been imported and after all pytest_configure hooks have run. Module-level code and a pytest_configure in the same conftest are both fine.

[!IMPORTANT] The conftest.py must be one pytest loads before collection starts: the rootdir conftest.py, one on the path to the arguments you pass on the command line, or one inside a test* directory next to them. A conftest.py buried deeper in the tree is imported lazily during collection, which may be too late. --noconftest skips them entirely.

Limits worth knowing:

  • Every value round-trips through the environment as a string.
  • The shape of the matrix stays static in TOML. Each image can be dynamic, but you can't generate a variable-length list of them — for N images, declare N placeholders: images = ["${IMG_A}", "${IMG_B}"].
  • Referencing an unset variable is a collection error; use ${VAR:-default} for a fallback.
  • Config paths are never expanded — only image, metadata, and extra_args.

How it works

  • Collection only parses the YAML — pytest --collect-only never touches Docker.
  • Environment variables in image, metadata, and extra_args are expanded when the config file is collected — after conftest files load — so a conftest.py can set them.
  • At run time, the binary is invoked once per config × image × platform combination and each collected test looks up its own result from that run's JSON report, so N tests in one config cost one image run per combination.
  • If the binary itself fails (Docker daemon down, image missing), every test in that config fails with the captured stderr.
  • The binary is resolved from PATH; set PYTEST_CONTAINER_STRUCTURE_TEST_BINARY to use a specific container-structure-test binary instead.

License

pytest-container-structure-test is distributed under the terms of the MIT license.

Metadata

Release files for pytest-container-structure-test 0.0.3

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pytest-container-structure-test 0.0.3
File Size Uploaded Provenance
pytest_container_structure_test-0.0.3.tar.gz 26.0 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for pytest-container-structure-test 0.0.3
File Interpreter ABI Platform
pytest_container_structure_test-0.0.3-py3-none-any.whl Python 3 none any Details

Total release size: 41.5 kB

Release files / pytest_container_structure_test-0.0.3.tar.gz

Download URL pytest_container_structure_test-0.0.3.tar.gz
Size 26.0 kB
Tags Source
SHA-256 checksum
How to use checksums
167b6b948fc4538b487d2c88ef4c1892d953c26c6e1336289f2474d50576c65e
BLAKE2b-256 checksum
How to use checksums
451c14cda33e58caa1f985148022cb76cda7b18600602454104e867aea74cc6a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 27, 2026.

Transparency log

Release files / pytest_container_structure_test-0.0.3-py3-none-any.whl

Download URL pytest_container_structure_test-0.0.3-py3-none-any.whl
Size 15.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
3a8b095e9faa1190c62fd75ca09c2119f2efd834c8cffd1da3701995aadffaf1
BLAKE2b-256 checksum
How to use checksums
9e24256a1aba465b88fa7e914f8fa0b63ff8db3bb7dd8250673a1e5e97c5cac7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 27, 2026.

Transparency log

Security for pytest-container-structure-test 0.0.3

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for pytest_container_structure_test-0.0.3.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 27, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page