Tutorial: Writing Acceptance Tests That Guide Delivery
Time: ~15 minutes (6 steps) Platform: macOS, Linux, or Windows Prerequisites: Python 3.10+, Claude Code with nWave installed, Tutorial 1 completed Dependencies: None beyond pytest. Pure Python.
Setup
Run from a directory where you want the tutorial project created (e.g. ~/projects):
curl -fsSL https://raw.githubusercontent.com/nWave-ai/nWave/main/docs/guides/tutorial-writing-tests/setup.py | python3
Prefer to read first? See manual-setup.md.
After setup you should have
- A
md-converter/directory withsrc/md_converter/__init__.py,tests/__init__.py,pyproject.toml, and.gitignore - A
.venv/virtual environment withpytestinstalled - A clean git repository with one initial commit
What You'll Build
A Markdown-to-HTML converter — built entirely from tests you write yourself.
Before — raw Markdown:
# Hello World
This is **bold** and this is *italic*.
- Item one
- Item two
After — your markdown_to_html() function produces:
<h1>Hello World</h1>
<p>This is <strong>bold</strong> and this is <em>italic</em>.</p>
<ul>
<li>Item one</li>
<li>Item two</li>
</ul>
In Tutorial 1, the tests were written for you. This time, you write them. This is the core skill: define "done" precisely, then let nWave build it. Good tests are the difference between code that works and code that merely compiles.
Step 1 of 6: Open the project (~1 minute)
After running setup, cd md-converter and activate the virtualenv:
cd md-converter
source .venv/bin/activate
Windows users: Replace
source .venv/bin/activatewith.venv\Scripts\activate.
This time there's no starter repo — you'll write the tests from scratch in the next step.
Next: you'll write your first acceptance test — the simplest possible behavior.
Step 2 of 6: Write Your First Test — The Simplest Behavior (~2 minutes)
Here's the key principle: start with the simplest test, then add complexity. This is the density ramp — the same idea as learning to walk before you run.
Your first test should define the most basic thing the converter does: turn a heading into HTML.
Create tests/test_markdown.py:
# tests/test_markdown.py
from md_converter import markdown_to_html
def test_converts_heading_to_html():
"""A line starting with # becomes an <h1> tag."""
result = markdown_to_html("# Hello World")
assert result.strip() == "<h1>Hello World</h1>"
Notice three things about this test:
- The name reads as a specification:
test_converts_heading_to_html— anyone can tell what the feature does - It tests behavior, not implementation: we don't say "use regex" or "split the string" — just what goes in and what comes out
- It's tiny: one input, one output, one assertion
Run it to confirm it fails:
pytest tests/ -v
FAILED tests/test_markdown.py::test_converts_heading_to_html - ImportError
1 failed
Red. Good. That failing test is your specification — it defines one behavior you want.
If you see
ModuleNotFoundErrorinstead ofImportError: Both mean the same thing — the function doesn't exist yet. Your test is working correctly.
Next: you'll add tests for bold/italic — defining how inline formatting works.
Step 3 of 6: Add Inline Formatting Tests (~3 minutes)
Now add a second behavior: bold and italic text. This is slightly more complex because it handles inline formatting rather than whole lines.
Add this test to tests/test_markdown.py (below the existing test):
def test_converts_bold_and_italic_text():
"""Double asterisks become <strong>, single become <em>."""
result = markdown_to_html("This is **bold** and *italic* text.")
assert "<strong>bold</strong>" in result
assert "<em>italic</em>" in result
Why in instead of ==? Because we're testing the bold/italic behavior specifically. We don't care whether the surrounding text is wrapped in <p> tags yet — that would test two things at once. Keep tests focused on one behavior.
Now add an edge case. Edge cases define boundaries — they're where bugs hide:
def test_plain_text_returns_as_paragraph():
"""Text with no Markdown syntax wraps in a <p> tag."""
result = markdown_to_html("Just plain text.")
assert result.strip() == "<p>Just plain text.</p>"
This test answers: "What happens when there's no Markdown?" That boundary matters — it prevents the converter from mangling regular text.
Run the tests:
pytest tests/ -v
FAILED tests/test_markdown.py::test_converts_heading_to_html
FAILED tests/test_markdown.py::test_converts_bold_and_italic_text
FAILED tests/test_markdown.py::test_plain_text_returns_as_paragraph
3 failed
Three red tests, three behaviors specified. Each test name reads like a requirement:
- Converts headings to HTML
- Converts bold and italic text
- Wraps plain text in paragraphs
Next: you'll add one more test for lists, completing your specification.
Step 4 of 6: Add a List Conversion Test (~2 minutes)
The final behavior: unordered lists. Lines starting with - become list items.
Add this to tests/test_markdown.py:
def test_converts_unordered_list():
"""Lines starting with '- ' become <ul> with <li> items."""
markdown = "- First\n- Second\n- Third"
result = markdown_to_html(markdown)
assert "<ul>" in result
assert "<li>First</li>" in result
assert "<li>Second</li>" in result
assert "<li>Third</li>" in result
assert "</ul>" in result
Run all four tests:
pytest tests/ -v
FAILED tests/test_markdown.py::test_converts_heading_to_html
FAILED tests/test_markdown.py::test_converts_bold_and_italic_text
FAILED tests/test_markdown.py::test_plain_text_returns_as_paragraph
FAILED tests/test_markdown.py::test_converts_unordered_list
4 failed
Four red tests. Let's look at what you've built — your complete test file should look like this:
# tests/test_markdown.py
from md_converter import markdown_to_html
def test_converts_heading_to_html():
"""A line starting with # becomes an <h1> tag."""
result = markdown_to_html("# Hello World")
assert result.strip() == "<h1>Hello World</h1>"
def test_converts_bold_and_italic_text():
"""Double asterisks become <strong>, single become <em>."""
result = markdown_to_html("This is **bold** and *italic* text.")
assert "<strong>bold</strong>" in result
assert "<em>italic</em>" in result
def test_plain_text_returns_as_paragraph():
"""Text with no Markdown syntax wraps in a <p> tag."""
result = markdown_to_html("Just plain text.")
assert result.strip() == "<p>Just plain text.</p>"
def test_converts_unordered_list():
"""Lines starting with '- ' become <ul> with <li> items."""
markdown = "- First\n- Second\n- Third"
result = markdown_to_html(markdown)
assert "<ul>" in result
assert "<li>First</li>" in result
assert "<li>Second</li>" in result
assert "<li>Third</li>" in result
assert "</ul>" in result
Read these test names aloud. They're a specification:
- Converts heading to HTML
- Converts bold and italic text
- Wraps plain text as paragraphs
- Converts unordered lists
That's what "done" looks like. You didn't describe how to parse Markdown. You described what correct output looks like for each case.
Commit your tests before handing off to nWave:
git add -A && git commit -m "test: acceptance tests for markdown-to-html converter"
Next: you'll hand these tests to nWave and watch it build the implementation.
Step 5 of 6: Let nWave Deliver (~5 minutes)
AI output varies between runs. Your code will differ from the examples below. That is normal. We define success by what the code does (tests pass), not what the agent says.
Open Claude Code and start the delivery:
claude
Then type this Claude Code command (not a terminal command):
/nw-deliver "Markdown-to-HTML converter"
This takes 3-5 minutes. You'll see phases scroll by:
● nw-solution-architect(Fill roadmap skeleton) ← Planning (~30s)
● nw-software-crafter(Execute step 01-01) ← Writing code (~2-3 min)
● nw-software-crafter-reviewer(Adversarial review) ← Quality check (~1 min)
The agent reads your four tests, plans an implementation, and builds code that makes each test pass — one at a time, following the TDD cycle.
Messages you can safely ignore
Lines like these are normal internal coordination, not errors:
PreToolUse:Task hook error— DES validation checkpointDES_MARKERS_MISSING— Internal formatting check, auto-retries
Verify success
When delivery completes, run:
pytest tests/ -v
Expected:
tests/test_markdown.py::test_converts_heading_to_html PASSED
tests/test_markdown.py::test_converts_bold_and_italic_text PASSED
tests/test_markdown.py::test_plain_text_returns_as_paragraph PASSED
tests/test_markdown.py::test_converts_unordered_list PASSED
4 passed
All green. Try the converter:
PYTHONPATH=src python3 -c "
from md_converter import markdown_to_html
print(markdown_to_html('# Hello\n\nThis is **bold** and *italic*.\n\n- One\n- Two'))
"
You should see something like:
<h1>Hello</h1>
<p>This is <strong>bold</strong> and <em>italic</em>.</p>
<ul>
<li>One</li>
<li>Two</li>
</ul>
Your exact output may differ slightly — what matters is that all four Markdown features are converted.
If tests fail after delivery: Run
/nw-deliveragain — it resumes from where it left off and fixes remaining failures.
If you see no output for 2+ minutes: Check the Claude Code status bar at the bottom. A pulsing indicator means it's still working.
Next: a recap of the test-writing principles you just used.
Step 6 of 6: What Just Happened (~1 minute)
You started with an empty project and wrote four acceptance tests. nWave turned them into working code. But the real skill you practiced was writing tests that guide delivery.
The Five Principles You Applied
Test the WHAT, not the HOW — Your tests said "heading becomes
<h1>", never "use regex to parse". The agent chose its own implementation.Descriptive names are specifications —
test_converts_heading_to_htmlreads like a requirement. Anyone (human or AI) can understand what "done" means.Start simple, add complexity — Heading first, then inline formatting, then lists. Each test built on the previous concept. This is the density ramp.
Edge cases define boundaries —
test_plain_text_returns_as_paragraphasks "what happens with no Markdown?" That boundary prevents bugs.Keep tests independent — Each test runs alone. No test depends on another test's state. This lets nWave tackle them one at a time.
What You Built
- A complete project from scratch (no starter repo)
- 4 acceptance tests that serve as a living specification
- A working Markdown-to-HTML converter you never had to implement
- Atomic git commits tracing every TDD step
What You Didn't Have to Do
- Write the implementation
- Figure out how to parse Markdown syntax
- Decide on architecture (functions, classes, modules)
- Write additional unit tests (nWave wrote them from your acceptance tests)
- Review the code (the reviewer agent did it)
Next Steps
- Tutorial 3: Understanding the Delivery Pipeline — See exactly what happens inside
/nw-deliverand learn to read the output like a pro - Tutorial 7: Generating Acceptance Tests — Don't want to write tests by hand?
/nw-distillgenerates them from user stories - Try it on your own project — Pick a small feature, write 3-4 tests that define "done", and run
/nw-deliver
Troubleshooting
| Symptom | Fix |
|---|---|
ImportError when running tests before delivery |
Expected — the function doesn't exist yet. That's the point of red tests. |
ModuleNotFoundError after delivery |
Make sure your venv is active and pyproject.toml has pythonpath = ["src"] |
| Tests still failing after delivery | Run /nw-deliver again — it resumes and fixes remaining failures |
| Agent creates files in wrong location | Check that src/md_converter/__init__.py exists before delivery |
| Want to start over | git stash && git checkout main then recreate the test file |
| Delivery takes more than 10 minutes | Normal for slower machines. Check the Claude Code status bar for activity. |
Last Updated: 2026-02-17