01 Real seams 6 chapters

Theme 05 · Test-Confidence Oriented

Real seams

Explanation

Real seams

Plain Human Explanation

A seam is a planned boundary where a test can replace the outside world with something local and predictable. The goal is not to mock everything. The goal is to keep the product rule real while swapping only the slow, flaky, or expensive part.

For a password reset, the important behavior is “create a reset token and send one message.” A good seam lets the test prove that behavior without sending a real email or depending on the real clock.

Technical Explanation

Put external effects behind small interfaces: Clock, TokenGenerator, Mailer, PaymentGateway, Queue, or Store. Keep the domain decision in normal TypeScript, then pass the effectful dependencies into the workflow.

A good seam is narrow. It names exactly what the workflow needs instead of passing a whole framework object, environment object, or global client.

Why It Matters

  • User impact: tests can catch broken account, billing, or notification behavior before it reaches users.
  • Product behavior: the test still exercises the rule people care about, not only the test double.
  • Risk: tests that patch globals or mock private internals can pass while the real workflow is broken.
  • Decision point: add a seam when code touches time, randomness, network calls, queues, payments, email, storage, or another outside system.

The Core Move

Keep the behavior real and make the outside-world boundary explicit. Replace the boundary in tests, not the rule being tested.

Small Example

Real seams: Small Example

Bad TypeScript Example

import { sendEmail } from "./email";

export async function sendWelcomeEmail(user: { email: string }) {
  const subject = `Welcome - ${Date.now()}`;
  await sendEmail(user.email, subject);
}
import { sendEmail } from "./email";

export async function sendWelcomeEmail(user: {
  email: string;
}) {
  const subject = `Welcome - ${Date.now()}`;

  await sendEmail(user.email, subject);
}

Good TypeScript Example

type Clock = {
  now(): Date;
};

type Mailer = {
  send(to: string, subject: string): Promise<void>;
};

type WelcomeUser = {
  email: string;
};

export async function sendWelcomeEmail(user: WelcomeUser, mailer: Mailer, clock: Clock) {
  const sentAt = clock.now().toISOString().slice(0, 10);
  await mailer.send(user.email, `Welcome - ${sentAt}`);
}

const sentMessages: Array<{
  to: string;
  subject: string;
}> = [];
const testMailer: Mailer = {
  async send(to, subject) {
    sentMessages.push({ to, subject });
  },
};

await sendWelcomeEmail({ email: "dana@example.com" }, testMailer, { now: () => new Date("2026-01-02T00:00:00Z") });
type Clock = {
  now(): Date;
};

type Mailer = {
  send(
    to: string,
    subject: string,
  ): Promise<void>;
};

type WelcomeUser = {
  email: string;
};

export async function sendWelcomeEmail(
  user: WelcomeUser,
  mailer: Mailer,
  clock: Clock,
) {
  const sentAt = clock
    .now()
    .toISOString()
    .slice(0, 10);

  await mailer.send(
    user.email,
    `Welcome - ${sentAt}`,
  );
}

const sentMessages: Array<{
  to: string;
  subject: string;
}> = [];
const testMailer: Mailer = {
  async send(to, subject) {
    sentMessages.push({
      to,
      subject,
    });
  },
};

await sendWelcomeEmail(
  {
    email: "dana@example.com",
  },
  testMailer,
  {
    now: () =>
      new Date("2026-01-02T00:00:00Z"),
  },
);

What Changed

  • The bad version reaches into the real clock and email module directly.
  • The good version names the two outside-world dependencies the workflow needs.
  • Tests can keep the welcome-email behavior real while using a local mailer and fixed time.

Realistic Example

Real seams: Realistic Example

A password-reset flow needs random tokens, time, storage, and email. Without seams, tests either hit real services or mock so much that they stop proving the workflow.

Bad TypeScript Example

import { randomUUID } from "node:crypto";
import { db } from "./db";
import { emailClient } from "./email-client";

