Testing & ChatMock
The ChatMock utility is a purpose-built simulation wrapper native to the WhatsBotCord library. It allows you to execute your commands locally, feed them synthetic inbound payloads, and assert their exact outbound responses within standard testing frameworks (like bun:test, vitest, or jest).
This means you don’t need an active WhatsApp connection, a testing device, or QR scans to rigorously validate logic flows!
Core Mechanics
Section titled “Core Mechanics”A standard testing implementation involves four specific phases: Configuration, Simulation Enqueueing, Execution, and Assertion Validation.
import { test, expect } from "your-testing-framework";import { SenderType } from "whatsbotcord";import { ChatMock } from "whatsbotcord/testing";// Import the command you want to testimport MyCommand from "./myCommand.js";
test("MyCommand should respond correctly", async () => { // 1. Configuration const chat = new ChatMock(new MyCommand(), { senderType: SenderType.Individual, });
// 2. Simulation Enqueueing (Used if your command awaits input) // chat.EnqueueIncoming_Text("yes");
// 3. Execution const botOutputs = await chat.StartChatSimulation();
// 4. Assertion Validation expect(chat.SentFromCommand.Texts).toHaveLength(1); expect(chat.SentFromCommand.Texts[0].text).toEqual("Expected reply!");});1. Configuration (MockingChatParams)
Section titled “1. Configuration (MockingChatParams)”When instantiating ChatMock, you can forge the origin state. The MockingChatParams parameter accepts comprehensive customization overrides:
Chat & Sender Targeting
Section titled “Chat & Sender Targeting”senderType: Simulates whether the command executes inside an individual direct message (SenderType.Individualtargeting@s.whatsapp.netidentifiers) or a group chat (SenderType.Grouptargeting@g.usor@lididentifiers). Defaults toIndividualunless a participant ID is explicitly provided.chatId: The simulated chat ID. If omitted, an ID with the correct WhatsApp suffix (@g.usor@s.whatsapp.net) is automatically generated.participantId_LID: The modern Linked Device identifier (@lid) of the simulated sender.participantId_PN: The standard phone number format ID (@s.whatsapp.net) of the simulated sender.
// Example: Simulating a group messageconst groupChatMock = new ChatMock(new MyCommand(), { senderType: SenderType.Group, chatId: "123456789@g.us", participantId_PN: "987654321@s.whatsapp.net"});Message Setup
Section titled “Message Setup”args: Simulates text appended to the command trigger.msgType: The explicit type of the initial command-triggering message.
// Example: Simulating '!ban @user reason'const argsMock = new ChatMock(new BanCommand(), { args: ["@123456789", "spamming"],});Context Overrides
Section titled “Context Overrides”botSettings: Temporarily overwrites internalBotproperties specifically for this test run.chatContextConfig: Granular overrides for theChatContextpipeline.cancelKeywords: An array of string payloads that will immediately trigger an explicit Wait cancellation error during the execution. By default["cancel"].
const chat = new ChatMock(command, { senderType: SenderType.Group, participantId_LID: "12345678@lid", args: ["testArg"], chatContextConfig: { timeoutSeconds: 30 }, botSettings: { initialCommandsToAdd: [{ command: new HelperTargetCMD(), commandType: CommandType.Normal }] }});2. Simulation Enqueuing
Section titled “2. Simulation Enqueuing”If your command implements conversational flows utilizing ctx.WaitText or ctx.WaitMultimedia, the test will pause indefinitely and throw a deadlock error unless you provide the expected simulated responses preemptively (or via dynamic simulation).
ChatMock exposes strict enqueuing methods resolving precisely into the awaited flows:
EnqueueIncoming_Text(text): Simulates an incoming raw string.EnqueueIncoming_Img(urlOrOpts)/EnqueueIncoming_Video: Simulates rich media components.EnqueueIncoming_Contact(contacts): Pushes specific virtual card datasets.
test("Multi-step configuration command", async () => { const chat = new ChatMock(new ConfigCommand(), { senderType: SenderType.Group });
// Note: Enqueue methods return `void`. They just stage the payloads // for when `StartChatSimulation()` reaches a wait operation. chat.EnqueueIncoming_Text("Confirm settings"); chat.EnqueueIncoming_Text("yes");
// Runs the command logic uninterrupted through the queues! // Returns an array of mocked outgoing payloads (BotReplies[]) const outputs = await chat.StartChatSimulation();
console.log("Bot replied with:", outputs);});3. Assertion Validation
Section titled “3. Assertion Validation”Once StartChatSimulation() finishes resolving, the ChatMock instance formally exposes read-only property structures spanning everything your command did.
Validating Outbound Sends
Section titled “Validating Outbound Sends”The chat.SentFromCommand property offers specific arrays storing chronological outbound payloads issued through the IChatContext:
// Example: Validating a text was sentexpect(chat.SentFromCommand.Texts[0].text).toBe("Hello world!");
// Example: Validating an emoji reactionexpect(chat.SentFromCommand.ReactedEmojis[0].emoji).toBe("✅");Texts: Stores all payloads fired viactx.SendText(). Access output strings viachat.SentFromCommand.Texts[0].text.Images/Videos/Audios/Documents/Stickers: Stores multimedia outputs and their captions.ReactedEmojis: Specific emoji metrics logging responses toctx.SendReactEmojiToInitialMsg(),ctx.Ok(), etc.Polls/Locations/Contacts: Complex associative payload objects evaluated inside specific test scopes.
Validating Inbound Requests
Section titled “Validating Inbound Requests”The chat.WaitedFromCommand array explicitly chronicles what WaitX() calls the command initiated, capturing exact timestamp delays and expected return configurations dynamically queued inside your script.
// Check if the command requested text inputexpect(chat.WaitedFromCommand[0].msgType).toBe(MsgType.Text);Validating Raw Socket Behavior
Section titled “Validating Raw Socket Behavior”If your command leveraged the raw global bot properties (e.g., executing api.InternalSocket.Send...) rather than relying on context wrappers, you can validate system calls via:
SentFromCommandSocketQueue: Simulated payloads specifically pushed through standard queue management arrays.SentFromCommandSocketWithoutQueue: Simulated payloads directly dispatched entirely bypassing rate-limits.
Advanced examples
Section titled “Advanced examples”Here are robust examples ensuring commands correctly handle missing parameters, confirmation loops, and dynamic mocking utilizing ChatMock:
import { describe, expect, test } from "your-testing-framework";import { SenderType } from "whatsbotcord";import { ChatMock } from "whatsbotcord/testing";import AddMemberCommand from "./commands/AddMemberCommand.js";
describe("Add Member Command Workflows", () => { test("Should execute failure payload when no arguments are provided", async () => { const command = new AddMemberCommand(); const chat = new ChatMock(command, { senderType: SenderType.Group, args: [], // Represents missing arguments! });
await chat.StartChatSimulation();
// Assert the response texts expect(chat.SentFromCommand.Texts).toHaveLength(1); expect(chat.SentFromCommand.Texts[0].text).toContain("Required arguments missing");
// Assert a Failure emoji reaction was posted over the original command msg expect(chat.SentFromCommand.ReactedEmojis[0].emoji).toBe("❌"); });
test("Should prompt for confirmation and finalize logic upon confirmation", async () => { const command = new AddMemberCommand(); const chat = new ChatMock(command, { senderType: SenderType.Group, args: ["@123456789"], });
// We enqueue 'yes' so the WaitYesOrNoAnswer method resolves correctly automatically chat.EnqueueIncoming_Text("yes"); // We enqueue a generic PNG buffer mimicking a user sending a profile photo chat.EnqueueIncoming_Img({ imgContentBufferMock: Buffer.from("mockImgData") });
await chat.StartChatSimulation();
// Confirm execution sequence text output expect(chat.SentFromCommand.Texts).toHaveLength(2); expect(chat.SentFromCommand.Texts[0].text).toBe("Are you sure you want to add this member? (yes/no)"); expect(chat.SentFromCommand.Texts[1].text).toBe("Member added successfully.");
// Validate a success payload was emitted expect(chat.SentFromCommand.ReactedEmojis[0].emoji).toBe("✅"); });});Dynamic Simulation
Section titled “Dynamic Simulation”Instead of enqueuing all inputs at once, you can step through the chat interactively using dynamic queues.
test("Interactive simulation", async () => { const chat = new ChatMock(new InteractiveCommand());
// Start the simulation, it stops at the first `ctx.Wait*` const botOutputs = await chat.StartChatSimulation(); expect(botOutputs[0].msg).toContain("Please send your name:");
// Provide the user response interactively const nextOutputs = await chat.QueueAction_SimulateSendText("John Doe"); expect(nextOutputs[0].msg).toContain("Hello John Doe!");});Advanced Integration Testing (with Dependency Injection)
Section titled “Advanced Integration Testing (with Dependency Injection)”For production-scale applications, you should test your commands using robust test runners like bun:test, and inject mock databases or file systems into your commands using Dependency Injection (e.g., tsyringe). This ensures pure determinism across tests.
import { describe, expect, it } from "bun:test";import path from "path";import { container, Lifecycle } from "tsyringe";import { SenderType } from "whatsbotcord";import { ChatMock } from "whatsbotcord/testing";
// ... your domain specific mocks ...
function createPerfilCommand({ players = [], fsEntries = {} } = {}) { const localContainer = container.createChildContainer();
// Mock the database localContainer.register("PlayerRepository", { useValue: players }, { lifecycle: Lifecycle.Transient });
// Mock the file system localContainer.register("IFileSystem", { useValue: new FileSystemMock({ entries: fsEntries }), });
return localContainer.resolve(PerfilCommand);}
describe("Perfil Command Tests", () => {
it("INDIVIDUAL | User not found | Should send error msg", async () => { const command = createPerfilCommand();
const chat = new ChatMock(command, { senderType: SenderType.Individual, chatId: "notvalid@s.whatsapp.net", }); await chat.StartChatSimulation();
expect(chat.SentFromCommand.Images).toHaveLength(0); expect(chat.SentFromCommand.Texts.at(0)!.text).toContain("Not found"); expect(chat.SentFromCommand.ReactedEmojis[0].emoji).toBe("❌"); });
it("GROUP | User found | Should send profile card", async () => { const command = createPerfilCommand({ players: [{ ID: "valid_id", name: "John" }], fsEntries: { "profile.png": "mockImgData" }, });
const chat = new ChatMock(command, { senderType: SenderType.Group, participantId_PN: "valid_id@s.whatsapp.net", });
await chat.StartChatSimulation();
expect(chat.SentFromCommand.Texts).toHaveLength(0); expect(chat.SentFromCommand.Images).toHaveLength(1); expect(chat.SentFromCommand.ReactedEmojis[0].emoji).toBe("✅"); });
});Injecting Custom Adapters (Mocking the Entire Bot)
Section titled “Injecting Custom Adapters (Mocking the Entire Bot)”If you are writing holistic Integration tests for your application instead of isolating individual commands, or if you simply want to switch the underlying WhatsApp engine, Bot supports injecting a Custom Adapter (or a Mock Adapter) right into its constructor.
This prevents the bot from generating a real QR code or connecting to the Baileys network, while still allowing all your logic to execute.
import Whatsbotcord, { BaileysAdapter } from "whatsbotcord";import { MockAdapter } from "whatsbotcord/testing";// Optional: Import a custom adapter// import { MyCustomWhatsappWebJsAdapter } from "./my-adapter";
// 1. Standard initialization: Uses Baileys by default and connects to WhatsApp if you do not privade an adapter.const bot = new Whatsbotcord({ commandPrefix: "!",}, new BaileysAdapter({ credentialsFolder: "./auth" }));
// 2. Custom/Mock Initialization: Injects a different adapter engine// const botMocked = new Whatsbotcord({ commandPrefix: "!" }, new MyCustomWhatsappWebJsAdapter());const botMocked = new Whatsbotcord({ commandPrefix: "!",}, new MockAdapter());