Testing & Mocking
Meloqui provides testing utilities to help you write reliable tests for your LLM-powered applications without making real API calls.
Quick Start
typescript
import { ChatClient, createMockProvider } from 'meloqui';
const provider = createMockProvider();
const client = new ChatClient({ provider, model: 'test' });
// Test your code
const response = await client.chat('Hello');
expect(response.content).toBe('Mock response');
// Verify calls
expect(provider.chat).toHaveBeenCalledTimes(1);Mock Provider Functions
createMockProvider
Creates a fully-mocked provider with jest mock functions.
typescript
import { createMockProvider } from 'meloqui';
const provider = createMockProvider();
// All methods are jest mocks
expect(jest.isMockFunction(provider.chat)).toBe(true);
expect(jest.isMockFunction(provider.stream)).toBe(true);
// Default response
const response = await provider.chat([{ role: 'user', content: 'Hi' }]);
// { content: 'Mock response', role: 'assistant', metadata: { model: 'mock-model', tokensUsed: 10 } }Options
typescript
interface MockProviderOptions {
/** Provider name (default: 'mock') */
name?: string;
/** Provider capabilities */
capabilities?: Partial<ProviderCapabilities>;
/** Custom chat mock */
chat?: jest.Mock;
/** Custom stream mock */
stream?: jest.Mock;
/** Custom chatWithTools mock */
chatWithTools?: jest.Mock;
/** Custom streamWithTools mock */
streamWithTools?: jest.Mock;
}Custom Capabilities
typescript
// Disable specific capabilities for testing fallbacks
const provider = createMockProvider({
capabilities: {
vision: false,
streaming: false
}
});
expect(provider.capabilities.vision).toBe(false);
expect(provider.capabilities.chat).toBe(true); // Default preservedcreateMockProviderWithResponse
Creates a provider that returns a specific response.
typescript
import { createMockProviderWithResponse } from 'meloqui';
const provider = createMockProviderWithResponse({
content: 'Paris is the capital of France.',
role: 'assistant',
metadata: { model: 'gpt-4o', tokensUsed: 15 }
});
const client = new ChatClient({ provider, model: 'test' });
const response = await client.chat('What is the capital of France?');
expect(response.content).toBe('Paris is the capital of France.');createMockProviderWithError
Creates a provider that throws an error. Useful for testing error handling.
typescript
import { createMockProviderWithError, RateLimitError } from 'meloqui';
const provider = createMockProviderWithError(
new RateLimitError('Rate limit exceeded', 'openai')
);
const client = new ChatClient({ provider, model: 'test' });
await expect(client.chat('Hello')).rejects.toThrow(RateLimitError);createMockProviderWithStream
Creates a provider that streams specific chunks.
typescript
import { createMockProviderWithStream } from 'meloqui';
const provider = createMockProviderWithStream([
{ content: 'Hello', role: 'assistant' },
{ content: ' world', role: 'assistant' },
{ content: '!', role: 'assistant' }
]);
const client = new ChatClient({ provider, model: 'test' });
const chunks: string[] = [];
for await (const chunk of client.stream('Hi')) {
chunks.push(chunk.content);
}
expect(chunks).toEqual(['Hello', ' world', '!']);Testing Patterns
Basic Chat Testing
typescript
import { ChatClient, createMockProvider } from 'meloqui';
describe('MyService', () => {
let client: ChatClient;
let provider: ReturnType<typeof createMockProvider>;
beforeEach(() => {
provider = createMockProvider();
client = new ChatClient({ provider, model: 'test' });
});
it('should send user message', async () => {
await client.chat('Hello, AI!');
expect(provider.chat).toHaveBeenCalledWith(
[{ role: 'user', content: 'Hello, AI!' }],
undefined
);
});
it('should include system prompt', async () => {
const clientWithSystem = new ChatClient({
provider,
model: 'test',
systemPrompt: 'You are helpful.'
});
await clientWithSystem.chat('Hi');
expect(provider.chat).toHaveBeenCalledWith(
expect.arrayContaining([
{ role: 'system', content: 'You are helpful.' }
]),
undefined
);
});
});Testing Different Responses
typescript
it('should handle dynamic responses', async () => {
provider.chat
.mockResolvedValueOnce({ content: 'First', role: 'assistant' })
.mockResolvedValueOnce({ content: 'Second', role: 'assistant' });
const first = await client.chat('1');
const second = await client.chat('2');
expect(first.content).toBe('First');
expect(second.content).toBe('Second');
});
it('should respond based on input', async () => {
provider.chat.mockImplementation(async (messages) => {
const lastMessage = messages[messages.length - 1];
if (lastMessage.content.includes('hello')) {
return { content: 'Hi there!', role: 'assistant' };
}
return { content: 'I don\'t understand', role: 'assistant' };
});
expect((await client.chat('hello')).content).toBe('Hi there!');
expect((await client.chat('xyz')).content).toBe('I don\'t understand');
});Testing Error Handling
typescript
import {
ChatClient,
createMockProviderWithError,
RateLimitError,
AuthenticationError,
ChatError
} from 'meloqui';
describe('error handling', () => {
it('should handle rate limit errors', async () => {
const provider = createMockProviderWithError(
new RateLimitError('Too many requests', 'openai')
);
const client = new ChatClient({ provider, model: 'test' });
try {
await client.chat('Hello');
fail('Expected error');
} catch (error) {
expect(error).toBeInstanceOf(RateLimitError);
expect((error as ChatError).isRetryable).toBe(true);
}
});
it('should handle authentication errors', async () => {
const provider = createMockProviderWithError(
new AuthenticationError('Invalid API key', 'openai')
);
const client = new ChatClient({ provider, model: 'test' });
await expect(client.chat('Hello')).rejects.toThrow(AuthenticationError);
});
it('should retry on transient errors', async () => {
const provider = createMockProvider();
provider.chat
.mockRejectedValueOnce(new RateLimitError('Rate limited', 'openai'))
.mockResolvedValueOnce({ content: 'Success', role: 'assistant' });
const client = new ChatClient({
provider,
model: 'test',
retryConfig: { maxAttempts: 2, initialBackoffMs: 10 }
});
const response = await client.chat('Hello');
expect(response.content).toBe('Success');
expect(provider.chat).toHaveBeenCalledTimes(2);
});
});Testing Streaming
typescript
import { ChatClient, createMockProviderWithStream } from 'meloqui';
describe('streaming', () => {
it('should stream response chunks', async () => {
const provider = createMockProviderWithStream([
{ content: 'The ', role: 'assistant' },
{ content: 'answer ', role: 'assistant' },
{ content: 'is 42.', role: 'assistant' }
]);
const client = new ChatClient({ provider, model: 'test' });
const chunks: string[] = [];
for await (const chunk of client.stream('Question')) {
chunks.push(chunk.content);
}
expect(chunks.join('')).toBe('The answer is 42.');
});
it('should handle stream errors', async () => {
const provider = createMockProviderWithError(new Error('Stream failed'));
const client = new ChatClient({ provider, model: 'test' });
await expect(async () => {
for await (const _chunk of client.stream('Hello')) {
// Should throw
}
}).rejects.toThrow('Stream failed');
});
});Testing with Tools
typescript
import { ChatClient, createMockProvider, ChatResponse } from 'meloqui';
import { z } from 'zod';
describe('tool calling', () => {
it('should handle tool calls', async () => {
const provider = createMockProvider();
// Mock a response with tool call
const toolCallResponse: ChatResponse = {
content: '',
role: 'assistant',
toolCalls: [{
id: 'call_123',
name: 'get_weather',
arguments: { city: 'Paris' }
}]
};
provider.chatWithTools.mockResolvedValue(toolCallResponse);
const client = new ChatClient({ provider, model: 'test' });
const getWeather = {
name: 'get_weather',
description: 'Get weather for a city',
parameters: z.object({ city: z.string() }),
execute: jest.fn().mockResolvedValue({ temp: 20, condition: 'sunny' })
};
await client.chat('What is the weather in Paris?', {
tools: [getWeather]
});
expect(getWeather.execute).toHaveBeenCalledWith({ city: 'Paris' });
});
});Testing Conversation History
typescript
import { ChatClient, createMockProvider, InMemoryStorage } from 'meloqui';
describe('conversation history', () => {
it('should maintain history across messages', async () => {
const provider = createMockProvider();
const client = new ChatClient({
provider,
model: 'test',
storage: new InMemoryStorage(),
conversationId: 'test-conv'
});
await client.chat('First message');
await client.chat('Second message');
// Second call should include first message in history
expect(provider.chat).toHaveBeenLastCalledWith(
expect.arrayContaining([
expect.objectContaining({ content: 'First message' }),
expect.objectContaining({ content: 'Mock response' }),
expect.objectContaining({ content: 'Second message' })
]),
undefined
);
});
});Testing Capability Checks
typescript
import { ChatClient, createMockProvider, CapabilityError } from 'meloqui';
describe('capabilities', () => {
it('should check vision capability', async () => {
const provider = createMockProvider({
capabilities: { vision: false }
});
const client = new ChatClient({ provider, model: 'test' });
expect(client.capabilities.vision).toBe(false);
});
it('should throw on unsupported capability', async () => {
const provider = createMockProvider({
capabilities: { vision: false }
});
const client = new ChatClient({ provider, model: 'test' });
await expect(
client.chat({
role: 'user',
content: [{ type: 'image', image: './photo.jpg' }]
})
).rejects.toThrow(CapabilityError);
});
});Integration Testing
For integration tests that need real API calls, use environment variables and skip conditions:
typescript
describe('OpenAI Integration', () => {
const apiKey = process.env.OPENAI_API_KEY;
beforeAll(() => {
if (!apiKey) {
console.log('Skipping integration tests: OPENAI_API_KEY not set');
}
});
it('should complete chat', async () => {
if (!apiKey) return;
const client = new ChatClient({
provider: 'openai',
model: 'gpt-4o-mini',
apiKey
});
const response = await client.chat('Say "test" and nothing else');
expect(response.content.toLowerCase()).toContain('test');
});
});Best Practices
1. Use Fresh Mocks Per Test
typescript
beforeEach(() => {
provider = createMockProvider();
client = new ChatClient({ provider, model: 'test' });
});2. Type Your Mocks
typescript
import { MockedProviderPlugin } from 'meloqui';
let provider: MockedProviderPlugin;
// provider.chat.mock is now typed3. Test Edge Cases
typescript
it('should handle empty response', async () => {
provider.chat.mockResolvedValue({ content: '', role: 'assistant' });
const response = await client.chat('Hi');
expect(response.content).toBe('');
});
it('should handle very long response', async () => {
const longContent = 'x'.repeat(100000);
provider.chat.mockResolvedValue({ content: longContent, role: 'assistant' });
const response = await client.chat('Hi');
expect(response.content.length).toBe(100000);
});4. Verify Call Arguments
typescript
it('should pass options correctly', async () => {
await client.chat('Hi', { temperature: 0.5, maxTokens: 100 });
expect(provider.chat).toHaveBeenCalledWith(
expect.any(Array),
expect.objectContaining({
temperature: 0.5,
maxTokens: 100
})
);
});Next Steps
- Error Handling - Testing error scenarios
- Streaming - Testing streaming responses
- Tools & Agents - Testing tool calling
