@usehenri/testing
v1.2.0
Published
henri testing helpers: boots the app for vitest and binds a supertest agent to it
Readme
@usehenri/testing
Boots a henri app for its tests and binds supertest
to the running server. Built for Vitest, which henri test runs.
pnpm add -D vitest @usehenri/testingSetup
vitest.config.js in the app:
const { defineConfig } = require('vitest/config');
module.exports = defineConfig({
test: {
environment: 'node',
fileParallelism: false,
globals: true,
include: ['test/**/*.{spec,test}.js'],
setupFiles: ['@usehenri/testing/setup-file'],
},
});The setup file boots henri (NODE_ENV=test, config/test.json, a free port,
workers skipped) before each test file and stops it after, inside the test
worker: henri and the models are globals in the tests, exactly like in the
app. fileParallelism: false keeps one server and one database at a time --
drop it only when each file gets a database of its own, which is what the disk
adapter does under NODE_ENV=test.
Writing tests
// test/tasks.spec.js
const { request } = require('@usehenri/testing');
describe('tasks', () => {
test('lists tasks', async () => {
await Task.create({ title: 'write tests' });
const res = await request().get('/tasks').set('Accept', 'application/json');
expect(res.status).toBe(200);
expect(res.body.tasks).toHaveLength(1);
});
});Without the setup file, boot henri from the test file itself; setup() is a
no-op when henri is already running:
const { request, setup, teardown } = require('@usehenri/testing');
beforeAll(() => setup());
afterAll(() => teardown());Factories
A factory makes a valid record with the fields the test does not care about
already filled in. It lives in test/factories/<name>.js, is named after its
file and writes to the model of that name.
// test/factories/task.js
module.exports = {
attributes: {
name: ({ sequence }) => `Task ${sequence}`,
ownerId: async ({ create }) => (await create('user')).id,
},
traits: {
done: { completedAt: () => new Date(), state: 'done' },
},
};const { build, create, createList } = require('@usehenri/testing');
await create('task'); // saved, with an owner
await create('task', 'done'); // with the trait
await create('task', { ownerId: me.id }); // no second user is made
await createList('task', 3, 'done');
await build('task'); // the attributes, unsavedA value is a literal or a function of the build context (attrs, build,
create, sequence, traits, uid), and fields resolve on demand: reading
await attrs.eventId from another field resolves that one first. An override
always wins, and the definition's value is then not evaluated at all.
after(record, context) runs on the saved record and may replace it, which is
how a factory grants roles the model will not mass assign.
defineFactory(name, definition) declares one from a test file.
Mail and jobs
inbox() is the mail the application was asked to send, oldest first, and
enqueued() is what the job queue holds:
const { enqueued, inbox, request } = require('@usehenri/testing');
await request().post('/signup').send({ email: '[email protected]', password });
const [mail] = inbox();
expect(inbox()).toHaveLength(1);
expect(mail.to).toContain('[email protected]');
expect(mail.subject).toBe('Confirm your address');
expect(mail.html).toContain('/confirm/');
expect(await enqueued('welcome')).toHaveLength(1);Both doors are captured: deliver(), which reaches the transport, and
deliverLater(), which reaches the queue -- an entry says which with
deferred, and carries to, cc, bcc, from, subject, html, text,
the mailer and action that rendered it, and message, the nodemailer
payload itself. inbox(filter) narrows on action, deferred, mailer,
subject and to, or on a predicate. The setup file empties the inbox before
every test; clearInbox() does it by hand.
enqueued(filter) reads the queue back, so a row's args are what a runner
will claim; henri.jobs.performNow(job.name, job.args) runs one. It needs
@usehenri/jobs, which this package does not depend on: an application
without a queue is told what to install rather than handed an empty list.
clearJobs() forgets the rows, and nothing calls it for you.
API
setup({ workers = false })boots henri for the app inprocess.cwd()and resolves with the instance. Idempotent.teardown()stops it.request()a supertest request bound to the running server.agent()a supertest agent (keeps cookies between requests).inbox(filter)the mail the app was asked to send;clearInbox()empties it.enqueued(filter)the jobs the queue holds;clearJobs(filter)forgets them.create(name, ...traits, overrides)a saved record from a factory.build(name, ...traits, overrides)the attributes, without saving.createList(name, count, ...traits, overrides)several of them.defineFactory(name, definition)a factory without a file.resetFactories()forgets the definitions and the sequences.henrithe running instance (alsoglobal.henri).supertestthe underlying module.
One server for the whole run
globalSetup: ['@usehenri/testing/global-setup'] boots henri once in Vitest's
main process. Tests only get HTTP access through request() (no henri or
model globals in the workers) since global setup runs in another process.
Browser tests
@usehenri/testing/playwright is the same boot under
Playwright, and the whole of what an application
writes for one:
// playwright.config.js
module.exports = {
globalSetup: '@usehenri/testing/playwright',
testDir: './test/browser',
};The port is the kernel's answer to listen(0), so it does not exist when
playwright.config.js is read. The global setup boots henri and exports
PLAYWRIGHT_TEST_BASE_URL, which is what Playwright's use.baseURL falls
back to in every worker — measured against Playwright 1.63.0, along with the
rest of that precedence: anything written in use wins over the variable,
and henri says so rather than letting the browser go somewhere else in
silence.
Playwright is the application's dependency: this package never imports it, and
henri doctor reports a playwright.config.* with no @playwright/test in
package.json. The database is left alone — one server for the run is one
database for the run, so seeding belongs to a global setup of your own that
wraps boot(config) from this subpath. See
the guide.
henri test # vitest run
henri test --watch # vitest in watch mode
henri test test/tasks.spec.js -t "lists"