n4nAI

Mocking OpenAI API calls in Jest and pytest

Hands-on tutorial to mock OpenAI API calls in Jest and pytest. Write fast, deterministic CI tests for LLM integrations using real SDKs.

n4n Team3 min read576 words

Audio narration

Coming soon — every post will get a voice note here.

When your test suite depends on a live LLM endpoint, CI becomes slow and flaky. This tutorial shows you how to mock OpenAI API calls in Jest and pytest so your tests stay fast and deterministic while still exercising your real integration code. We’ll use the official SDKs and intercept at the right layer for both TypeScript and Python services.

Prerequisites

  • Node.js 18+ and npm installed.
  • Python 3.10+ with pip.
  • Official SDKs: openai for Node (v4+) and openai for Python (v1+).
  • Test runners: jest with ts-jest (if using TypeScript), and pytest with pytest-mock (or respx for HTTP mocking).
  • No API keys required to run the mocked tests.

Install the toolchain:

npm install openai jest ts-jest @types/jest typescript
pip install openai pytest pytest-mock respx

The code under test

We’ll write a small wrapper that sends a chat completion and extracts the message text. Keeping the SDK client injectable makes mocking trivial, but we’ll also show module-level mocking for cases where the client is a singleton.

TypeScript wrapper

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

const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

export async function summarize(text: string): Promise<string> {
  const resp = await client.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [{ role: 'user', content: `Summarize: ${text}` }],
    temperature: 0.2,
  });
  return resp.choices[0].message.content ?? '';
}

Python wrapper

# src/llm.py
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ.get("OPENAI_API_KEY"))

def summarize(text: str) -> str:
    resp = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{"role": "user", "content": f"Summarize: {text}"}],
        temperature=0.2,
    )
    return resp.choices[0].message.content or ""

If you point these clients at an OpenAI-compatible gateway such as n4n.ai by setting base_url, the mocking techniques below are identical because the request shape and SDK surface don’t change.

Mock OpenAI API in Jest

We’ll mock the openai module so new OpenAI() returns an object with a fake chat.completions.create. This avoids any network call and lets us assert on the prompt and options.

Test file

// src/llm.test.ts
import { summarize } from './llm';
import OpenAI from 'openai';

const mockCreate = jest.fn();

jest.mock('openai', () => {
  return {
    __esModule: true,
    default: jest.fn(() => ({
      chat: { completions: { create: mockCreate } },
    })),
  };
});

describe('summarize', () => {
  beforeEach(() => {
    mockCreate.mockReset();
  });

  it('returns content from the mocked API', async () => {
    mockCreate.mockResolvedValue({
      choices: [{ message: { content: 'A short summary.' } }],
      usage: { prompt_tokens: 10, completion_tokens: 3, total_tokens: 13 },
    });

    const result = await summarize('Long article about testing.');
    expect(result).toBe('A short summary.');
    expect(mockCreate).toHaveBeenCalledWith(
      expect.objectContaining({
        model: 'gpt-4o-mini',
        messages: [{ role: 'user', content: 'Summarize: Long article about testing.' }],
      })
    );
  });
});

Run with:

npx jest src/llm.test.ts

Expected output:

 PASS  src/llm.test.ts
  summarize
 returns content from the mocked API (3 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total

This pattern to mock OpenAI API calls in Jest and pytest keeps the test hermetic. You control the token count, latency, and error paths by returning different mocked values.

Mock OpenAI API in pytest

Python’s openai SDK is synchronous by default. We’ll patch the create method on the client instance. Using pytest-mock’s mocker fixture is the cleanest path.

Test with pytest-mock

# tests/test_llm.py
import pytest
from src.llm import summarize

def test_summarize(mocker):
    mock_create = mocker.patch("src.llm.client.chat.completions.create")
    mock_create.return_value = type(
        "Resp", (), {
            "choices": [type("Choice", (), {
                "message": type("Msg", (), {"content": "A short summary."})()
            })()],
            "usage": type("Usage", (), {
                "prompt_tokens": 10,
                "completion_tokens": 3,
                "total_tokens": 13,
            })(),
        }
    )()

    result = summarize("Long article about testing.")
    assert result == "A short summary."
    mock_create.assert_called_once()
    _, kwargs = mock_create.call_args
    assert kwargs["model"] == "gpt-4o-mini"
    assert "Summarize: Long article about testing." in kwargs["messages"][0]["content"]

Run:

pytest tests/test_llm.py -q

Expected output:

.                                                                [100%]
1 passed in 0.02s

HTTP-level mocking with respx

If you prefer to mock at the transport layer (useful for verifying retry or timeout logic), respx intercepts the HTTPX calls the SDK makes.

# tests/test_llm_respx.py
import respx
import httpx
from src.llm import summarize

@respx.mock
def test_summarize_respx():
    route = respx.post("https://api.openai.com/v1/chat/completions").mock(
        return_value=httpx.Response(
            200,
            json={
                "choices": [
                    {"message": {"content": "A short summary."}}
                ],
                "usage": {"prompt_tokens": 10, "completion_tokens": 3, "total_tokens": 13},
            },
        )
    )
    result = summarize("Long article about testing.")
    assert result == "A short summary."
    assert route.called

This also demonstrates how to mock OpenAI API calls in Jest and pytest when you cannot easily patch the client object—for example, when the SDK is instantiated inside a third-party library you don’t control.

Mocking streaming responses

Production code often uses stream=True (Python) or stream: true (Node). The SDK returns an async iterator of chunks. Mock it as a generator or async iterable.

Python streaming mock

def test_stream(mocker):
    def fake_stream(**kwargs):
        chunks = [
            type("C", (), {"choices": [type("Ch", (), {"delta": type("D", (), {"content": "Hello"})()})()]})(),
            type("C", (), {"choices": [type("Ch", (), {"delta": type("D", (), {"content": " world"})()})()]})(),
        ]
        for c in chunks:
            yield c

    mocker.patch("src.llm.client.chat.completions.create", side_effect=fake_stream)
    # Your stream_summarize would concatenate delta.content fields

Jest streaming mock

mockCreate.mockResolvedValue({
  async *[Symbol.asyncIterator]() {
    yield { choices: [{ delta: { content: 'Hello' } }] };
    yield { choices: [{ delta: { content: ' world' } }] };
  },
});

Mocking errors and rate limits

Resilience logic is the main reason to mock. Simulate a RateLimitError to confirm your retry or fallback works.

In Jest:

mockCreate.mockRejectedValue(new Error('Rate limited'));

In pytest:

mocker.patch(
    "src.llm.client.chat.completions.create",
    side_effect=Exception("Rate limited")
)

If your gateway supports automatic fallback when a provider is degraded, you can mock the first call to throw and the second to succeed, then assert your wrapper retries or switches models. The same applies when you use a client that honors routing directives—mock the create method to inspect the model field your code passes.

CI considerations

  • Never let tests hit the real API in CI. Set OPENAI_API_KEY to a dummy value and rely on mocks.
  • For Jest, add a jest.config.js with resetMocks: true to avoid cross-test leakage.
  • For pytest, use a conftest.py to set env vars and auto-load mocking fixtures.
  • Include usage objects in mocks to test token metering and logging without a live call.
  • Keep mock response shapes in sync with the SDK version. The openai packages change response types; a typed mock catches drift at compile time in TS and via pytest type checks.

Deterministic tests make refactoring LLM integration code safe. The patterns above give you full control over request and response shapes without leaving your process, and they apply unchanged to any OpenAI-compatible endpoint.

Tagsmockingtestingjestpytest

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All testing & mocking llm apis in ci posts →