Testing
@njinlabs/njin/testing starts your real app, with your models, hooks, events, routes and vars, and lets a test call it without opening a port. Tests use Bun's own test runner.
Setting up
A test file creates the app once, and closes it at the end:
// tests/post.test.ts
import { afterAll, expect, it } from "bun:test";
import { createTestApp } from "@njinlabs/njin/testing";
import config from "../config";
import post from "../src/models/post";
const app = await createTestApp(config);
afterAll(() => app.close());
it("lists posts for a signed-in user", async () => {
await app.seed(post, { title: "Hello" });
const res = await app.get("/api/post", { as: await app.createUser() });
expect(res.status).toBe(200);
});Run the tests with:
bun test --isolateThe project's bun run test script does this. njin's modules are process-wide singletons, so one process can hold only one test app. --isolate gives every test file its own process. Without it, a second createTestApp() call throws an error that says so.
What createTestApp() does differently from the real server:
- The database is an in-memory SQLite database, unless you pass
adapters: { db }in the config you give it. Your realconfig.tsmay use another database, and the test ignores it. - It listens on no port. Requests go straight to the app.
- Logging is turned off. Pass
loggerin the config to change it. - No Vite dev server starts.
To test against the database you use in production, pass its adapter:
import postgresAdapter from "@njinlabs/njin/adapters/postgres";
const app = await createTestApp({
...config,
adapters: { db: postgresAdapter({ url: process.env.TEST_DATABASE_URL }) },
});Calling the API
get, post, put, patch and delete take a path and an options object, and return a standard Response:
const res = await app.post("/api/post", {
body: { title: "Hello", slug: "hello" },
as: user,
});
expect(res.status).toBe(200);
const { data } = await res.json();| Option | What it does |
|---|---|
body |
Sent as JSON. A FormData, string or Uint8Array is sent as it is, which is how you test an upload |
query |
An object of query string parameters |
headers |
Extra headers |
as |
Signs the request in. Pass a user, and a session token is created for it. You can also pass a ready-made token string |
app.request(method, path, init) is the same call with the method as an argument.
Seeding data and users
const record = await app.seed(post, { title: "Hello" });
const user = await app.createUser();
const token = await app.tokenFor(user);seed(model, data)creates a record through the model, so its hooks run anduniquefields are checked. The schema's own rules, such asmin(3), are not applied. They run only on API requests, so test them by calling the endpoint.createUser()makes a user with a random email and the password"password". You can passname,email,passwordandgroups.tokenFor(user)returns a session token, to put in your ownAuthorizationheader.
Testing permissions
A user from createUser() is in the built-in Full access group, so it can do everything. To test a narrower role, make a group and put the user in it:
const editors = await app.createGroup({
name: "Editors",
grants: ["post:*:own"],
});
const editor = await app.createUser({ groups: [editors.id] });
const nobody = await app.createUser({ groups: [] });
const own = await app.get("/api/post", { as: editor });
const denied = await app.get("/api/post", { as: nobody });
expect(denied.status).toBe(403);createUser({ groups: [] }) makes a user that can do nothing. createGroup() takes name, grants and an optional description. See Grants.
Resetting between tests
app.reset() empties every table, including users and sessions, and keeps the schema. Use it in beforeEach when tests must not see each other's data:
import { beforeEach } from "bun:test";
beforeEach(() => app.reset());app.db is the database adapter, for assertions the HTTP surface cannot express.
Testing your own code
Anything that runs in the app can be tested by calling it through the app. A custom route, a hook triggered by creating a record, or an event listener runs as it does in production:
it("creates a slug before saving", async () => {
const res = await app.post("/api/post", { body: { title: "Hello World" }, as: user });
const { data } = await res.json();
expect(data.slug).toBe("hello-world");
});