LangChain Tutorial (TypeScript): mocking LLM calls in tests for intermediate developers

By Cyprian AaronsUpdated 2026-04-21
langchainmocking-llm-calls-in-tests-for-intermediate-developerstypescript

This tutorial shows how to write deterministic tests for LangChain TypeScript code by mocking LLM calls instead of hitting OpenAI or Anthropic in CI. You need this when your agent logic is correct but your tests are flaky, slow, or expensive because they depend on live model responses.

What You'll Need

  • Node.js 18+
  • TypeScript 5+
  • A test runner: jest or vitest
  • LangChain packages:
    • langchain
    • @langchain/openai
    • @langchain/core
  • If you want to run the real chain locally once:
    • OPENAI_API_KEY
  • Familiarity with:
    • RunnableSequence
    • prompt templates
    • basic async testing

Step-by-Step

  1. Start with a small chain that uses an LLM. Keep it simple so the test focuses on mocking the model, not on prompt plumbing.
// src/summarize.ts
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";

export function createSummaryChain() {
  const model = new ChatOpenAI({
    model: "gpt-4o-mini",
    temperature: 0,
  });

  const prompt = ChatPromptTemplate.fromMessages([
    ["system", "Summarize the user's message in one sentence."],
    ["human", "{text}"],
  ]);

  return prompt.pipe(model).pipe(new StringOutputParser());
}
  1. Extract the model creation behind a factory so tests can inject a fake implementation. This is the cleanest way to avoid patching internals or relying on network calls.
// src/summarize.ts
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { StringOutputParser } from "@langchain/core/output_parsers";

export function createSummaryChain(
  model: BaseChatModel = new ChatOpenAI({
    model: "gpt-4o-mini",
    temperature: 0,
  })
) {
  const prompt = ChatPromptTemplate.fromMessages([
    ["system", "Summarize the user's message in one sentence."],
    ["human", "{text}"],
  ]);

  return prompt.pipe(model).pipe(new StringOutputParser());
}
  1. Build a fake chat model for tests. In LangChain TS, extending BaseChatModel gives you a real runnable that behaves like a chat model but returns fixed output.
// test/fakeChatModel.ts
import {
  BaseChatModel,
} from "@langchain/core/language_models/chat_models";
import {
  AIMessageChunk,
  BaseMessage,
} from "@langchain/core/messages";
import { RunnableConfig } from "@langchain/core/runnables";

export class FakeSummaryModel extends BaseChatModel {
  _llmType() {
    return "fake-summary-model";
  }

  async _generate(
    messages: BaseMessage[],
    _options: this["ParsedCallOptions"],
    _runManager?: any
  ) {
    const last = messages[messages.length - 1]?.content ?? "";
    return {
      generations: [
        {
          text: `Mock summary for: ${String(last)}`,
          message: new AIMessageChunk(`Mock summary for: ${String(last)}`),
        },
      ],
      llmOutput: {},
    };
  }

  bind(_kwargs: Record<string, unknown>) {
    return this;
  }
}
  1. Write a test that injects the fake model and asserts on the exact output. The key point is that no API key is needed and no network request happens.
// test/summarize.test.ts
import { describe, expect, it } from "vitest";
import { createSummaryChain } from "../src/summarize";
import { FakeSummaryModel } from "./fakeChatModel";

describe("createSummaryChain", () => {
  it("returns a deterministic summary", async () => {
    const chain = createSummaryChain(new FakeSummaryModel());

    const result = await chain.invoke({
      text: "The customer submitted a claim after water damage in the kitchen.",
    });

    expect(result).toBe(
      "Mock summary for: The customer submitted a claim after water damage in the kitchen."
    );
  });
});
  1. If you also want to verify that your prompt formatting is correct, test the input before it reaches the fake model. This catches regressions where someone changes placeholders or system instructions and breaks downstream behavior.
// test/prompt.test.ts
import { describe, expect, it } from "vitest";
import { ChatPromptTemplate } from "@langchain/core/prompts";

describe("prompt formatting", () => {
  it("formats messages correctly", async () => {
    const prompt = ChatPromptTemplate.fromMessages([
      ["system", "Summarize the user's message in one sentence."],
      ["human", "{text}"],
    ]);

    const messages = await prompt.formatMessages({
      text: "Policy renewal is due next week.",
    });

    expect(messages[0].content).toBe(
      "Summarize the user's message in one sentence."
    );
    expect(messages[1].content).toBe("Policy renewal is due next week.");
  });
});

Testing It

Run your test suite with vitest or jest and confirm it passes without setting OPENAI_API_KEY. If you see network traffic or auth errors, you are still constructing a real ChatOpenAI inside the test path instead of injecting the fake model.

A good check is to temporarily disconnect your machine from the network and rerun the tests. They should still pass because all outputs are local and deterministic.

If you want stronger guarantees, add an assertion around runtime behavior too, such as ensuring your fake model was called with expected content or that specific prompts produce specific outputs.

Next Steps

  • Add structured output tests using JsonOutputParser and mocked models that return valid JSON strings.
  • Move from unit tests to contract tests by snapshotting prompts and tool-call payloads.
  • Learn how to mock multi-step agent graphs with injected fake tools and fake retrievers.

Keep learning

By Cyprian Aarons, AI Consultant at Topiax.

Want the complete 8-step roadmap?

Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.

Get the Starter Kit

Related Guides