A complete testing framework for streaming AI applications that tests AI logic directly using AI SDK's built-in mock providers instead of complex HTTP mocking.
HTTP Request → Hono Web Server → AI SDK (streamText/generateObject) → MockLanguageModelV1
Instead of mocking HTTP requests to OpenAI, we inject mock models directly into AI SDK calls for cleaner, more reliable testing.
# Install dependencies
npm install
# Set up environment (optional - mocks work without API key)
echo "OPENAI_API_KEY=your-key-here" > .env
# Run all tests
npm test
# Start development server
npm run devai-testing-hono/
├── app.js # Hono app with dependency injection
├── app.test.js # Complete test suite with AI SDK mocks
├── package.json
├── vitest.config.js
└── README.md
// Factory function for dependency injection
export function createApp(options = {}) {
const app = new Hono()
// Use provided models or default to OpenAI
const chatModel = options.chatModel || openai('gpt-4o')
const objectModel = options.objectModel || openai('gpt-4o')
app.post('/chat', async (c) => {
const result = await streamText({
model: chatModel, // Injected model
prompt: message,
})
return result.toDataStreamResponse()
})
return app
}
// Default export for production
export default createApp()Key Innovation: Factory function allows injecting mock models for testing while using real OpenAI in production.
import { MockLanguageModelV1, simulateReadableStream } from 'ai/test'
import { createApp } from './app.js'
test('should stream creative chat responses', async () => {
const mockModel = new MockLanguageModelV1({
doStream: async () => ({
stream: simulateReadableStream({
chunks: [
{ type: 'text-delta', textDelta: 'Once ' },
{ type: 'text-delta', textDelta: 'upon ' },
{ type: 'text-delta', textDelta: 'a ' },
{ type: 'text-delta', textDelta: 'time' },
{
type: 'finish',
finishReason: 'stop',
usage: { completionTokens: 4, promptTokens: 3 },
},
],
}),
rawCall: { rawPrompt: null, rawSettings: {} },
}),
})
const app = createApp({ chatModel: mockModel })
const response = await app.request('/chat', {
method: 'POST',
body: JSON.stringify({ message: 'Tell me a creative story' }),
headers: { 'Content-Type': 'application/json' }
})
expect(response.status).toBe(200)
const fullResponse = await streamToText(response)
expect(fullResponse).toContain('Once')
})What it does: Uses AI SDK's testing utilities to inject mock models that return predefined responses in the correct AI SDK format.
✓ Health Check > should return health status
✓ AI Tests > should stream creative chat responses
✓ AI Tests > should handle simple greetings
✓ AI Tests > should handle tool calling
✓ AI Tests > should generate structured profiles
✓ AI Tests > should handle malformed JSON
✓ AI Tests > should handle missing message content
✓ AI Tests > should handle missing prompt for profile generation
Test Files 1 passed (1)
Tests 8 passed (8)- Factory function
createApp(options)accepts models as parameters - Production:
createApp()uses real OpenAI models - Testing:
createApp({ chatModel: mockModel })uses mocks - Clean separation between business logic and AI provider
- Use
MockLanguageModelV1fromai/testpackage - No HTTP mocking complexity - test at the right abstraction level
- AI SDK handles all format conversion automatically
- Built-in streaming simulation with proper chunk timing
- Real HTTP streaming endpoints using Hono
- AI SDK
streamTextwithtoDataStreamResponse() - Mock providers return proper AI SDK stream format
- Automatic word-by-word streaming simulation
const mockModel = new MockLanguageModelV1({
doStream: async () => ({
stream: simulateReadableStream({
chunks: [
{
type: 'tool-call',
toolCallType: 'function',
toolCallId: 'call_1',
toolName: 'getWeather',
args: { location: 'San Francisco, CA' },
},
{ type: 'text-delta', textDelta: 'The weather is 72°F' },
{ type: 'finish', finishReason: 'stop' },
],
}),
rawCall: { rawPrompt: null, rawSettings: {} },
}),
})const mockModel = new MockLanguageModelV1({
defaultObjectGenerationMode: 'json', // Required for generateObject
doGenerate: async () => ({
text: JSON.stringify({
name: 'Luna Martinez',
age: 28,
occupation: 'Digital Artist'
}),
usage: { completionTokens: 50, promptTokens: 10 },
finishReason: 'stop',
rawCall: { rawPrompt: null, rawSettings: {} },
}),
})- JSON parsing errors caught and returned as 400
- Missing required fields validated
- AI SDK errors caught and returned as 500
- Framework limitations handled gracefully
Streams AI text responses chunk by chunk
curl -X POST http://localhost:3000/chat \
-H "Content-Type: application/json" \
-d '{"message": "Tell me a creative story"}'Streams AI responses with tool calling support
curl -X POST http://localhost:3000/chat-with-tools \
-H "Content-Type: application/json" \
-d '{"message": "What is the weather like?"}'Returns structured user profiles
curl -X POST http://localhost:3000/generate-profile \
-H "Content-Type": "application/json" \
-d '{"prompt": "Generate a creative artist profile"}'| Aspect | HTTP Mocking (MSW) | AI SDK Mocks |
|---|---|---|
| Abstraction Level | Network layer | Business logic layer |
| Code Complexity | 200+ lines | 50+ lines |
| Format Knowledge | Must know OpenAI JSON | Simple object structure |
| Maintenance | Breaks when API changes | Future-proof |
| Test Focus | HTTP transport details | AI application logic |
| Setup Complexity | Global server setup | Per-test model injection |
Before (MSW):
// Complex HTTP response crafting
function createOpenAIResponse(content, isStreaming) {
if (isStreaming) {
const chunks = content.split(' ').map(word =>
`data: {"choices":[{"delta":{"content":"${word} "}}]}\n\n`
).join('') + 'data: [DONE]\n\n'
return new HttpResponse(chunks, {
headers: { 'Content-Type': 'text/plain; charset=utf-8' }
})
}
// ... 50+ more lines
}After (AI SDK Mocks):
// Simple AI SDK format
const mockModel = new MockLanguageModelV1({
doStream: async () => ({
stream: simulateReadableStream({
chunks: [
{ type: 'text-delta', textDelta: 'Hello ' },
{ type: 'text-delta', textDelta: 'world' },
{ type: 'finish', finishReason: 'stop' },
],
}),
rawCall: { rawPrompt: null, rawSettings: {} },
}),
})- Test the right abstraction level - Mock AI models, not HTTP transport
- Dependency injection enables clean testing without global state
- AI SDK handles complexity - No manual format crafting required
- Framework limitations are OK - Accept realistic error behavior
- Business logic focus - Test what your app actually does with AI responses
const mockModel = new MockLanguageModelV1({
doStream: async () => ({
stream: simulateReadableStream({
chunks: [
{ type: 'text-delta', textDelta: 'Your ' },
{ type: 'text-delta', textDelta: 'response ' },
{ type: 'text-delta', textDelta: 'here' },
{ type: 'finish', finishReason: 'stop', usage: { ... } },
],
}),
rawCall: { rawPrompt: null, rawSettings: {} },
}),
})const mockModel = new MockLanguageModelV1({
doStream: async () => ({
stream: simulateReadableStream({
chunks: [
{
type: 'tool-call',
toolCallType: 'function',
toolCallId: 'call_1',
toolName: 'getWeather',
args: { location: 'San Francisco' },
},
{ type: 'text-delta', textDelta: 'Based on the weather data...' },
{ type: 'finish', finishReason: 'stop' },
],
}),
rawCall: { rawPrompt: null, rawSettings: {} },
}),
})const mockModel = new MockLanguageModelV1({
defaultObjectGenerationMode: 'json',
doGenerate: async () => ({
text: JSON.stringify({
name: 'Test User',
age: 25
}),
usage: { completionTokens: 20, promptTokens: 10 },
finishReason: 'stop',
rawCall: { rawPrompt: null, rawSettings: {} },
}),
})Add defaultObjectGenerationMode: 'json' to your MockLanguageModelV1 for generateObject tests.
Remove tool-result chunks - AI SDK handles tool execution internally. Only provide tool-call chunks.
Framework auto-parses based on Content-Type. Either accept 500 status or test with different content-type.
MockLanguageModelV1 is designed to be forward-compatible. Check if chunk format changed in AI SDK docs.
// Uses real OpenAI
const app = createApp() // No options = real models// Uses mocks
const app = createApp({
chatModel: mockModel,
objectModel: mockModel
})This architecture gives you the best of both worlds: real AI in production, fast reliable tests in development.