5.8.2 JavaScript/TypeScript 환경: Jest/Vitest를 이용한 비동기 체인 테스트

5.8.2 JavaScript/TypeScript 환경: Jest/Vitest를 이용한 비동기 체인 테스트

Node.js와 브라우저 생태계를 아우르는 JavaScript/TypeScript(JS/TS) 진영은, Vercel의 AI SDK나 LangChain.js의 공격적인 확장에 힘입어 프론트엔드와 백엔드를 가리지 않고 AI 에이전트가 배포되는 주요 격전지다. 이 생태계에서 결정론적 오라클을 구축하기 위해 가장 널리 쓰이는 무기는 Jest, 그리고 최근 그 자리를 위협하고 있는 초고속 테스트 프레임워크 Vitest다.

특히 JS/TS 생태계의 비동기(Asynchronous) 이벤트 루프 특성과 정적 타입 시스템(TypeScript)을 십분 활용하면, LLM의 연쇄적인 추론 과정(Chain)을 극도로 촘촘하게 검증할 수 있다.

1. Zod를 활용한 타입 기반(Type-driven) 오라클 단언

TypeScript 환경에서 LLM 테스트의 가장 강력한 방어선은 런타임 타입 검증 라이브러리인 Zod를 오라클의 평가 엔진으로 사용하는 것이다. 전통적인 테스트가 정규표현식으로 간신히 문자열을 찾아냈다면, TS 환경에서는 LLM이 반환하는 JSON의 스키마 정합성을 단 한 줄의 코드로 결정론적으로 분쇄하거나 통과시킬 수 있다.

import { describe, it, expect } from 'vitest';
import { z } from 'zod';
import { generateAIResponse } from './ai-service';

// 1. 오라클의 기준이 되는 강력한 Zod 스키마 정의
const RefundResponseSchema = z.object({
  intent: z.enum(['REFUND', 'COMPLAINT', 'QUESTION']),
  extractedItems: z.array(z.string()).min(1),
  confidenceScore: z.number().min(0).max(1)
});

describe('AI 환불 요청 파서 테스트', () => {
  it('사용자 입력을 명세된 JSON 구조로 완벽히 추출해야 한다', async () => {
    // 2. 비동기 LLM 호출
    const rawResponse = await generateAIResponse("어제 산 운동화 환불할래요.");
    
    // 3. Zod 스키마를 통한 무자비한 결정론적 구조 검증 (오라클)
    // 파싱에 실패하면 ZodError가 발생하며 테스트는 즉시 Fail 처리됨
    const parsedData = RefundResponseSchema.parse(rawResponse);
    
    // 4. 내용적 검증
    expect(parsedData.intent).toBe('REFUND');
    expect(parsedData.extractedItems).toContain('운동화');
  });
});

ZodVitest의 결합은 LLM 특유의 “키(Key) 이름 마음대로 바꾸기“나 “배열(Array) 대신 콤마 문자열(String) 반환하기” 같은 교활한 환각을 원천 차단하는 가장 훌륭한 파수꾼이다.

2. Promise 체이닝과 다단계(Multi-step) Agent 모킹

최근의 AI 애플리케이션은 단일 프롬프트를 넘어, 하나의 모델 응답이 다음 모델의 입력으로 이어지는 체인(Chain) 형태를 띤다. (예: 의도 파악 -> 데이터베이스 조회 -> 최종 응답 생성). Jest/Vitest의 jest.mock()vi.mock() 기능은 이 길고 불안정한 비동기 체인을 중간에서 끊어내고, 오직 타겟 로직만 테스트할 수 있도록 격리(Isolation)하는 데 탁월하다.

import { vi, describe, it, expect } from 'vitest';
import { orchestrator } from './agent-chain';
import * as llmClient from './llm-client';

// LLM API 클라이언트 전체를 Mocking
vi.mock('./llm-client');

describe('다단계 AI 오케스트레이터 검증', () => {
  it('의도 파악 후 데이터베이스 조회가 정상 호출되어야 한다', async () => {
    // 체인의 첫 번째 단계(의도 파악)의 응답을 가짜(Mock) Promise로 주입
    vi.mocked(llmClient.analyzeIntent).mockResolvedValue({ intent: 'CHECK_STOCK' });
    vi.mocked(llmClient.generateReply).mockResolvedValue("재고가 있습니다.");

    await orchestrator("노트북 재고 있나요?");

    // 오라클: 첫 번째 단계의 출력에 따라, 두 번째 단계가 호출되었는지가 가장 중요함
    expect(llmClient.analyzeIntent).toHaveBeenCalledTimes(1);
    expect(llmClient.generateReply).toHaveBeenCalledWith(
        expect.stringContaining('CHECK_STOCK') // 이전 체인의 상태가 다음 체인으로 잘 넘어갔는지 확인
    );
  });
});

이 기법은 LLM의 변덕스러운 텍스트 생성을 검증하는 것을 넘어, **“AI 에이전트가 프로그래머가 설계한 제어 흐름(Control Flow)을 이탈하지 않고 코드를 정확히 순회하였는가”**를 묻는 화이트박스 테스트의 교과서적인 접근이다.

동기적 순차 실행의 압박에서 자유로운 JS/TS의 비동기 환경은, 이처럼 수많은 Promise들을 가짜(Mock)로 교체하고 병렬로 채점함으로써 AI 파이프라인의 견고함을 극한으로 끌어올린다.