Skip to content

Testing philosophy

Testing philosophy

The BDD suite (service/features/) makes no attempt to test this service in isolation from what it actually does: clone real git repos over git/http/https/ssh, push real images through a real BuildKit daemon, read real Kubernetes Secrets/ConfigMaps at startup. Every scenario runs against a real, disposable Kubernetes namespace and real fixture services — there is no mocked git server, no stubbed BuildKit, no faked HTTP client. See 0005: The BDD suite runs on Thomas for the decision record, and Running the tests for how to actually run it.

Why real commands, not mocks

A mock only proves the code calls the mock the way the test expects — it can drift silently from what the real dependency actually does (a real git clone's exact error text on a bad branch, whether BuildKit actually respects an insecure-registry trust config, whether an SSH host key policy really fails closed with no pin configured). Every scenario in this suite is written so a failure means the real behavior changed, not that a mock's assumptions did. Concretely: SSH credential scenarios run against a real, disposable git-daemon/git http-backend/sshd fixture (charts/test-git-server), registry scenarios push to a real, disposable registry (charts/test-registry), and every build scenario runs a real docker buildx build --push against a real BuildKit daemon.

Built on Thomas

The suite is written in Thomas, a separate, general-purpose real-command BDD framework this project's own suite was migrated onto. Thomas owns the actual step-definition vocabulary (Helm, Kubernetes, HTTP, Docker/buildx, SSH, TLS) and the "define, then act" pattern every scenario follows — a pure Given <Type> known as "<Alias>": step constructs a typed object, a separate When step performs the real action. That vocabulary is documented on Thomas's own site, not duplicated here — see its BDD conventions and per-domain reference pages.

What each .feature file actually covers

File What it proves
health.feature Liveness/readiness signals, including a deliberately-never-ready pod
request_validation.feature /build's up-front shape check, independent of whether the values are usable
devcontainer_config_discovery.feature The devcontainer CLI's real auto-discovery locations (root, .devcontainer/, not an arbitrary sub-folder)
devcontainer_config_content.feature A discoverable-but-unusable devcontainer.json fails clearly, including the "build":{"dockerfile":...} path
image_resolution.feature Name/tag/registry defaulting and mapping-rule resolution
registry_auth.feature Ambient vs. per-request registry push credentials against a real auth-enforcing registry
git_source_resolution.feature URL parsing, credential precedence, and HTTPS/SSH protocol conversion
service_settings_file.feature The unified SERVICE_CONFIG_PATH JSON/YAML settings file
service_startup_configuration.feature Every other startup config source, including real crash-loop detection for misconfiguration
devcontainer_metadata.feature GET /devcontainer on real built images: Feature + devcontainer.json merging, rendered lifecycle scripts, VS Code customizations, authed registries, and unusable images
end_to_end_build.feature A handful of full round trips combining several resolution axes at once (@smoke)

Fixture cost is real and accepted

Every scenario that needs a deployed devcontainer-builder pod builds and pushes this repository's own current source first (via the shared, already-running production BuildKit instance and real GHCR credentials — never a disposable in-cluster registry for this image, since it's the one thing the deployed pod's own kubelet needs to genuinely pull, and kubelet pulls can't be routed through BuildKit's own, separately-configurable registry trust). A scenario whose server configuration needs to vary (most of them) installs its own Helm release rather than reusing one across scenarios — real, measured cost (minutes per file, not seconds), accepted deliberately in exchange for every scenario being a genuinely independent, real round trip rather than one sharing hidden state with whichever scenario happened to run before it.