---
title: Testing
description: Test your bot handlers and custom adapters with @chat-adapter/tests — Vitest factories, custom matchers, and a setup file.
type: guide
prerequisites:
  - /docs/getting-started
related:
  - /docs/state
  - /docs/handling-events
  - /docs/contributing/testing
---

# Testing



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 — they should already be in your project.

## Setup file (recommended)

Auto-register all matchers 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` (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:

```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

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:

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