EmailTestingTesting

Testing

Test email-sending code with an in-memory harness and Vitest matchers from @visulima/email

Testing

@visulima/email/test makes it easy to assert on email behavior without sending anything over the network. createTestEmail() gives you a ready Mail instance wired to an in-memory mock provider, plus helpers to inspect, wait on, and reset the captured outbox. A set of Vitest matchers (toHaveSentTo, toHaveSentWithSubject, and more) lets you write expressive assertions over what was sent.

import { createTestEmail, registerEmailMatchers } from "@visulima/email/test";

createTestEmail

createTestEmail() returns a test harness backed by the in-memory mock provider. Nothing leaves the process — every message is captured for assertion.

const email = createTestEmail();
// email.mail   -> a ready Mail instance, used exactly like a real one
// email.sent() -> captured entries, oldest first
// email.reset()-> clears the captured outbox
// email.waitFor(predicate, options) -> resolves with the first matching entry

Returned Harness

MemberDescription
mailA Mail instance wired to the mock provider. Use it exactly like a real one.
sent()Returns the captured messages, oldest first.
reset()Removes all captured messages.
waitFor(predicate, options?)Resolves with the first captured message matching predicate, polling until timeout.

waitFor accepts an options object with timeout (ms, default 1000) and interval (ms, default 10). It is useful when the send happens asynchronously — for example, after a queue worker processes a job:

const email = createTestEmail();

void someBackgroundJob(email.mail);

const message = await email.waitFor((entry) => entry.options.subject === "Welcome", {
    timeout: 2000,
    interval: 20,
});

Vitest Matchers

registerEmailMatchers(expect) extends Vitest's expect with email-specific matchers. The raw matcher map is also exported as emailMatchers for manual use.

MatcherDescription
toHaveSentTo(address)Asserts a message was sent to address (To, Cc, or Bcc; case-insensitive).
toHaveSentWithSubject(stringOrRegExp)Asserts a message's subject equals a string or matches a RegExp.
toHaveSentWithAttachment(filename?)Asserts a message carried an attachment, optionally with an exact filename.
toHaveSentMatching(predicateOrPartialOptions)Asserts a message matches a predicate over the options, or shallow-matches a partial.

The matchers assert against a TestEmail harness or a raw array of captured entries.

Register the matchers once in your test (or a shared setup file) and add a declare module "vitest" augmentation so the new matchers are fully typed:

import { beforeAll, expect } from "vitest";
import { createTestEmail, registerEmailMatchers } from "@visulima/email/test";

declare module "vitest" {
    interface Assertion<T = any> {
        toHaveSentTo(address: string): T;
        toHaveSentWithSubject(expected: string | RegExp): T;
        toHaveSentWithAttachment(filename?: string): T;
        toHaveSentMatching(matcher: Record<string, unknown> | ((options: any) => boolean)): T;
    }
    interface AsymmetricMatchersContaining {
        toHaveSentTo(address: string): void;
        toHaveSentWithSubject(expected: string | RegExp): void;
        toHaveSentWithAttachment(filename?: string): void;
        toHaveSentMatching(matcher: Record<string, unknown> | ((options: any) => boolean)): void;
    }
}

beforeAll(() => {
    registerEmailMatchers(expect);
});

Writing a Test

With the matchers registered, assert directly on the harness:

import { beforeAll, expect, it } from "vitest";
import { createTestEmail, registerEmailMatchers } from "@visulima/email/test";

beforeAll(() => {
    registerEmailMatchers(expect);
});

it("sends a welcome email", async () => {
    const email = createTestEmail();

    await email.mail.send({
        from: { email: "sender@example.com" },
        to: { email: "user@example.com" },
        subject: "Welcome",
        html: "<h1>Welcome!</h1>",
    });

    expect(email).toHaveSentTo("user@example.com");
    expect(email).toHaveSentWithSubject("Welcome");
    expect(email).toHaveSentWithSubject(/welcome/i);
});

Matching on Arbitrary Options

Use toHaveSentMatching with a partial options object or a predicate for assertions the dedicated matchers don't cover:

// Shallow-match a partial of the sent options.
expect(email).toHaveSentMatching({ subject: "Welcome" });

// Or pass a predicate.
expect(email).toHaveSentMatching((options) => options.to?.email === "user@example.com");

Inspecting and Resetting

sent() returns the raw captured entries for manual assertions, and reset() clears the outbox between tests:

import { afterEach } from "vitest";

const email = createTestEmail();

afterEach(() => {
    email.reset();
});

it("captures the outbox", async () => {
    await email.mail.send({ from: { email: "a@x.com" }, to: { email: "b@x.com" }, subject: "Hi", text: "yo" });

    const captured = email.sent();
    expect(captured).toHaveLength(1);
    expect(captured[0]?.options.subject).toBe("Hi");
});

Using the Matchers Manually

If you prefer not to register the matchers globally, pass the raw emailMatchers map to expect.extend(...) yourself:

import { expect } from "vitest";
import { emailMatchers } from "@visulima/email/test";

expect.extend(emailMatchers);
Support

Contribute to our work and keep us going

Community is the heart of open source. The success of our packages wouldn't be possible without the incredible contributions of users, testers, and developers who collaborate with us every day.Want to get involved? Here are some tips on how you can make a meaningful impact on our open source projects.

Ready to help us out?

Be sure to check out the package's contribution guidelines first. They'll walk you through the process on how to properly submit an issue or pull request to our repositories.

Submit a pull request

Found something to improve? Fork the repo, make your changes, and open a PR. We review every contribution and provide feedback to help you get merged.

Good first issues

Simple issues suited for people new to open source development, and often a good place to start working on a package.
View good first issues