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.
