Skip to content

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!

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 test
import 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!");
});

When instantiating ChatMock, you can forge the origin state. The MockingChatParams parameter accepts comprehensive customization overrides:

  • senderType: Simulates whether the command executes inside an individual direct message (SenderType.Individual targeting @s.whatsapp.net identifiers) or a group chat (SenderType.Group targeting @g.us or @lid identifiers). Defaults to Individual unless a participant ID is explicitly provided.
  • chatId: The simulated chat ID. If omitted, an ID with the correct WhatsApp suffix (@g.us or @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 message
const groupChatMock = new ChatMock(new MyCommand(), {
senderType: SenderType.Group,
chatId: "123456789@g.us",
participantId_PN: "987654321@s.whatsapp.net"
});
  • 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"],
});
  • botSettings: Temporarily overwrites internal Bot properties specifically for this test run.
  • chatContextConfig: Granular overrides for the ChatContext pipeline.
  • 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 }]
}
});

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);
});

Once StartChatSimulation() finishes resolving, the ChatMock instance formally exposes read-only property structures spanning everything your command did.

The chat.SentFromCommand property offers specific arrays storing chronological outbound payloads issued through the IChatContext:

// Example: Validating a text was sent
expect(chat.SentFromCommand.Texts[0].text).toBe("Hello world!");
// Example: Validating an emoji reaction
expect(chat.SentFromCommand.ReactedEmojis[0].emoji).toBe("✅");
  • Texts: Stores all payloads fired via ctx.SendText(). Access output strings via chat.SentFromCommand.Texts[0].text.
  • Images / Videos / Audios / Documents / Stickers: Stores multimedia outputs and their captions.
  • ReactedEmojis: Specific emoji metrics logging responses to ctx.SendReactEmojiToInitialMsg(), ctx.Ok(), etc.
  • Polls / Locations / Contacts: Complex associative payload objects evaluated inside specific test scopes.

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 input
expect(chat.WaitedFromCommand[0].msgType).toBe(MsgType.Text);

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.

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("✅");
});
});

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());