export async function requestPasswordReset(email: string) {
  const token = randomUUID();
  const expiresAt = new Date(Date.now() + 30 * 60 * 1000);

  await db.passwordResets.insert({ email, token, expiresAt });
  await emailClient.send(email, "Reset your password", `Token: ${token}`);

  return { ok: true };
}
import { randomUUID } from "node:crypto";
import { db } from "./db";
import { emailClient } from "./email-client";

export async function requestPasswordReset(
  email: string,
) {
  const token = randomUUID();

  const expiresAt = new Date(
    Date.now() + 30 * 60 * 1000,
  );

  await db.passwordResets.insert({
    email,
    token,
    expiresAt,
  });

  await emailClient.send(
    email,
    "Reset your password",
    `Token: ${token}`,
  );

  return {
    ok: true,
  };
}

Good TypeScript Example

type PasswordResetStore = {
  save(reset: { email: string; token: string; expiresAt: Date }): Promise<void>;
};

type TokenGenerator = {
  create(): string;
};

type Clock = {
  now(): Date;
};

type Mailer = {
  send(to: string, subject: string, body: string): Promise<void>;
};

type PasswordResetPorts = {
  store: PasswordResetStore;
  tokens: TokenGenerator;
  clock: Clock;
  mailer: Mailer;
};

export async function requestPasswordReset(email: string, ports: PasswordResetPorts) {
  const token = ports.tokens.create();
  const expiresAt = new Date(ports.clock.now().getTime() + 30 * 60 * 1000);

  await ports.store.save({ email, token, expiresAt });
  await ports.mailer.send(email, "Reset your password", `Token: ${token}`);

  return { ok: true, expiresAt };
}
type PasswordResetStore = {
  save(reset: {
    email: string;
    token: string;
    expiresAt: Date;
  }): Promise<void>;
};

type TokenGenerator = {
  create(): string;
};

type Clock = {
  now(): Date;
};

type Mailer = {
  send(
    to: string,
    subject: string,
    body: string,
  ): Promise<void>;
};

type PasswordResetPorts = {
  store: PasswordResetStore;
  tokens: TokenGenerator;
  clock: Clock;
  mailer: Mailer;
};

export async function requestPasswordReset(
  email: string,
  ports: PasswordResetPorts,
) {
  const token = ports.tokens.create();

  const expiresAt = new Date(
    ports.clock.now().getTime() +
      30 * 60 * 1000,
  );

  await ports.store.save({
    email,
    token,
    expiresAt,
  });

  await ports.mailer.send(
    email,
    "Reset your password",
    `Token: ${token}`,
  );

  return {
    ok: true,
    expiresAt,
  };
}

What Changed

  • The bad version hides the important boundaries behind imports and globals.
  • The good version makes randomness, time, persistence, and email visible at the workflow edge.
  • A test can use in-memory ports and still prove the real reset behavior: token saved, expiry calculated, message sent.

System Example

Real seams: System Example

At system scale, seams should separate product decisions from outside services. That keeps tests close to real behavior without requiring real providers.

Larger System-Level Bad TypeScript Example

export class TrialSignupService {
  constructor(private db: any, private stripe: any, private email: any) {}

  async startTrial(request: any) {
    const customer = await this.stripe.customers.create({ email: request.email });
    const trial = await this.stripe.subscriptions.create({
      customer: customer.id,
      trial_period_days: request.days || 14,
    });

    await this.db.users.insert({
      email: request.email,
      stripeCustomerId: customer.id,
      subscriptionId: trial.id,
    });

    await this.email.send(request.email, "Trial started", "Your trial is active.");
    return trial;
  }
}
export class TrialSignupService {
  constructor(
    private db: any,
    private stripe: any,
    private email: any,
  ) {}

