Skip to content

Testing

Popii includes a testing sub-export that lets you run command handlers against a mock context — no bot token, no Discord connection, no test guild. Tests run with Bun’s built-in test runner.

import { createTestClient, runCommand, mockOptions } from "popii-framework/testing";

These are separate from the main export so they never ship in your production bot’s bundle.

test/commands/ping.test.ts
import { describe, it, expect } from "bun:test";
import { createTestClient, runCommand } from "popii-framework/testing";
import pingCommand from "../../src/commands/ping";
describe("/ping", () => {
it("replies with Pong!", async () => {
const client = createTestClient();
const result = await runCommand(pingCommand, { client });
expect(result.replies[0].content).toContain("Pong!");
});
});

result gives you everything the handler sent back:

PropertyTypeDescription
result.repliesReply[]All replies sent via pop.reply()
result.deferredbooleanWhether pop.defer() was called
result.localsPopiiLocalsFinal state of pop.locals after the handler
import { runCommand, mockOptions } from "popii-framework/testing";
import greetCommand from "../../src/commands/greet";
it("greets the target user", async () => {
const client = createTestClient();
const result = await runCommand(greetCommand, {
client,
options: mockOptions({
user: { id: "123456789", username: "Alice" },
message: "Hey there",
}),
});
expect(result.replies[0].content).toContain("Alice");
});

mockOptions accepts the same shape as the real options object and wraps it in the CommandInteractionOptionResolver interface your command expects.

Providing locals (pre-populated middleware data)

Section titled “Providing locals (pre-populated middleware data)”

Skip middleware by seeding pop.locals directly:

it("rejects non-premium users", async () => {
const client = createTestClient();
const result = await runCommand(exportCommand, {
client,
locals: { dbUser: { id: "123", balance: 0, premium: false } },
});
expect(result.replies[0].ephemeral).toBe(true);
expect(result.replies[0].content).toContain("Premium");
});

To test that middleware interacts correctly with a command, pass the middleware array:

import loadUserMiddleware from "../../src/middlewares/02-load-user";
it("loads the user record before the handler runs", async () => {
const client = createTestClient();
const result = await runCommand(profileCommand, {
client,
middlewares: [loadUserMiddleware],
});
expect(result.locals.dbUser).toBeDefined();
});
it("defers before fetching data", async () => {
const client = createTestClient();
const result = await runCommand(reportCommand, { client });
expect(result.deferred).toBe(true);
expect(result.replies).toHaveLength(1); // the follow-up reply after defer
});
Terminal window
bun test # run all tests
bun test test/commands/ # run a directory
bun test test/commands/ping.test.ts # run one file
bun test --watch # re-run on save