Docs rot. I've watched it happen on every team I've worked with. You write a stellar guide in January, and by March the CLI flags have changed, a dependency dropped a feature, and the example that once ran perfectly now errors out with a stack trace nobody wants to debug. The worst part? Nobody notices until release day, when a customer hits that broken command and your support inbox lights up. This is where toolchain smoke tests come in. Not the ones that check your code compiles—those are old news. I mean tests that check your documentation still works: the commands run, the paths exist, the API examples match the schema. It's a niche practice, but it can save you from shipping lies to your users.
The Silence Before the Scream: Where Docs Decay
A day in the life of stale docs
Friday, 4:47 PM. The release is scheduled for Monday morning. A new hire—three weeks in—opens the onboarding guide to find the API endpoint changed eight commits ago. The old URL returns a 404. The screenshot shows a login screen that no longer exists. She emails the team lead, who forwards it to the tech writer, who pings the engineer who wrote the code and then left for a two-week vacation. That hurt. The documentation said one thing. The code said another. And nobody found out until the exact moment when finding out costs the most.
This is not a failure of effort or skill. It's a failure of timing. Docs decay quietly because nothing in the build pipeline screams when words drift from reality. Tests pass. Linters pass. The deploy succeeds. The manual—that sprawling, well-intentioned manual—silently rots in a corner of the repo.
The release-day scramble
We have all been on that call. Someone reads an old config example, the integration fails in staging, and the team spends ninety minutes reverse-engineering what the documentation should have said. The root cause is never "lazy writers" or "careless devs." It's the absence of a cheap, automated check that compares the living code against the living prose. The fix is not more discipline. Discipline breaks under deadline pressure. The fix is a smoke test—a dumb, fast, mechanical check that catches the obvious before the obvious humiliates you in front of customers.
The tricky bit is naming the problem correctly. Most teams call this a "docs issue" and assign it to whoever has time. That's the wrong frame. It's a system issue, and it deserves a systems answer.
Why your tests aren't catching it
Unit tests verify the code does what the code does. They never verify the code does what the docs say it does. Linters check formatting, not truth. A broken link checker finds dead URLs but not outdated parameters, changed return types, or examples that were deleted in the last refactor. These tools are useful, but they operate in separate universes. What actually keeps docs honest is a smoke test that treats documentation as a specification—parse the docs, extract the claims, and compare them against the real running system or its test suite.
That sounds fine until you run it the first time. Then you discover seventeen broken examples, three deprecated flags, and a table of contents that references a chapter nobody wrote. The silence breaks. The scream comes. And it's loud, but it's better to scream on a Tuesday morning than on release day.
The catch is that nobody wants to build this on day one. It feels like overhead. It feels like a tax on a project that already has too many moving parts. So the docs decay quietly, and the team pays the tax later—in debugging sessions, in support tickets, in a new hire's lost afternoon. The investment is small. The payoff is specific: you stop learning about doc rot from an angry customer and start learning about it from a terminal message.
Docs that ship stale don't fail loudly. They fail quietly, at the worst possible moment, and the cost lands on people who didn't write them.
— observation from a platform engineer, after a third incident blamed on outdated setup instructions
Quick reality check: a smoke test won't catch every lie in your documentation. It will catch the mechanical ones—the mismatched examples, the broken commands, the config keys that no longer exist. That's enough. That's where the worst pain lives. The rest is judgment, and judgment belongs to humans. Give the machine the repetitive checks. Save the humans for the hard calls.
Smoke Tests vs. Unit Tests vs. Linters: The Usual Confusion
What a smoke test actually checks
You don't need the full build. You don't need every page rendered. A smoke test for docs is a fast, shallow pass that answers one question: did this change break something obvious? Think of it as the docs equivalent of booting a server—if the machine catches fire in the first ten seconds, you don't care about the styling. We run ours on every pull request that touches the docs/ folder, and it takes under a minute. The check looks for dead internal links, missing images, broken anchors, and pages that fail to render at all. That's it. Shallow, deliberate, and boring—which is exactly the point.
I have seen teams build elaborate validation suites and call them smoke tests. They spin up headless browsers, crawl every page, compare screenshots pixel by pixel. That's not a smoke test anymore; that's a full regression harness wearing a smaller hat. The distinction matters because the cost of running a heavy check means people stop running it locally, and then they stop trusting it, and then the check gets skipped in CI. A real smoke test is the thing you run when you're slightly hungover and still want to ship something safe.
Where linters and unit tests fall short
Linters catch style drift—spacing, heading levels, broken Markdown fences, typos in metadata. Useful, but they can't tell you that a link points to a page that was deleted last Tuesday. Unit tests, by contrast, validate logic in code, not prose. They will never notice that your quickstart references a flag that the CLI removed in v2.4. The gap between those two tools is precisely where docs decay lives.
That sounds fine until you inherit a repo where the linter passes with zero warnings and the docs are still garbage. Wrong order. Orphaned pages. A tutorial that tells users to run a command that doesn't exist. The linter is happy—it only cares about formatting. The unit tests are happy—they never looked at the docs. The smoke test is the only one standing between you and a support ticket storm. The catch is that smoke tests don't verify correctness, only existence. They confirm a link resolves, not that the content on the other side is accurate. That's a trade-off worth naming, because teams that expect smoke tests to validate prose quality will be disappointed.
Smoke tests don't tell you if the docs are good. They tell you if the docs are broken enough to notice.
— from a conversation about why we stopped pretending otherwise
Field note: technical plans crack at handoff.
The overlap with CI and docs-as-code
Docs-as-code workflows make smoke tests natural, almost inevitable. Once your docs live in a repo with a CI pipeline, the overhead of adding a smoke-test job is trivial—a few lines of YAML, a script that crawls the built output, a threshold for failures. The trick is keeping the smoke test fast and deterministic, so it doesn't become the flaky check everyone ignores. Pin your dependencies. Cache the build. Fail on the first broken link, not the 47th.
The real question is where you draw the line between smoke and sanity. I have a rule of thumb: if a check takes longer than two minutes, it's not a smoke test. If it requires a database or a live API, it's a test fixture problem. If it catches a bug every week, you have a deeper docs decay issue that the smoke test is merely surfacing—fix the source, don't just patch the test. That said, don't skip the smoke test just because it feels insufficient. Incomplete coverage beats no coverage when your docs are rotting quietly and nobody has looked at them for six months.
Patterns That Keep Docs Honest
Embedding Commands in Code Blocks
The simplest trick is also the one most teams skip: put the actual command in the code block, not a paraphrased version of it. I have seen documentation that says npm run build -- --prod when the real command is npm run build:prod. That difference costs someone forty minutes of confusion. The fix is brutally direct—write the command exactly as a shell would consume it, then test that exact string. No variables, no ellipses, no "you probably get the idea." The smoke test becomes a grep against the source files, checking that every <pre> block containing a shell prompt matches a pattern from your Makefile or package.json. Wrong order? The test fails. Missing flag? The test fails. It's dumb, mechanical, and astonishingly effective because docs decay at the edges—the flags, the paths, the env vars—not in the grand narrative.
Snapshot Testing for Outputs
Snapshots work because they turn "does this still look right?" into a binary check. You run a command, capture the output, and commit that output as a fixture. Next CI run, you execute again and diff. The output drifted? You see exactly which line moved. This catches the sneaky stuff: a library bump that reorders JSON keys, a CLI that starts emitting warnings to stderr, a default value that silently changes from UTC to local. That last one is the killer. Nobody reads release notes for minor versions. A snapshot test will scream at you before your users do. Keep the snapshots small—don't snapshot your entire build log. Trim to the five lines that matter, or the diff noise will train your team to ignore the failure.
Automated Example Validation
Every example in your docs is a promise. Automated validation keeps that promise by executing the example code against a live system. The pattern is straightforward: extract all fenced code blocks tagged with language-bash or language-python, wrap each in a try/catch harness, and run them in CI. The catch—and there is always a catch—is side effects. Examples that write files, spin up servers, or hit external APIs will bite you. Scope the validation to examples that are idempotent or can run against a scratch container. We fixed this by tagging example blocks with custom info strings: <!-- smoke:isolated --> for runnable-only blocks, <!-- smoke:skip --> for ones that need manual setup. That annotation layer gives you control without turning every doc into a living nightmare.
Treating Docs Like Code in CI
Docs are not a special snowflake. Put them in the same repo, same review process, same CI pipeline. That sounds banal until you realize most teams treat docs as an export artifact—a Markdown dump from a wiki or a Confluence page that got copied into a folder somewhere. Wrong move. When docs live in the repo, a pull request that changes a CLI flag breaks the doc test in the same run, right next to the unit test that broke. The developer sees both failures simultaneously. That proximity is the whole game. They can't merge a code change that invalidates a documented example because the smoke test is in the merge queue. One caveat: keep the smoke suite fast. If your doc tests take seven minutes, developers will skip them or, worse, disable them. Target under two minutes for the full doc suite. Anything longer and you have created a new source of decay, not a cure.
Anti-Patterns That Make Teams Revert
Testing Only the Happy Path
The first mistake is almost always the same: someone writes a smoke test that checks whether the docs build, the links resolve, and the example snippet compiles. All green. Then the real world shows up—a config value that changed three weeks ago, a screenshot that now shows the wrong dialog, a code block that was truncated mid-function because the markdown broke. That stuff never shows up in a happy-path test because the test wasn’t looking for it. You need to test the worst path, the path where a junior engineer, three time zones away, pastes a snippet from the docs and gets a cryptic error.
The fix is to add negative assertions. Check that the docs don’t contain placeholder text like TODO or lorem ipsum. Check that every image file referenced on disk actually exists in the repo. Check that version strings in your docs match the latest release tag—not the one you remember, the one in CI. That sounds simple until you realize your team has twelve docs pages that hardcode the version number. I have seen a smoke test fail because someone wrote v2.1 while the release was v2.1.1. One character. A whole afternoon of confusion.
Over-Engineering the Harness
The opposite failure is just as common. Teams decide they need a full test framework—Jest, pytest, a custom Node script with eleven dependencies—just to verify that a README doesn’t lie. Six weeks later, the harness itself is the bottleneck. It needs maintenance, it fails on local machines because of platform-specific paths, and nobody on the docs team knows how to debug a failing beforeAll hook. The smoke test becomes a second job nobody asked for.
The catch is: a smoke test should be boring. A single shell script, a Make target, a CI step that runs python -m pytest docs/ and checks for exit code zero—that’s enough. If you need to install a parser to read your own markdown, you’ve already lost. I once saw a team build a Docker container just to test their docs. It worked, technically. It also took nine minutes to run, so everyone stopped running it locally and started committing broken docs anyway.
Ignoring Test Failures
Here’s the sneaky one: the test fails, but nobody fixes it. The broken link was reported, the PR merged anyway, and the failure became background noise. That’s worse than not having a test at all—now you have a false sense of safety plus a dog that doesn’t bark. The failure needs to block the merge. Not a warning, not a log line, an actual hard stop.
Teams revert the whole smoke test setup when this happens because the test feels like dead weight. It’s not. It’s a symptom of a workflow where docs are an afterthought, not a deliverable. If your CI allows a known-broken docs check to pass, you’re training the team to distrust automation. The fix is ruthless: make the test gate the merge, then give one person the explicit job of triaging failures within the hour. No one wants to be that person, so they’ll fix it fast.
Writing Tests That Are Too Brittle
Then there’s the over-correction. The test checks that the word “authenticate” appears exactly once in the intro, that a specific heading is an <h3> not an <h4>, that the command output matches a log line that changes on every patch release. The result? Every docs edit breaks the test, so nobody edits the docs.
Your smoke test should be a tripwire, not a caliper. It catches structural rot, not stylistic preference. Pin the test to things that absolutely can't break—a required heading in the quickstart, a valid JSON code block, no undefined in a JavaScript example—and leave the rest to code review. Brittle tests get deleted, then the whole pipeline goes with them. A better rule: if the test fails and the fix takes more than ten minutes of thought, the test is probably asking the wrong question.
What usually breaks first is the bond between docs and the actual product surface. So test that seam. One example, one command, one expected output. Keep it stupid. Keep it running. And when it fails, treat it like a production incident, not a chore.
The Real Cost: Maintenance and Drift
Keeping Tests in Sync With Docs
The maintenance burden starts quietly. You write a smoke test that checks a code example returns 42. Docs say 42. Test passes. Three months later, someone updates the code to return 43, updates the test, but forgets the prose. Now the test passes, the docs are wrong, and nobody notices until a customer hits the wall. That's the hidden tax—your smoke tests become stale with the docs, not against them.
We fixed this once by embedding the expected value directly in the Markdown as a comment. The test runner parsed that comment, compared it to the live output, and flagged mismatches. Annoying to set up. Worth it for a month, then the friction crept back. The real trick is pairing each smoke test with a single source of truth—one config file, one fixture, one snippet that both docs and test import. Duplication is drift in disguise. Every copy you maintain by hand is a future lie.
The Maintenance Budget
Nobody budgets for test upkeep. You allocate time for writing, editing, and maybe a review cycle. Smoke tests eat hours like a background process you forgot to kill. The catch is you only feel it when something breaks—and by then, you're debugging your test harness instead of your documentation. Give smoke tests a shrinking time cap, not a growing one. If a check requires more than ten minutes of maintenance per month, delete it or automate the upkeep away.
What usually breaks first is the environment: a dependency version bumps, a CLI flag changes, a network call becomes flaky. Then your smoke test fails for reasons unrelated to docs, and the team starts ignoring failures wholesale. That's worse than no test at all—it's a silent false alarm. Treat flaky tests as critical bugs. A smoke test that fails intermittently is a liability, not a safety net.
“The best smoke test is the one you can delete without regret—because the docs became self-verifying.”
— senior docs engineer, after killing a 200-line test harness
When Drift Becomes the New Normal
Drift is not a binary state. It's a gradient, and most teams slide down it without noticing. First, the test skips a section because the setup is too slow. Then, someone adds an exception for a legacy endpoint. Then, a comment says // TODO: re-enable after migration. The test still runs. It passes green. It checks nothing that matters. I have seen this happen in six weeks, not six months. The sad part is everyone still believes the pipeline is protecting them.
Reality check—drift becomes the norm when nobody owns the test suite. Assign one person, not a team, to review each smoke test quarterly. Ask two questions: does this still map to a user-facing behavior? Would anyone notice if it vanished? If the answer is no, remove it. A deleted test frees mental overhead; a rotting one just adds noise. The goal is not more coverage. It's less distance between what docs promise and what the product delivers.
One more lever: log every smoke test failure with a one-line note about the docs change that caused it. That history tells you where your drift is coming from. If failures cluster around API parameter renames, fix the source docs first. If they cluster around config examples, the problem is your sample data. But don't let the log become a graveyard—review it monthly and prune. Otherwise, you're maintaining a record of your own decay.
When Smoke Tests Are Overkill
Small projects and prototypes
A two-day hackathon project doesn't need a smoke test. Neither does the throwaway script that generates your personal site's changelog. The docs are four paragraphs long. You wrote them this morning. They're still warm. Adding a test harness means adding a config file, a CI step, and a habit of maintaining the test itself—all to verify something you can eyeball in ten seconds.
The catch is knowing when "small" hides a trap. I have seen teams skip smoke tests on a "prototype" that quietly became the internal admin panel for the next eighteen months. That hurt. The rule I now use: if the docs outlive the toolchain, test them. If the toolchain outlives the docs, skip the test and delete the docs.
Smoke tests are a tax you pay today to avoid a debt you can't see. Prototypes rarely live long enough to accrue that debt.
— field note from a developer who shipped a prototype that became production
Docs that are purely conceptual
Not all documentation references code. Architecture decision records, design rationales, and "why we chose Postgres over MySQL" notes don't compile, link, or execute. A smoke test that checks for dead links or broken commands adds nothing when the content is argument, not instruction. The error surface doesn't exist.
What usually breaks first in conceptual docs is coherence, not correctness. No script catches a stale rationale. No linter flags a decision record that contradicts the system you actually built. The fix isn't automation—it's a quarterly read-through by someone who wasn't in the room. That's a people problem, and pretending smoke tests solve it's wishful engineering.
When your toolchain is unstable
Reverse the situation. Your build system changes weekly. The docs are fragile—not because the content rots, but because the harness itself breaks. Every Monday you're debugging the test, not writing. Every Tuesday you're disabling a check because the staging environment hiccupped. The smoke test becomes the thing that cries wolf.
That's not a smoke test. That's a second job. Unstable toolchains inflate the cost of every validation layer you add, and the false positives train everyone to ignore failures. If your CI pipeline already has a 20% flake rate, adding doc checks multiplies the noise. Fix the pipeline first. Or drop the test and rely on a manual checklist that takes three minutes—an honest trade-off over an automated lie.
The cost-benefit threshold
Here's the blunt math. A smoke test costs you: setup time, maintenance time, and debugging time when it falsely alarms. It saves you: the time to notice a broken doc, the time to fix it, and the embarrassment of shipping garbage. If the docs take thirty seconds to review manually and you ship twice a month, the test pays for itself in about four years. That's not an investment—that's a hobby.
The threshold shifts when the docs are large, when the team is distributed, or when the consequences of a broken doc are severe. A financial API with misdocumented endpoints costs customers real money. A quickstart that fails for new hires burns onboarding hours every single week. Those justify the test. But if your docs are a landing page and a README, deploy them and move on.
One more thing: the cheapest smoke test is the one you delete. I have killed more test suites than I have written. When the burden outweighs the benefit, the professional move is to rip it out—not to add another rule. That's the real trap: the sunk cost of having built it. Wrong order. Not yet. That hurts.
Questions People Ask (and Probably Should)
How often should smoke tests run?
Daily, and that's not me being precious. Docs decay on a timer—someone merges a PR at 4:55 PM, renames a function, and the reference page quietly points at a ghost. A nightly cron job catches that by morning. Weekly feels fine until a Friday release strands your users over the weekend. The catch is that daily runs only help if someone actually reads the failure report. I have seen teams set up the perfect pipeline and then ignore it for three weeks. That hurts more than no tests at all—false confidence is a slower killer.
What if our docs are in Markdown?
Markdown is the easy case, honestly. You're not fighting a proprietary CMS or a compiled binary. A small script can grep for broken anchors, missing images, or code blocks that reference symbols no longer in the codebase. We fixed this exact problem on a Hugo site by writing a 40-line Python script that parses the markdown, extracts inline code, and cross-checks it against the repo's exported API surface. Run it in CI on every merge, not just nightly. The tricky bit is false positives—Markdown has a habit of including shell examples that look like function calls. Tune the regex, or you will mute the signal with noise. Nobody wants to be the person who chases phantom failures at 9 AM.
Start with one test. Just one. Pick the doc page that breaks most often—the one your support team complains about, the one with the longest bug history. Write a smoke test that checks the three most common mistakes on that page: a dead link, an outdated code snippet, a missing heading. Ship it. Then add a second test next month. The mistake most teams make is trying to cover everything upfront; they write a twenty-test suite, get bored, and abandon it within a quarter. A single meaningful test that runs every day and gets fixed when it fails is worth more than a hundred tests nobody reads.
Who owns the tests?
The person who owns the docs. That sounds obvious, but it rarely works that way in practice. Usually the docs are "owned" by whoever wrote them last, and the tests get lobbed into a dev team's lap. Don't do that. Assign one human—not a team, not a rotation—whose job review includes the line "smoke tests pass and the failure log is reviewed weekly." That person needs write access to the test script, not just the docs, because the test will need updating when the product changes. I have watched this fail when the docs owner could edit content but had to file a ticket to touch the test harness. The whole thing stalls. A trade-off: giving writers test ownership means teaching them a little bit of scripting. That's a small price for stopping the decay cycle. If you can't find a volunteer, that's a signal your docs are already viewed as a dumping ground—fix that first, not the tests.
Ask not what your docs need from the test. Ask what the test needs from your docs.
— paraphrase of an engineer who spent two months untangling a broken anchor farm
Next Steps: A Tiny Test That Pays Off
Pick one command and test it
Open your docs. Find the command you wrote last week—the one with flags, env vars, and a path or two. Copy it from the page, paste it into a fresh terminal, and run it against a clean checkout of your project. Not your dev environment where everything works by accident. Clean. That one paste is your smoke test. Most teams I've watched skip this because they assume "we tested this before merge." Before merge, your brain filled in the gaps—the missing --force, the deprecated token, the now-renamed config file. The docs don't have that luxury.
Wire it into your CI
You'll be tempted to make a grand test suite. Don't. Take that one command and drop it into a shell script. Five lines—check the exit code, maybe grep for a known success string, fail loudly if either breaks. Hook it to a cron job or a CI action that runs nightly. Nothing blocks merges yet. It just sits there, reporting failures while you sleep.
The catch is that your first run will probably fail. That's fine—it's a real failure, not a theoretical one. You'll fix the docs or fix the command, and then you'll know what "working" actually looks like in writing. Store that passing script as a baseline. That is your guardrail.
Measure the time saved
For two weeks, track every time that smoke test catches something. I did this once with a CLI tool's docs—fourteen days, eleven failures. Three were stale flags, two were broken copy-paste examples, six were silent dependency shifts. Each one would have cost a user ten to thirty minutes of confusion, maybe an email to support, maybe a GitHub issue with the word "useless" in it. Quick reality check—you don't need to estimate dollar values. Just count the minutes.
Most teams discover the test pays for itself within a month. The maintenance cost is near zero: a dozen lines of script, a quarter-hour per month when a command changes. The drift stops being a mystery and becomes a scheduled event.
Scale from there
After one command feels boring, add a second. Then a third. Group them by user journey—setup, deploy, rollback—not by page order. Wrong order is a common mistake; you test what's easy, not what's painful. The painful paths are where docs decay hurts most.
Run the expanded set weekly. Keep it under five minutes. If it grows past that, split it—you're writing unit tests now, not smoke tests. The distinction matters because smoke tests should be quick enough to run without dread. The moment a test feels heavy, teams stop running it. That's the pitfall, and I've seen it kill more than one good intention.
Smoke tests aren't about proving docs right. They're about catching when reality and writing quietly disagree.
— senior developer, after their third broken install guide
You'll also find that a passing smoke test builds a strange kind of confidence. When someone asks “is this doc current?” you can say yes without hedging. That's rare in technical writing. Then, and only then, consider expanding to two commands.
Comments (8)
Please sign in to post a comment.
Don't have an account? Create one