---
title: Testing
description: "Test your bot handlers and custom adapters with @chat-adapter/tests, which provides Vitest factories, custom matchers, and a setup file."
docs_index: /llms.txt
lastUpdated: 2026-10-27
type: guide
prerequisites:
  - /docs/getting-started
related:
  - /docs/state-adapters
  - /docs/handling-events
  - /docs/contributing/testing
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

The [`@chat-adapter/tests`](https://www.npmjs.com/package/@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

```bash
pnpm add -D @chat-adapter/tests
```

`chat` and `vitest` are peer dependencies, so install them in your project if they aren't there already.

## Setup file (recommended)

Register all matchers automatically by adding the package's setup file to your Vitest config:

```typescript title="vitest.config.ts" lineNumbers
import { defineConfig } from "vitest/config";

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

Without the setup file, register matchers manually:

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

## Mock factories

```typescript
import {
  createMockAdapter,
  createMockChatInstance,
  createMockState,
  createTestMessage,
  mockLogger,
} from "@chat-adapter/tests";
```

| Factory                                   | Returns            | Notes                                                                                            |
| ----------------------------------------- | ------------------ | ------------------------------------------------------------------------------------------------ |
| `createMockAdapter(name?, overrides?)`    | `Adapter`          | Every method is `vi.fn()` with sensible defaults                                                 |
| `createMockChatInstance(options?)`        | `ChatInstance`     | Every `process*` handler is `vi.fn()`; `getState`/`getUserName`/`getLogger` wired up             |
| `createMockState()`                       | `MockStateAdapter` | In-memory `Map`s for subscriptions, locks, KV, lists, queues; `cache` exposes the underlying map |
| `createTestMessage(id, text, overrides?)` | `Message`          | Markdown text is parsed into the formatted AST                                                   |
| `mockLogger` / `createMockLogger()`       | `Logger`           | Shared default vs fresh-per-call                                                                 |

## Matchers

| Matcher                                                           | Asserts                                                                                              |
| ----------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `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:

```typescript title="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:

```typescript title="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](/docs/contributing/testing) for the hand-rolled patterns used inside `packages/`.

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)