Setting up a nestjs jest mock openai client is the fastest way to unit test LLM-backed services in NestJS without sending real requests or burning API quota. This guide walks through a complete, runnable pattern for mocking the OpenAI SDK inside the Nest dependency injection container, covering chat completions, streaming, and error paths.
Step 1: Isolate the OpenAI dependency in a NestJS provider
Hard-coding new OpenAI() inside a service makes the class impossible to test without a live network. Inject the client as a provider token so the test container can replace it. The service below depends only on the OpenAI class, keeping the API key and base URL at the module boundary.
import { Injectable } from '@nestjs/common';
import OpenAI from 'openai';
@Injectable()
export class ChatService {
constructor(private readonly openai: OpenAI) {}
async getReply(prompt: string): Promise<string> {
const res = await this.openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: prompt }],
});
return res.choices[0]?.message?.content ?? '';
}
}
Register it with a factory provider. This is the seam your tests will hook into.
import { Module } from '@nestjs/common';
import OpenAI from 'openai';
import { ChatService } from './chat.service';
@Module({
providers: [
ChatService,
{
provide: OpenAI,
useFactory: () => new OpenAI({ apiKey: process.env.OPENAI_API_KEY }),
},
],
exports: [ChatService],
})
export class ChatModule {}
If you later point the client at an OpenAI-compatible gateway, only the factory changes. The service and its tests stay put.
Step 2: Build a minimal nestjs jest mock openai client
Auto-mocking the whole openai package with jest.mock('openai') instantiates internal helpers and makes tests slower and noisier. A narrow fake that implements just the methods you call is cleaner and explicit. Build a helper that returns the shape your service touches:
function createMockOpenAI(createFn: jest.Mock) {
return {
chat: {
completions: {
create: createFn,
},
},
} as unknown as OpenAI;
}
Wire it into the Nest testing module. This is the core of a nestjs jest mock openai client setup—no network, no SDK internals.
import { Test } from '@nestjs/testing';
import OpenAI from 'openai';
import { ChatService } from './chat.service';
describe('ChatService', () => {
let service: ChatService;
let mockCreate: jest.Mock;
beforeEach(async () => {
mockCreate = jest.fn();
const moduleRef = await Test.createTestingModule({
providers: [
ChatService,
{ provide: OpenAI, useValue: createMockOpenAI(mockCreate) },
],
}).compile();
service = moduleRef.get(ChatService);
});
afterEach(() => jest.clearAllMocks());
});
The as unknown as OpenAI cast tells TypeScript to treat the fake as the real client. It is deliberate; we are not testing the SDK, only our code against its contract.
Step 3: Write deterministic tests for request/response shaping
A unit test should lock down both the output and the exact call shape. Assert the model name, message array, and streaming flag if present. This catches prompt formatting regressions that a happy-path mock would hide.
it('returns content from a completion', async () => {
mockCreate.mockResolvedValue({
choices: [{ message: { content: 'hello' } }],
});
const reply = await service.getReply('hi');
expect(reply).toBe('hello');
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: 'hi' }],
}),
);
});
it('returns empty string when choices are missing', async () => {
mockCreate.mockResolvedValue({ choices: [] });
expect(await service.getReply('x')).toBe('');
});
it('propagates client errors', async () => {
mockCreate.mockRejectedValue(new Error('rate limited'));
await expect(service.getReply('x')).rejects.toThrow('rate limited');
});
The expect.objectContaining matcher is stricter than checking toHaveBeenCalled(), and it documents the contract your service guarantees to the upstream API.
Step 4: Mock streaming responses without a socket
Production chat features often stream tokens. The OpenAI SDK returns an async iterable when stream: true. Add a generator method to the service:
async *streamReply(prompt: string): AsyncGenerator<string> {
const stream = await this.openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: prompt }],
stream: true,
});
for await (const chunk of stream) {
yield chunk.choices[0]?.delta?.content ?? '';
}
}
Mock the stream as a plain async generator. No timers, no events:
function fakeStream(chunks: string[]) {
async function* gen() {
for (const c of chunks) {
yield { choices: [{ delta: { content: c } }] };
}
}
return gen();
}
it('yields streamed tokens', async () => {
mockCreate.mockResolvedValue(fakeStream(['hello', ' world']));
const out: string[] = [];
for await (const token of service.streamReply('hi')) {
out.push(token);
}
expect(out.join('')).toBe('hello world');
});
This exercises your iteration and accumulation logic without a single byte leaving the process.
Step 5: Test retry or fallback logic if you own it
If your service implements retry or model fallback, drive those branches by queuing mock responses. Sequential mock rejection then resolution proves the loop works.
it('retries once on failure', async () => {
mockCreate
.mockRejectedValueOnce(new Error('timeout'))
.mockResolvedValueOnce({ choices: [{ message: { content: 'ok' } }] });
// assuming ChatService wraps the call with a retry
expect(await service.getReply('x')).toBe('ok');
expect(mockCreate).toHaveBeenCalledTimes(2);
});
If you delegate provider redundancy to a gateway, you can drop this code entirely. For example, n4n.ai exposes one OpenAI-compatible endpoint covering 240+ models with automatic fallback when a provider is degraded, so the client call is a single line and needs no local retry. Either way, the nestjs jest mock openai client pattern above stays identical because the surface area of the SDK call does not change.
Step 6: Keep mocks honest with slim interfaces
The as unknown as OpenAI cast is pragmatic but blind. In a larger codebase, define a minimal interface at the boundary and adapt the SDK once:
interface CompletionsClient {
create(params: Record<string, unknown>): Promise<any>;
}
// module provider
{ provide: 'CompletionsClient', useFactory: () => new OpenAI({ apiKey: process.env.OPENAI_API_KEY }) }
Your service depends on CompletionsClient, and your test provides a jest.fn() directly. TypeScript now fails the build if the real SDK removes chat.completions.create, turning silent contract drift into a compile error. This is stricter than a blanket nestjs jest mock openai client cast and scales better across teams.
Step 7: Run the suite and verify success
Execute the spec with Jest. The verbose flag shows each case:
npx jest chat.service.spec.ts --verbose
A green run shows passing specs for content extraction, empty choices, error propagation, streaming, and retry. To prove no real network calls slip through, run with an empty API key and add a guard in the factory that throws if invoked outside the test container. Coverage reports should show ChatService fully exercised while the openai package itself reports 0% (it is mocked everywhere).
The most common failure is TypeError: chat.completions.create is not a function. That means your fake omitted a nesting level—double-check the chat.completions.create path. Getting that shape right is 90% of a reliable nestjs jest mock openai client. Once it is solid, every downstream LLM feature can be tested in milliseconds.