Testing

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, so install them in your project if they aren't there already.

Register all matchers automatically 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. This matcher is async, so use 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 can't be matched by text. For those, assert without textPattern and inspect mock.calls directly.

Bot authors: test your handlers

If you're building a bot on Chat SDK, you can exercise your handlers with a mock adapter and mock state instead of real platform webhooks:

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

If you're building a custom Adapter, pass it a mock ChatInstance 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 one platform's wire format, such as signed Slack webhooks or Teams claim builders, live in each adapter's own /testing subpath rather than in this package. That way, 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 the hand-rolled patterns used inside packages/.

Read more