DEV Community

Mr Zack
Mr Zack

Posted on

Your `{}` smoke test passes. Your users still crash.

We run 45 scrapers on Apify Store. Every one of them gets an automated smoke test every day: start the actor with an empty input {}, check that it finishes and returns rows. Green across the board for weeks.

Meanwhile, one of our paid actors — the Amazon one — was failing for real users. Four failed runs in one day, from people who were paying for it.

The crash was a one-line bug that no {} test can ever catch. Here's the shape of it, because you probably have the same one.

The bug

The actor has several modes: keyword search, a list of ASINs, or "Best Sellers" rankings. If the user gives none of them, we fall back to a demo search so the run isn't empty:

const keywords = list(input.keywords);
const asins    = list(input.asins);

if (!keywords.length && !asins.length && !bestSellersCategories.length) {
  keywords.push('air fryer'); // demo
}

// ...eight lines later:
const bestSellersCategories = list(input.bestSellersCategories);
Enter fullscreen mode Exit fullscreen mode

bestSellersCategories is a const declared below the line that reads it. In JavaScript that isn't undefined — it's a ReferenceError (the temporal dead zone). But && short-circuits: as long as keywords has something in it, the bad expression is never evaluated.

So:

  • {} → the platform injects the schema default keywords: ["air fryer"] → short-circuit → fine.
  • Our unit tests → parser only, never boot the actor → fine.
  • Our "all paid flags on" test → keywords present → fine.
  • A user who opens the Best Sellers mode, deletes the default keyword, and picks a category → keywords: [] → the expression runs → crash on line 1 of the run.

The mode we put in the title of the actor was the one that crashed.

Why every gate was green

Each of our checks answered a question that wasn't the question:

gate question it answers
{} smoke test "does the default path work?"
unit tests "does the parser work?"
all-flags-on run "do the paid options work on top of the default path?"
node --check "is it syntactically valid?" (TDZ is valid syntax)

None of them asked: "does each alternative input mode work when the primary mode is explicitly empty?"

That's not a corner case. It's what a real user does the moment they want the second feature instead of the first one.

Two fixes, one of them permanent

  1. Move the demo line below the declarations. Thirty seconds.
  2. Make sure it can't come back:

A static gate for use-before-declaration. ESLint's no-use-before-define catches this, but we wanted something that runs against the deployed entry point of all 45 actors in one command, with zero config per actor. Forty lines with acorn: parse the module, list top-level let/const/class by statement index, walk every top-level statement (skipping function bodies — they run later) and flag any identifier that resolves to a declaration further down. It found exactly the two amazon lines and nothing else across 160 files.

A third smoke-test scenario. Our user-path audit ran A ({}) and B (paid flags on). It now runs C: for every actor with more than one input mode, an input that empties the primary field and fills the alternative one — {"keywords": [], "bestSellersCategories": ["electronics"]}, {"queries": [], "sources": ["reuters.com"]}, {"videoUrls": [], "channelUrls": [...]}, and so on.

Scenario C paid for itself within the hour. Running it across the catalogue turned up a second, quieter bug in a different actor: a Google News scraper where sources without a query fell back to the top-stories feed and then filtered it against the source list — zero articles, status "Succeeded". Same family: the alternative mode was never exercised on its own.

The takeaway

If your actor (or CLI, or API) has more than one way to specify what to fetch, your smoke tests need one run per way, with the other ways explicitly empty — not absent. Absent gets defaults injected. Empty is what users send.

And add the TDZ gate. It's the cheapest test you'll ever write: it costs nothing to run and it catches the one class of crash that "it works on {}" is structurally unable to see.


The gate and the scenario runner live in the tooling behind our Apify Store actors. Previous post in this series: The scraper said "Succeeded". The column was dead for weeks.

Top comments (0)