llmao

How to mock the OpenAI SDK in Jest

Tests that call the real OpenAI API are slow, flaky, need an API key in CI and cost money. Hand-written mocks (jest.fn() returning a fake completion) have to fake the whole response shape, and they drift every time the SDK changes.

llmao is a fake LLM that behaves like the real API: same client, same response objects, same errors. You swap it in with one line and your app code stays untouched.

Install

npm install --save-dev llmao

Mock the module

Say your app has a function that uses the official SDK:

// src/support.ts
import OpenAI from 'openai';

const client = new OpenAI();

export async function classify(ticket: string) {
  const completion = await client.chat.completions.create({
    model: 'gpt-4o',
    messages: [
      { role: 'system', content: 'Classify the ticket as billing, shipping or other.' },
      { role: 'user', content: ticket },
    ],
  });
  return completion.choices[0].message.content;
}

In the test, replace openai with llmao/openai and script the answers:

// src/support.test.ts
jest.mock('openai', () => require('llmao/openai'));

import OpenAI from 'openai'; // this is llmao now
import * as llmao from 'llmao/testing';
import { classify } from './support';

beforeEach(() => llmao.reset());

test('classifies shipping tickets', async () => {
  llmao.configure({ script: [{ when: /never arrived/i, text: 'shipping' }] });

  expect(await classify('My order never arrived')).toBe('shipping');
});

test('sends the ticket with the right system prompt', async () => {
  llmao.configure({ script: [{ text: 'billing' }] });
  await classify('I was charged twice');

  expect(llmao.lastCall()).toMatchObject({
    model: 'gpt-4o',
    system: 'Classify the ticket as billing, shipping or other.',
    prompt: 'I was charged twice',
  });
});

test('surfaces rate limits', async () => {
  llmao.configure({ failures: { rateLimit: 1 } });
  await expect(classify('hi')).rejects.toBeInstanceOf(OpenAI.RateLimitError);
});

What you get

Scripting a whole conversation

llmao.configure({
  unscripted: 'error',
  script: [
    { when: 'weather', toolCalls: [{ name: 'get_weather', args: { city: 'Lima' } }] },
    { when: 'weather', afterToolResults: true, text: 'It is sunny in Lima.' },
    { error: 'server_error', once: true }, // fail once, then fall through
  ],
});

Related