Nubis / Testing
Ship
Testing
Run commands, components, modals and menus the way Discord would, with no network and no token.
nubis/testing sends your handlers the interactions Discord would send, records what they answer, and hands you the result. It works with bun:test, Vitest or any runner with expect.
import { expect, test } from "bun:test";
import { PermissionFlagsBits } from "discord-api-types/v10";
import { fake, testCommand } from "nubis/testing";
import ban from "../commands/ban";
test("asks for confirmation", async () => {
const result = await testCommand(ban)
.as({ permissions: PermissionFlagsBits.BanMembers })
.run({ user: fake.user("alice") });
expect(result.replies).toEqual(["Ban alice?"]);
expect(result.ephemeral).toBe(true);
});
test("members without the permission are turned away", async () => {
const result = await testCommand(ban).run({ user: fake.user("alice") });
expect(result.status).toBe("denied");
});The options you pass to run are typed from the command, so a test can't send an option the command doesn't have.
Testers
| Function | Then |
|---|---|
testCommand(command) | .run(options) |
testComponent(button) | .click(state) |
testComponent(select) | .select(values, state) — values for string selects, fake users, roles or channels for the others |
testModal(modal) | .submit(values, state) |
testMenu(menu) | .run(target) — a user or a fake.message() |
Each takes, before the final call:
.as(actor)— who is acting:user,roles,permissions, the bot'sappPermissions,guildId,channelId,dm: true, and for components theinvokerwho ran the original command..with(options)— bot-wideguards,middleware,autoDeferMsor anapi, as the bot would run with them.
testComponent(...).send(customId) sends a custom_id as-is, to test what tampered state does.
Results
| Field | What it is |
|---|---|
status | "handled", "denied", "stale", "error", … |
replies | The text of every message sent, edited or followed up, in order. |
messages | The full message bodies. |
ephemeral | Whether the first message was private. |
modals | Modals opened. |
uploads | File names attached to each call. |
trace | The calls in order, e.g. ["respond:5", "editOriginal"]. |
error | What the handler threw, when status is "error". |
Fakes
fake.user(name), fake.role(name), fake.channel(name) and fake.message(content) build Discord objects with fresh ids. Each takes overrides for any field.
Discord's API
fakeApi() records calls to api instead of making them. Methods resolve to {} unless you give a response:
import { fakeApi, testComponent } from "nubis/testing";
const api = fakeApi({ "guilds.get": { id: "g1", name: "Test" } });
await testComponent(confirmBan).with({ api }).click({ userId: "42" });
expect(api.callsTo("guilds.banUser")).toHaveLength(1);Slow handlers
Testers turn the automatic "thinking…" off, so a result never depends on how fast the machine running the tests is. To test a deferral, pass .with({ autoDeferMs: 10 }) and check trace.