Email SDK

Test email behavior

Assert sends, retries, fallbacks, and plugin 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

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

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

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

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

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

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

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

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

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

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

  • 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.

On this page