v1.4.3These docs are for laramail v1.4.3. View changelog

Testing

Test email sending without SMTP, without mocks, without network. Mail.fake() intercepts all outgoing mail and gives you assertions that read like plain English.

beforeEach(() => Mail.fake());
afterEach(() => Mail.restore());

it('sends a welcome email on signup', async () => {
  await registerUser({ name: 'John', email: 'john@example.com' });

  Mail.assertSent(WelcomeEmail, (mail) => mail.hasTo('john@example.com'));
});

Why Mail.fake() over vi.fn() / jest.mock()

Every Node.js codebase ends up with some version of this:

jest.mock('nodemailer', () => ({
  createTransport: jest.fn().mockReturnValue({
    sendMail: jest.fn().mockResolvedValue({ messageId: 'test-id' }),
  }),
}));

It breaks in predictable ways:

  • Someone upgrades nodemailer and the mock no longer matches the real API
  • A new send call is added and the mock swallows it silently — tests still pass
  • You want to assert the right address and end up digging through sendMail.mock.calls[0][0].to
  • You switch providers and the entire mock layer needs to be rebuilt

Mail.fake() intercepts at the abstraction layer, not the transport layer. Your tests don't know or care what provider you're using.

BEFORE                                     AFTER
──────────────────────────────────────     ──────────────────────────────────────
jest.mock('nodemailer', ...)               Mail.fake()
sendMail.mock.calls[0][0].to              mail.hasTo('user@example.com')
no assertion on Mailable class            Mail.assertSent(WelcomeEmail)
breaks when provider changes              works with any provider

laramail/testing subpath

Import testing utilities from a dedicated subpath to keep fake mode out of your production bundle:

import { MailFake, AssertableMessage } from 'laramail/testing';

Works with all TypeScript moduleResolution settings (node, NodeNext, bundler).

The main import { Mail } from 'laramail' + Mail.fake() approach also works — use whichever fits your project.


Mail.fake() / Mail.restore()

Enable fake mode to intercept all emails instead of sending them. This is the primary testing pattern:

import { Mail } from 'laramail';

beforeEach(() => {
  Mail.fake();    // All emails will be stored, not sent
});

afterEach(() => {
  Mail.restore(); // Restore real mail system
});
MethodSignatureDescription
Mail.fake()Mail.fake(): MailFakeEnable fake mode. Returns the MailFake instance.
Mail.restore()Mail.restore(): voidRestore the real mail system (disable fake mode).
Mail.getFake()Mail.getFake(): MailFake | nullGet the current MailFake instance (or null if not faking).

Send Assertions

MethodSignatureDescription
assertSent()Mail.assertSent<T>(MailableClass, callback?)Assert a mailable was sent (optionally matching a condition)
assertSentCount()Mail.assertSentCount<T>(MailableClass, count)Assert a mailable was sent exactly N times
assertNotSent()Mail.assertNotSent<T>(MailableClass, callback?)Assert a mailable was NOT sent
assertNothingSent()Mail.assertNothingSent()Assert no emails were sent at all
// Assert WelcomeEmail was sent
Mail.assertSent(WelcomeEmail);

// Assert with conditions
Mail.assertSent(WelcomeEmail, (mail) => {
  return mail.hasTo('user@example.com') && mail.subjectContains('Welcome');
});

// Assert sent exactly 2 times
Mail.assertSentCount(WelcomeEmail, 2);

// Assert a specific mailable was NOT sent
Mail.assertNotSent(PasswordResetEmail);

// Assert nothing was sent at all
Mail.assertNothingSent();

Queue Assertions

MethodSignatureDescription
assertQueued()Mail.assertQueued<T>(MailableClass, callback?)Assert a mailable was queued
assertQueuedCount()Mail.assertQueuedCount<T>(MailableClass, count)Assert a mailable was queued exactly N times
assertNotQueued()Mail.assertNotQueued<T>(MailableClass, callback?)Assert a mailable was NOT queued
assertNothingQueued()Mail.assertNothingQueued()Assert nothing was queued
// Assert WelcomeEmail was queued
Mail.assertQueued(WelcomeEmail);

// Assert with conditions
Mail.assertQueued(WelcomeEmail, (mail) => mail.hasTo('user@example.com'));

// Assert queued exactly once
Mail.assertQueuedCount(WelcomeEmail, 1);

// Assert nothing was queued
Mail.assertNothingQueued();

Retrieval Methods

MethodSignatureDescription
sent()Mail.sent<T>(MailableClass?): AssertableMessage[]Get all sent messages (optionally filtered)
queued()Mail.queued<T>(MailableClass?): AssertableMessage[]Get all queued messages (optionally filtered)
hasSent()Mail.hasSent(): booleanCheck if any messages were sent
hasQueued()Mail.hasQueued(): booleanCheck if any messages were queued
// Get all sent WelcomeEmails
const messages = Mail.sent(WelcomeEmail);

// Inspect the first one
const first = messages[0];
console.log(first.getTo());      // ['user@example.com']
console.log(first.getSubject()); // 'Welcome, John!'

Failure Simulation

MethodSignatureDescription
simulateFailures()fake.simulateFailures(count: number)Simulate failures for the first N sends
resetFailures()fake.resetFailures()Clear failure simulation state
clear()fake.clear()Clear all sent/queued messages and reset failures
sentCount()fake.sentCount(): numberGet the number of sent messages
queuedCount()fake.queuedCount(): numberGet the number of queued messages
const fake = Mail.fake();

// First 2 sends will return { success: false }
fake.simulateFailures(2);

