Testing

Test your bot handlers and custom adapters with @chat-adapter/tests — Vitest factories, custom matchers, and a setup file.

The @chat-adapter/tests package gives you Vitest factories, custom matchers, and a setup file for testing bots and custom adapters built on Chat SDK.

Install

pnpm add -D @chat-adapter/tests

chat and vitest are peer dependencies — they should already be in your project.

Auto-register all matchers by adding the package's setup file to your Vitest config:

vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    setupFiles: ["@chat-adapter/tests/setup"],
  },
});

Without the setup file, register matchers manually:

import { matchers } from "@chat-adapter/tests/matchers";
expect.extend(matchers);

Mock factories

import {
  createMockAdapter,
  createMockChatInstance,
  createMockState,
  createTestMessage,
  mockLogger,
} from "@chat-adapter/tests";
FactoryReturnsNotes
createMockAdapter(name?, overrides?)AdapterEvery method is vi.fn() with sensible defaults
createMockChatInstance(options?)ChatInstanceEvery process* handler is vi.fn(); getState/getUserName/getLogger wired up
createMockState()MockStateAdapterIn-memory Maps for subscriptions, locks, KV, lists, queues; cache exposes the underlying map
createTestMessage(id, text, overrides?)MessageMarkdown text is parsed into the formatted AST
mockLogger / createMockLogger()LoggerShared default vs fresh-per-call

Matchers

MatcherAsserts
expect(adapter).toHavePosted(threadId, textPattern?)adapter.postMessage was called for this thread
expect(adapter).toHaveEdited(threadId, messageId, textPattern?)adapter.editMessage was called for this message
expect(adapter).toHaveDeleted(threadId, messageId)adapter.deleteMessage was called for this message
expect(adapter).toHaveReactedWith(threadId, messageId, emoji)adapter.addReaction was called with the emoji (string or EmojiValue.name)
expect(adapter).toHaveStartedTyping(threadId)adapter.startTyping was called for this thread
expect(adapter).toHavePostedToChannel(channelId, textPattern?)adapter.postChannelMessage was called for this channel
expect(chat).toHaveDispatched(handler)The named process* handler on the mock ChatInstance was called
expect(state).toBeSubscribedTo(threadId)state.isSubscribed(threadId) resolves to true (async — await expect(...))

Text-pattern matchers extract a comparable string from AdapterPostableMessage — strings directly, PostableMarkdown.markdown, PostableRaw.raw, and PostableCard.fallbackText. AST-shaped messages and cards without fallbackText aren't text-matchable; assert without textPattern and inspect mock.calls directly.

Bot authors: test your handlers

When you're building a bot on top of Chat SDK, the kit lets you exercise your handlers without a real Slack/Teams/etc. webhook on the wire:

bot.test.ts
import { describe, expect, it } from "vitest";
import { Chat } from "chat";
import { createMockAdapter, createMockState } from "@chat-adapter/tests";

describe("bot handlers", () => {
  it("replies with a greeting on mention", async () => {
    const slack = createMockAdapter("slack");
    const state = createMockState();
    const bot = new Chat({
      userName: "mybot",
      adapters: { slack },
      state,
    });

    bot.onNewMention(async (thread) => {
      await thread.post("hello there");
    });

    // Drive a synthesized mention through the bot…
    // (use your adapter's webhook path or a thread-level call)

    expect(slack).toHavePosted("slack:C1:t1", /hello there/);
  });
});

Adapter authors: test webhook → dispatch

When you're building a custom Adapter, the kit gives you a ChatInstance mock you can hand to your adapter and assert that webhooks route through the right process* hook with the right normalized payload:

adapter.test.ts
import { describe, expect, it } from "vitest";
import { createMockChatInstance } from "@chat-adapter/tests";
import { MyAdapter } from "./adapter";

describe("MyAdapter.handleWebhook", () => {
  it("dispatches incoming messages through processMessage", async () => {
    const chat = createMockChatInstance();
    const adapter = new MyAdapter({ /* config */ });
    await adapter.initialize(chat);

    const request = new Request("https://example.com/webhook", {
      method: "POST",
      body: JSON.stringify({ /* platform-specific payload */ }),
      headers: { "content-type": "application/json" },
    });
    const response = await adapter.handleWebhook(request);

    expect(response.status).toBe(200);
    expect(chat).toHaveDispatched("processMessage");
  });
});

Adapter-specific helpers

Helpers that depend on a specific platform's wire format (signed Slack webhooks, Teams claim builders, etc.) live in each adapter's own /testing subpath rather than in this kit, so adopting @chat-adapter/tests doesn't pull in adapter dependencies you don't use.

If you're contributing adapters or core to this repo, see the Testing adapters contributing guide for hand-rolled patterns used inside packages/.