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.
Importing the test utilities
Section titled “Importing the test utilities”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.
Testing a command
Section titled “Testing a command”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:
| Property | Type | Description |
|---|---|---|
result.replies | Reply[] | All replies sent via pop.reply() |
result.deferred | boolean | Whether pop.defer() was called |
result.locals | PopiiLocals | Final state of pop.locals after the handler |
Providing slash command options
Section titled “Providing slash command options”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");});Running the full middleware pipeline
Section titled “Running the full middleware pipeline”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();});Testing deferred commands
Section titled “Testing deferred commands”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});Running tests
Section titled “Running tests”bun test # run all testsbun test test/commands/ # run a directorybun test test/commands/ping.test.ts # run one filebun test --watch # re-run on save