// Test failover behavior...

fake.resetFailures(); // Stop simulating failures
fake.clear();         // Reset all state

AssertableMessage

Returned by Mail.sent() and Mail.queued(). Provides methods to inspect message properties.

Recipients

MethodSignatureDescription
hasTo()hasTo(email: string): booleanCheck if message has a specific TO recipient
hasCc()hasCc(email: string): booleanCheck if message has a specific CC recipient
hasBcc()hasBcc(email: string): booleanCheck if message has a specific BCC recipient
getTo()getTo(): string[]Get all TO recipients
getCc()getCc(): string[]Get all CC recipients
getBcc()getBcc(): string[]Get all BCC recipients

Sender

MethodSignatureDescription
hasFrom()hasFrom(email: string): booleanCheck if message has a specific FROM address
getFrom()getFrom(): string | undefinedGet the FROM address
hasReplyTo()hasReplyTo(email: string): booleanCheck for a specific reply-to address

Subject

MethodSignatureDescription
hasSubject()hasSubject(subject: string): booleanExact subject match
subjectContains()subjectContains(text: string): booleanCase-insensitive substring match
getSubject()getSubject(): stringGet the subject

Content

MethodSignatureDescription
hasHtml()hasHtml(): booleanCheck if message has HTML content
htmlContains()htmlContains(text: string): booleanCheck if HTML contains a string
getHtml()getHtml(): string | undefinedGet the HTML content
hasText()hasText(): booleanCheck if message has plain text content
textContains()textContains(text: string): booleanCheck if plain text contains a string
getText()getText(): string | undefinedGet the plain text content

Attachments

MethodSignatureDescription
hasAttachments()hasAttachments(): booleanCheck if message has any attachments
hasAttachment()hasAttachment(filename: string): booleanCheck for a specific attachment by filename
getAttachments()getAttachments(): Attachment[]Get all attachments

Headers

MethodSignatureDescription
hasHeader()hasHeader(name: string, value?: string): booleanCheck for header (optionally with specific value)
getHeader()getHeader(name: string): string | undefinedGet a header value

Template

MethodSignatureDescription
hasTemplate()hasTemplate(template: string): booleanCheck if a specific template was used
getTemplate()getTemplate(): string | undefinedGet the template name
hasData()hasData(key: string, value?: unknown): booleanCheck for template data (optionally with specific value)
getData()getData(key: string): unknownGet a template data value
getAllData()getAllData(): Record<string, unknown> | undefinedGet all template data

Markdown

MethodSignatureDescription
isMarkdown()isMarkdown(): booleanCheck if message was built from markdown
getMarkdown()getMarkdown(): string | undefinedGet the raw markdown source
markdownContains()markdownContains(text: string): booleanCheck if markdown source contains a string

Failover

MethodSignatureDescription
wasFailoverUsed()wasFailoverUsed(): booleanCheck if failover was triggered
getProvider()getProvider(): string | undefinedGet the provider that actually sent the message
getFailoverAttempts()getFailoverAttempts(): FailoverDetail[]Get all failover attempt details
getResponse()getResponse(): MailResponse | undefinedGet the full MailResponse

Other

MethodSignatureDescription
getOptions()getOptions(): MailOptionsGet the underlying mail options
getMailable()getMailable(): Mailable | undefinedGet the mailable instance (if any)

Real-World Test Example

import { Mail, Mailable } from 'laramail';

class WelcomeEmail extends Mailable {
  constructor(public userName: string) {
    super();
  }

  build() {
    return this
      .subject(`Welcome, ${this.userName}!`)
      .html(`<h1>Hello ${this.userName}!</h1>`)
      .from('noreply@example.com');
  }
}

class PasswordResetEmail extends Mailable {
  constructor(public resetUrl: string) {
    super();
  }

  build() {
    return this
      .subject('Reset Your Password')
      .html(`<a href="${this.resetUrl}">Reset</a>`);
  }
}

describe('User Registration', () => {
  beforeEach(() => {
    Mail.fake();
  });

  afterEach(() => {
    Mail.restore();
  });

  it('sends welcome email on registration', async () => {
    // Application code that sends email
    await Mail.to('john@example.com').send(new WelcomeEmail('John'));

    // Assertions
    Mail.assertSent(WelcomeEmail);
    Mail.assertSent(WelcomeEmail, (mail) => {
      return mail.hasTo('john@example.com') &&
             mail.hasSubject('Welcome, John!') &&
             mail.hasFrom('noreply@example.com') &&
             mail.htmlContains('Hello John');
    });
    Mail.assertSentCount(WelcomeEmail, 1);
    Mail.assertNotSent(PasswordResetEmail);
  });

  it('does not send email when validation fails', async () => {
    // Code that doesn't send email
    Mail.assertNothingSent();
  });

  it('queues welcome email for background sending', async () => {
    await Mail.to('john@example.com').queue(new WelcomeEmail('John'));

    Mail.assertQueued(WelcomeEmail);
    Mail.assertQueued(WelcomeEmail, (mail) => mail.hasTo('john@example.com'));
    Mail.assertNothingSent(); // Queued, not sent
  });
});

Staging Redirect

Mail.fake() bypasses Mail.alwaysTo(). Test assertions always see the original recipients, not the staging redirect address — even if you have alwaysTo configured.

Mail.fake();
Mail.alwaysTo('staging@example.com'); // ignored in fake mode

await Mail.to('user@example.com').send(new WelcomeEmail());
Mail.assertSent(WelcomeEmail, (m) => m.hasTo('user@example.com')); // passes