Level 3: Build Output Verification
Testing built files, SSG output, templates, and bundler configuration.
What Level 3 Tests
Level 3 tests verify build output β the actual files produced by your build tool. Instead of testing source code directly, you run the build and inspect the results.
Typical targets:
- SSG (Static Site Generation) output HTML
- Template rendering results
- Bundler output (correct chunks, code splitting)
- Generated configuration files
- Build-time data transforms (MDX compilation, content collections)
Tools
| Tool | Role |
|---|---|
| vitest | Test runner, reading and asserting on files |
| fs/path | Node.js file system APIs to read build output |
| cheerio | Parse and query HTML output |
Example: SSG Output Verification
// tests/build-output.test.ts
import { describe, it, expect } from "vitest";
import { readFileSync } from "fs";
import { join } from "path";
const DIST = join(__dirname, "../dist");
describe("build output", () => {
it("generates index.html with correct title", () => {
const html = readFileSync(join(DIST, "index.html"), "utf-8");
expect(html).toContain("<title>My Site</title>");
});
it("generates sitemap.xml", () => {
const sitemap = readFileSync(join(DIST, "sitemap.xml"), "utf-8");
expect(sitemap).toContain("<urlset");
});
it("includes all expected pages", () => {
const pages = ["index.html", "about/index.html", "docs/index.html"];
for (const page of pages) {
const content = readFileSync(join(DIST, page), "utf-8");
expect(content).toContain("<!DOCTYPE html>");
}
});
});
Example: MDX Formatter Contract Testing
A real-world pattern from mdx-formatter: the Vitest suite tests the Rust formatting engine by running it on fixture files and comparing output:
// tests/format.test.ts
import { describe, it, expect } from "vitest";
import { format } from "../src/format";
describe("mdx formatting", () => {
it("is idempotent", () => {
const input = readFixture("sample.mdx");
const first = format(input);
const second = format(first);
expect(first).toBe(second);
});
});
π‘ Tip
Idempotency testing is a powerful invariant for any formatter or transformer: applying the operation twice should produce the same result as applying it once.
Executing Emitted Code
The examples above compare emitted output as static text β read the file, assert on its contents. But some build tools generate JavaScript that runs: adapters that wrap a user handler, bundler wrappers, scaffolders. For these, a string-equality check proves the template was written β it does not prove the generated code behaves correctly when executed. To verify that, you have to write the artifact, import it, and run it.
βΉοΈ Info
This is a different question from the static-text comparison Level 3 usually covers. There, the assertion is βdid the build produce the right file?β. Here, it is βdoes the generated wrapper actually thread its inputs through to the inner handler at runtime?β β a codegen-threading check you can only answer by executing the emitted module.
The reusable recipe is temp dir + dynamic import + env stub:
- Write a synthetic inner bundle into an
mkdtempscratch directory. - Run the emit step (
emitWorker()) to produce a real_worker.js. import(pathToFileURL(...))the emitted file, then calldefault.fetch(new Request(...), envStub).- Assert that
(env, ctx)are threaded through correctly β the wrapper your build emits must passenvto the user handler. - Clean the scratch directories in
afterEach.
// tests/emit-worker.test.ts
import { mkdtemp, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { pathToFileURL } from "node:url";
import { afterEach, describe, expect, it } from "vitest";
import { emitWorker } from "../src/build.js";
let scratchDirs: string[] = [];
afterEach(async () => {
for (const d of scratchDirs) {
await rm(d, { recursive: true, force: true });
}
scratchDirs = [];
});
async function scratch(): Promise<string> {
const d = await mkdtemp(join(tmpdir(), "emit-worker-"));
scratchDirs.push(d);
return d;
}
describe("emitWorker threads env/ctx into the inner bundle", () => {
it("the generated wrapper passes env through to the user handler", async () => {
const dir = await scratch();
// A synthetic inner bundle that reads env from the request scope
// the wrapper sets up (here via AsyncLocalStorage), then echoes it.
const inputPath = join(dir, "inner.mjs");
await writeFile(
inputPath,
`import { AsyncLocalStorage } from "node:async_hooks";
const STORAGE_KEY = "__adapter_als__";
function getStore() {
const g = globalThis;
let als = g[STORAGE_KEY];
if (!als) { als = new AsyncLocalStorage(); g[STORAGE_KEY] = als; }
return als.getStore();
}
export default {
async fetch(request) {
const store = getStore();
const env = store?.env ?? {};
return new Response(
JSON.stringify({
token: env.API_KEY ?? null,
ctxKind: typeof store?.ctx?.waitUntil,
}),
{ status: 200, headers: { "content-type": "application/json" } },
);
},
};
`,
"utf8",
);
// Run the real emit step to produce a real _worker.js.
const out = await emitWorker({
inputBundlePath: inputPath,
outdir: join(dir, "dist"),
});
// Import the emitted artifact as ESM and drive it with a
// synthetic Request + env + ctx -- no wrangler / miniflare needed.
const worker = (await import(pathToFileURL(out.workerPath).href)) as {
default: {
fetch: (
request: Request,
env: Record<string, unknown>,
ctx: { waitUntil: (p: Promise<unknown>) => void },
) => Promise<Response>;
};
};
const envStub = { API_KEY: "sk-test-1234" };
const ctx = { waitUntil: () => {} };
const request = new Request("https://worker.test/api/foo", { method: "POST" });
const response = await worker.default.fetch(request, envStub, ctx);
const body = (await response.json()) as { token: string | null; ctxKind: string };
// The headline check: env actually reached the inner handler.
expect(body.token).toBe("sk-test-1234");
// ctx was threaded too -- its surface matches the runtime shape.
expect(body.ctxKind).toBe("function");
});
});
π‘ Tip
Keep the inner bundle synthetic and self-contained β inline the read-side of whatever mechanism the wrapper uses (here, the globalThis-keyed AsyncLocalStorage). You are testing your emitted wrapperβs threading, not the real user package, so you do not need to bundle it. The same pattern stubs any binding the wrapper treats opaquely (a database handle, a secret, a queue), because the wrapper just stores env verbatim and never inspects its members.
β οΈ Warning
This still is not a full runtime test. Importing the emitted module exercises it in Node, not in the real target runtime (Cloudflare Workers, Deno, an edge sandbox). Runtime-specific globals, the actual asset server, and platform request semantics are out of scope β for those, you need Level 4 (or a real wrangler dev / miniflare run). What this does prove is the contract your codegen is responsible for: inputs in, inputs threaded through.
Blind Spots
β οΈ Warning
Level 3 tests verify file contents, not runtime behavior. They cannot detect:
- JavaScript runtime errors
- Client-side hydration issues
- Visual rendering problems
- Browser API interactions
- Dynamic content loaded after page load
When to Use Level 3
| Scenario | Level 3 Appropriate? |
|---|---|
| SSG page missing from build | Yes |
| Wrong HTML structure in output | Yes |
| Bundle too large / wrong chunks | Yes |
| Hydration mismatch in browser | No β use Level 4 |
| Page renders blank in browser | No β use Level 4/5 |
| CSS not applied correctly | No β use Level 5 |