  async startTrial(request: any) {
    const customer =
      await this.stripe.customers.create({
        email: request.email,
      });

    const trial =
      await this.stripe.subscriptions.create(
        {
          customer: customer.id,
          trial_period_days:
            request.days || 14,
        },
      );

    await this.db.users.insert({
      email: request.email,
      stripeCustomerId: customer.id,
      subscriptionId: trial.id,
    });

    await this.email.send(
      request.email,
      "Trial started",
      "Your trial is active.",
    );

    return trial;
  }
}

Larger System-Level Good TypeScript Example

type TrialSignup = {
  email: string;
  trialDays: number;
};

type BillingPort = {
  createTrial(input: TrialSignup): Promise<{
    customerId: string;
    subscriptionId: string;
  }>;
};

type UserStore = {
  saveTrialUser(user: { email: string; customerId: string; subscriptionId: string }): Promise<void>;
};

type Mailer = {
  send(to: string, subject: string, body: string): Promise<void>;
};

type TrialSignupPorts = {
  billing: BillingPort;
  users: UserStore;
  mailer: Mailer;
};

export async function startTrial(input: TrialSignup, ports: TrialSignupPorts) {
  const billing = await ports.billing.createTrial(input);

  await ports.users.saveTrialUser({
    email: input.email,
    customerId: billing.customerId,
    subscriptionId: billing.subscriptionId,
  });

  await ports.mailer.send(input.email, "Trial started", "Your trial is active.");
  return { ok: true, subscriptionId: billing.subscriptionId };
}
type TrialSignup = {
  email: string;
  trialDays: number;
};

type BillingPort = {
  createTrial(input: TrialSignup): Promise<{
    customerId: string;
    subscriptionId: string;
  }>;
};

type UserStore = {
  saveTrialUser(user: {
    email: string;
    customerId: string;
    subscriptionId: string;
  }): Promise<void>;
};

type Mailer = {
  send(
    to: string,
    subject: string,
    body: string,
  ): Promise<void>;
};

type TrialSignupPorts = {
  billing: BillingPort;
  users: UserStore;
  mailer: Mailer;
};

export async function startTrial(
  input: TrialSignup,
  ports: TrialSignupPorts,
) {
  const billing =
    await ports.billing.createTrial(input);

  await ports.users.saveTrialUser({
    email: input.email,
    customerId: billing.customerId,
    subscriptionId: billing.subscriptionId,
  });

  await ports.mailer.send(
    input.email,
    "Trial started",
    "Your trial is active.",
  );

  return {
    ok: true,
    subscriptionId: billing.subscriptionId,
  };
}

What Changed

  • The bad version makes the test depend on provider-specific clients and loose request data.
  • The good version exposes billing, storage, and email as explicit ports.
  • The workflow can be tested with local substitutes, while a smaller adapter test can verify the real billing client mapping.

When To Use It

Real seams: When To Use It

Use This When

  • The code touches time, randomness, email, queues, payments, storage, or network services.
  • A test needs realistic behavior but cannot safely call the real outside system.
  • The current test setup relies on patching globals, mocking private modules, or sleeping for timing.

Avoid This When

  • The code is already a pure calculation with simple inputs and outputs.
  • A one-line adapter is the only thing being tested.
  • The seam would be broader than the behavior it is meant to protect.

Tradeoffs

Real seams add a small amount of wiring. The payoff is tests that are faster and more trustworthy because they replace only the external dependency, not the product rule.

  • Integration tests
  • SQLite/local substitutes
  • Observable behavior over mocks/spies

Practice Prompt

Real seams: Practice Prompt

Beginner Exercise

Find one test that mocks time, randomness, email, or a provider module. Name the outside dependency the code actually needs.

Intermediate Exercise

Move that dependency behind a small interface and pass it into the workflow. Keep the product rule in the workflow, not in the test double.

Stretch Exercise

Split one provider-heavy flow into two tests: a workflow test using local ports, and a tiny adapter test that checks the real provider request shape.

Reflection Question

Which part of the behavior should stay real in the test, and which outside-world dependency should be replaced?

Suggest an edit

Leave a private editorial note. This creates a GitHub issue for this curriculum page.