Test-Driven Vibing is a lightweight iterative workflow where you write the smallest possible test that encodes the desired behavior—or "vibe"—of a component before implementing it. It differs from full-scale Test-Driven Development in that the tests are deliberately minimal, often focused on a single happy path or a specific edge condition, and the goal is rapid feedback rather than exhaustive specification. It matters when you're prototyping an API, spinning up a new service, or trying to tame a complex configuration without getting bogged down in boilerplate test infrastructure. The approach treats tests as a fast feedback loop, not a deliverable artifact.
Write the minimal failing test
The first step is to create a test file that captures the intent of the component you're about to build. Keep it tiny: one assertion, one function call, no fixtures unless absolutely necessary. The test should fail immediately with a clear message, confirming that the component doesn't exist yet.
Create test_welcome.py:
def test_welcome_message():
from welcome import greet
assert greet() == "Welcome aboard"
Run it with pytest:
$ pytest -xvs test_welcome.py
Typical output when the module is absent:
================== test session starts ==================
platform linux -- Python 3.11.6, pytest-8.3.4
cachedir: .cache
rootdir: /home/user/project
import test_welcome
import welcome
E import failure
E ...
E ImportError: No module named 'welcome'
If the module exists but returns the wrong value, the error looks like:
E assert 'Welcome aboard' == 'Welcome, traveler!'
E + where 'Welcome aboard' = greet()
E - expected: 'Welcome, traveler!'
At this point the test serves its purpose: it documentates the expected "vibe" and fails conspicuously until implementation catches up.
Iterate: implement just enough to make it pass
Write the minimal implementation that makes the assertion pass. The goal is not production-quality code but a passing test that proves the workflow works.
Create welcome.py:
# welcome.py
def greet():
return "Welcome aboard"
Re-run the test:
$ pytest -xvs test_welcome.py
Output:
================== test session starts ==================
platform linux -- Python 3.11.6, pytest-8.3.4
cachedir: .cache
rootdir: /home/user/project
import test_welcome
import welcome
PASSED test_welcome.py::test_welcome_message
1 passed. The loop is now established: write a failing test, implement the bare minimum, verify it passes. Because the test is small, the implementation step is usually under a minute.
Loop for tight feedback
When the shape of the component is still uncertain, running the test once per edit becomes friction. A shell loop can bridge the gap. The following uses only standard shell tools—no custom daemons or polling daemons required:
$ while true; do pytest -x 2>&1 | tail -1; sleep 0.7; done
Each iteration runs pytest, prints the last line of output (pass/fail summary), then waits three-quarters of a second before repeating. When the test flips from FAILED to PASSED, the loop still runs but the visual cue is immediate. If you have entr(1) available, the same pattern becomes:
$ while true; do echo test_welcome.py | entr pytest -x; done
entr watches the file and re-invokes pytest automatically on save, eliminating the sleep altogether.
Assert HTTP behavior with curl
Not all vibing happens in-process. When the component under test is a service endpoint, the test can be a one-liner curl assertion embedded in a shell script or a tiny pytest wrapper. Suppose you have a local dev server running on port 8080:
Create test_endpoint.py:
# test_endpoint.py
import subprocess
def test_status():
result = subprocess.run(
["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "http://localhost:8080/status"],
capture_output=True, text=True,
)
code = result.stdout.strip()
assert code == "200", f"Expected 200, got {code}"
Run it:
$ pytest -xvs test_endpoint.py
Output on a healthy server:
PASSED test_endpoint.py::test_status
Output when the server returns 500:
E assert '500' == '200'
E + where '500' = result.stdout.strip()
If the server isn’t up, curl itself exits non-zero and pytest reports the subprocess error:
E subprocess.CalledProcessError: Command "curl -s -o /dev/null -w %...>" returned non-zero exit status 7.
The sharp edge here is that curl’s exit code 7 means “failure in name resolution or connection,” which pytest surfaces as a generic subprocess error, masking the fact that the service isn’t listening. A complementary check—curl -f (fail on HTTP errors)—can distinguish a crashed server from a 404 or 500 response.
Integrate with CI
Because the tests are tiny and run in milliseconds, they fit naturally into a CI pipeline. A minimal GitHub Actions workflow looks like this:
name: vibe-check
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
- run: pip install pytest
- run: pytest -x
Every push triggers the suite. If a test that previously passed now fails, the workflow turns red and the commit is blocked from merging (depending on branch protection rules). Because the tests are deliberately narrow, the CI feedback is fast—often under five seconds—making it practical to run on every commit without slowing down developer velocity.
Sharp edges: what silently fails
- Test ordering matters. pytest runs files in lexical order and executes functions in definition order within a file. A test that creates a database table in one file and another test that drops it in a different file can leave behind silent state, causing the second test to pass on a fresh clone but fail in CI.
- Import side effects. If
welcome.pyexecutes code at module level (e.g.,connect_db()), importing it in the test file triggers that side effect even before the test runs. This can make a test appear to pass when it’s really just benefiting from leftover state. - Too-loose assertions. Writing
assert resultinstead ofassert result == expectedgives a false sense of correctness. The test passes for any truthy value, including error objects, empty strings, or partial data. Tighten assertions to exact expected values or structured checks. curloutput parsing. The-wformat string is fragile acrosscurlversions. A flag that works on version 8.3 may be ignored or produce different output on version 7.8. Always quote the format string and test the raw output once before embedding it in a test.- Fixture leakage. Even a session-scoped fixture that starts a temporary HTTP server can keep the port bound after the test suite ends, causing subsequent runs to fail with “Address already in use.” Explicitly shut down servers in a
teardownhook or use a tool likepytest-freezegunordocker-compose up -dwith per-job containers.
When to stop vibing and start testing
The workflow is intentionally ephemeral. Use Test-Driven Vibing while the component’s shape is still uncertain, while dependencies are being resolved, or while prototyping a new endpoint. Transition to a more structured testing strategy—full coverage goals, integration test suites, property‑based tests—once the component’s contract stabilizes and the cost of maintaining the minimal tests outweighs the speed benefit. The “vibe” test is a catalyst, not a permanent fixture.
This page was rewritten on 11 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
Join the Newsletter
Get the latest updates and deep insights shipped directly to your inbox.