Test a generator
Assert on a generator's output without a temp directory, using in-memory generation.
Rendered from
docs/how-to/test-a-generator.md
in the generator repository — where a correction belongs, and where the
test suite runs every example on this page or states why it does not.
A generator is a program, so test it like one. Run it in memory and assert on the volume: no temp directory, no cleanup, no cross-test interference.
import assert from 'node:assert'
import { Jostraca, Project, File, Content, each } from 'jostraca'
// The generator under test, as a plain function.
function apiGenerator(model) {
return () => {
Project({ folder: model.name }, () => {
each(model.routes, (route) => {
File({ name: route.val$ + '.js' }, () => {
Content("export const name = '" + route.val$ + "'\n")
})
})
})
}
}
// The test.
const jostraca = Jostraca({ mem: true })
const model = { name: 'acme', routes: ['health', 'login'] }
const res = await jostraca.generate({ folder: '/out' }, apiGenerator(model))
const vol = res.vol().toJSON()
assert.deepEqual(
Object.keys(vol).filter((p) => !p.includes('/.jostraca/')).sort(),
['/out/acme/health.js', '/out/acme/login.js'])
assert.equal(vol['/out/acme/health.js'], "export const name = 'health'\n")
console.log('ok')
ok
Three habits make this work well:
- Return the callback, do not call it.
apiGenerator(model)builds the functiongenerate()will run. That keeps the generator callable from a test and from your command with no difference. - Filter out
.jostraca/before asserting on the file set, or pin it deliberately. It is bookkeeping, and it will change when the bookkeeping changes. - Pin
nowif anything you assert on carries a timestamp—a merge or diff marker does.
To test a second run over an edited file, write into the volume between
generates. A global mem: true with no per-call vol keeps one volume
across generate() calls, which is what makes that possible:
import { Jostraca, Project, File, Content } from 'jostraca'
const jostraca = Jostraca({
mem: true,
now: () => 1735689600000,
existing: { txt: { write: true, merge: true } },
})
const run = (body) => jostraca.generate({ folder: '/out' }, () => {
Project({}, () => File({ name: 'c.txt' }, () => Content(body)))
})
const first = await run('alpha\nbeta\ngamma\n')
first.fs().writeFileSync('/out/c.txt', 'alpha\nbeta\ngamma\nmine\n')
const res = await run('ALPHA\nbeta\ngamma\n')
console.log(JSON.stringify(res.vol().toJSON()['/out/c.txt']))
console.log(JSON.stringify(res.files.conflicted))
"ALPHA\nbeta\ngamma\nmine\n"
[]
res.fs() is the filesystem the run used, so a test can write into it
the way a user would.
Keep the generator’s change and the simulated edit apart in a test like this. A merge works on regions, not lines, so an edit on the line next to a changed one conflicts, and a test written that way asserts conflict markers containing timestamps rather than the content you meant to check.