# Test email behavior (/docs/guides/test-email-behavior)



Use the memory provider when you want to replace the provider. Use the capture plugin when you want to observe the normal send pipeline.

## Memory provider [#memory-provider]

The memory provider stores successful sends and never calls an external API.

```ts
import { describe, expect, test } from "bun:test";
import { createEmailClient } from "@opencoredev/email-sdk";
import { memoryProvider } from "@opencoredev/email-sdk/testing";

describe("welcome email", () => {
  test("sends the expected subject", async () => {
    const memory = memoryProvider();
    const email = createEmailClient({
      adapters: [memory],
    });

    await email.send({
      from: "test@example.com",
      to: "user@example.com",
      subject: "Welcome",
      text: "Hello",
    });

    expect(memory.raw.sent[0]?.message.subject).toBe("Welcome");
  });
});
```

## Capture plugin [#capture-plugin]

The capture plugin records lifecycle events. It can be used with the memory provider or with a custom test adapter.

```ts
import { capturePlugin } from "@opencoredev/email-sdk/plugins/capture";

const memory = memoryProvider();
const email = createEmailClient({
  adapters: [memory],
  plugins: [capturePlugin()],
});

await email.send({
  from: "test@example.com",
  to: "user@example.com",
  subject: "Welcome",
  text: "Hello",
});

expect(email.capture.events.map((event) => event.type)).toEqual(["beforeSend", "afterSend"]);
```

## Assert defaults [#assert-defaults]

Defaults run before message validation, so tests can assert the final message that reached the provider.

```ts
import { defaultsPlugin } from "@opencoredev/email-sdk/plugins/defaults";

const memory = memoryProvider();
const email = createEmailClient({
  adapters: [memory],
  plugins: [
    defaultsPlugin({
      headers: { "X-App": "billing" },
      sendMetadata: { service: "billing" },
    }),
  ],
});

await email.send(
  {
    from: "billing@example.com",
    to: "user@example.com",
    subject: "Receipt",
    text: "Thanks.",
  },
  {
    metadata: { route: "receipt" },
  },
);

expect(memory.raw.sent[0]?.message.headers).toMatchObject({ "X-App": "billing" });
```

## Assert fallback [#assert-fallback]

Use `failingProvider()` for the primary route and `memoryProvider()` for the backup route. Assert the response provider and the backup send store.

```ts
import { failingProvider, memoryProvider } from "@opencoredev/email-sdk/testing";

const backup = memoryProvider("backup");
const email = createEmailClient({
  adapters: [failingProvider("primary"), backup],
  fallback: ["backup"],
});

const response = await email.send({
  from: "test@example.com",
  to: "user@example.com",
  subject: "Fallback",
  text: "Hello",
});

expect(response.provider).toBe("backup");
expect(backup.raw.sent).toHaveLength(1);
```

## Assert retry [#assert-retry]

Use a provider that fails once with a retryable error, then succeeds.

```ts
import { EmailProviderError, createEmailClient } from "@opencoredev/email-sdk";
import { capturePlugin } from "@opencoredev/email-sdk/plugins/capture";

let attempts = 0;
const retrying = {
  name: "retrying",
  send() {
    attempts += 1;

    if (attempts === 1) {
      throw new EmailProviderError("Temporary failure", {
        provider: "retrying",
        retryable: true,
      });
    }

    return { provider: "retrying", id: "ok" };
  },
};

const email = createEmailClient({
  adapters: [retrying],
  retry: { retries: 1, delay: () => 0 },
  plugins: [capturePlugin()],
});

const response = await email.send({
  from: "test@example.com",
  to: "user@example.com",
  subject: "Retry",
  text: "Hello",
});

expect(response.provider).toBe("retrying");
expect(attempts).toBe(2);
expect(email.capture.events.map((event) => event.type)).toContain("retry");
```

## Test checklist [#test-checklist]

* Use `memoryProvider()` for app tests that should not call real providers.
* Use `capturePlugin()` when you need lifecycle events.
* Assert provider field support with adapter unit tests.
* Add one test for fallback if the route matters.
* Test fallback routes with the same message shape used in production.
* Add one test for plugin ordering when multiple plugins mutate the same message.
