AI 기능을 붙이고 나서 테스트를 짜려다 막혔다. generateText를 감싼 내 함수를 테스트하려면 결국 실제 모델을 불러야 하는데, 그러면 테스트 한 번 돌릴 때마다 토큰을 태운다. 게다가 모델 응답이 매번 달라지니 expect로 결과를 못 박을 수가 없다. CI에서 돌리려면 API 키까지 넣어야 하고.
문제는 "모델을 호출한다"는 사실이 아니라 "응답을 받은 뒤 내 코드가 뭘 하는가"를 검증하고 싶다는 거다. 그렇다면 모델 자리에 가짜를 끼우면 된다. Vercel AI SDK는 ai/test로 MockLanguageModelV2를 제공한다.
generateText 모킹
doGenerate에 내가 원하는 응답을 그대로 반환하게 하면, 네트워크 없이 함수가 돈다.
// summarize.test.ts
import { generateText } from 'ai';
import { MockLanguageModelV2 } from 'ai/test';
import { expect, test } from 'vitest';
test('요약 결과 앞뒤 공백을 다듬는다', async () => {
const model = new MockLanguageModelV2({
doGenerate: async () => ({
finishReason: 'stop',
usage: { inputTokens: 10, outputTokens: 20, totalTokens: 30 },
content: [{ type: 'text', text: ' 세 줄 요약 ' }],
warnings: [],
}),
});
const { text } = await generateText({ model, prompt: '아무거나' });
expect(text.trim()).toBe('세 줄 요약');
});
응답이 고정이니 결과도 고정이다. 토큰 0, API 키 불필요.
스트리밍 모킹
스트리밍은 simulateReadableStream으로 청크를 하나씩 흘려보낸다. 실제 모델이 토큰을 쪼개 보내는 상황을 그대로 재현할 수 있다.
import { streamText, simulateReadableStream } from 'ai';
import { MockLanguageModelV2 } from 'ai/test';
const model = new MockLanguageModelV2({
doStream: async () => ({
stream: simulateReadableStream({
chunkDelayInMs: 10,
chunks: [
{ type: 'text-start', id: '1' },
{ type: 'text-delta', id: '1', delta: '안녕' },
{ type: 'text-delta', id: '1', delta: '하세요' },
{ type: 'text-end', id: '1' },
{
type: 'finish',
finishReason: 'stop',
usage: { inputTokens: 3, outputTokens: 5, totalTokens: 8 },
},
],
}),
}),
});
const result = streamText({ model, prompt: '인사해줘' });
expect(await result.text).toBe('안녕하세요');
chunkDelayInMs를 주면 딜레이까지 흉내 내니, 스트리밍 중간에 멈추거나 로딩 UI를 보여주는 로직도 검증할 수 있다.
핵심은 테스트가 모델의 답을 평가하는 게 아니라 내 코드의 동작을 평가하게 만드는 거다. 응답을 고정해두면 파싱·후처리·에러 분기 같은 내 로직을 결정론적으로 찍어볼 수 있다. 실제 모델 품질 확인은 별도의 평가(eval) 단계로 떼어내고, 단위 테스트는 가짜 모델로 빠르고 공짜로 돌리자.