통합 테스트하기
통합 테스트하기 (Integration testing)
통합 테스트는 에이전트가 모델 API와 외부 서비스와 함께 제대로 동작하는지 검증해요. 페이크와 목(mock)을 쓰는 단위 테스트와 달리, 통합 테스트는 실제 네트워크 호출을 해서 컴포넌트들이 함께 동작하는지, 자격 증명이 유효한지, 지연 시간이 수용 가능한지 확인합니다. LLM 응답은 비결정적이기 때문에 통합 테스트는 전통적인 소프트웨어 테스트와 다른 전략을 요구하죠. 이 가이드에서는 에이전트용 통합 테스트를 어떻게 구성하고 작성하며 실행하는지 다룹니다. (LangChain 자체에 기여할 때의 일반 테스트 인프라는 코드 기여하기 문서를 참고하세요.)
단위 테스트와 통합 테스트 분리하기 (Separate unit and integration tests)
통합 테스트는 느리고 API 자격 증명이 필요해요. 그래서 단위 테스트와 분리해 두는 게 좋죠. 이렇게 하면 변경할 때마다 빠른 단위 테스트를 돌리고, 통합 테스트는 CI나 배포 전 검사에만 쓸 수 있습니다. pytest 마커로 통합 테스트를 표시해 보세요.
import pytest
from langchain.agents import create_agent
from langchain.messages import HumanMessage
@pytest.mark.integration
def test_agent_with_real_model():
agent = create_agent("claude-sonnet-4-6", tools=[get_weather])
result = agent.invoke({
"messages": [HumanMessage(content="What's the weather in SF?")]
})
assert len(result["messages"]) > 1
pytest가 마커를 인식하고 기본 실행에서 통합 테스트를 제외하도록 설정합니다.
[pytest]
markers =
integration: tests that call real LLM APIs
addopts = -m "not integration"
[tool.pytest.ini_options]
markers = [
"integration: tests that call real LLM APIs"
]
addopts = "-m 'not integration'"
통합 테스트만 명시적으로 실행하려면:
pytest -m integration
API 키 관리하기 (Manage API keys)
통합 테스트는 실제 API 자격 증명이 필요해요. 키가 소스 코드 관리에서 벗어나도록 환경 변수에서 불러옵니다. conftest.py 픽스처로 필요한 키가 있는지 검증해 보세요.
import os
import pytest
@pytest.fixture(autouse=True)
def check_api_keys():
if not os.environ.get("OPENAI_API_KEY"):
pytest.skip("OPENAI_API_KEY not set")
로컬 개발에서는 키를 .env 파일에 저장하고 python-dotenv로 불러옵니다.
OPENAI_API_KEY=sk-...
from dotenv import load_dotenv
load_dotenv()
자격 증명이 커밋되지 않도록 .gitignore에 .env를 추가하세요. CI에서는 프로바이더의 시크릿 관리 기능(예: GitHub Actions secrets)으로 시크릿을 주입하면 됩니다.
내용이 아니라 구조를 단정하기 (Assert on structure, not content)
LLM 응답은 실행마다 달라져요. 정확한 출력 문자열을 단정(assert)하는 대신, 메시지 타입, 도구 호출 이름, 인자 형태, 메시지 개수 같은 응답의 구조적 속성을 검증합니다.
from langchain.agents import create_agent
from langchain.messages import AIMessage, HumanMessage
def test_agent_calls_weather_tool():
agent = create_agent("claude-sonnet-4-6", tools=[get_weather])
result = agent.invoke({
"messages": [HumanMessage(content="What's the weather in SF?")]
})
messages = result["messages"]
tool_calls = [
tc
for msg in messages
if hasattr(msg, "tool_calls")
for tc in (msg.tool_calls or [])
]
assert any(tc["name"] == "get_weather" for tc in tool_calls)
assert isinstance(messages[-1], AIMessage)
assert len(messages[-1].content) > 0
더 엄격한 궤적 단정이 필요하면 AgentEvals 이밸류에이터를 사용하면 돼요. unordered, superset 같은 퍼지 매칭 모드를 지원합니다.
비용과 지연 시간 줄이기 (Reduce cost and latency)
LLM API를 호출하는 통합 테스트는 실제 비용이 발생해요. 테스트 스위트를 빠르고 저렴하게 유지하는 몇 가지 방법이 있습니다.
- 더 작은 모델 사용: 도구 호출과 응답 구조만 검증하면 되는 테스트에는
gemini-3.1-flash-lite같은 모델을 씁니다. maxTokens설정: 응답 길이를 제한해서 길고 비싼 완성(completion)을 막습니다.- 테스트 범위 제한: 테스트 하나에 동작 하나만 검증하세요. 단일 턴 테스트로 충분한데 굳이 많은 LLM 호출을 이어 붙이는 end-to-end 시나리오는 피하는 게 좋아요.
- 선별적으로 실행: 위에서 본 테스트 분리를 활용해서 통합 테스트는 파일을 저장할 때마다가 아니라 CI나 배포 전에만 실행합니다.
from langchain.agents import create_agent
agent = create_agent(
"gemini-3.1-flash-lite",
tools=[get_weather],
model_kwargs={"max_tokens": 256},
)
HTTP 호출 기록하고 재생하기 (Record and replay HTTP calls)
CI에서 자주 실행되는 테스트라면, 첫 실행에서 HTTP 상호작용을 기록하고 이후 실행에서는 실제 API 호출 없이 재생할 수 있어요. 이러면 초기 기록 이후에는 비용과 지연 시간이 사라집니다. vcrpy는 HTTP 요청/응답 쌍을 YAML "카세트(cassette)" 파일에 기록하고, pytest-recording 플러그인이 이를 pytest와 연동합니다.
카세트에서 민감한 정보를 걸러내도록 conftest.py를 설정하세요.
import pytest
@pytest.fixture(scope="session")
def vcr_config():
return {
"filter_headers": [
("authorization", "XXXX"),
("x-api-key", "XXXX"),
],
"filter_query_parameters": [
("api_key", "XXXX"),
("key", "XXXX"),
],
}
프로젝트에서 vcr 마커를 인식하도록 설정합니다.
[pytest]
markers =
vcr: record/replay HTTP via VCR
addopts = --record-mode=once
[tool.pytest.ini_options]
markers = [
"vcr: record/replay HTTP via VCR"
]
addopts = "--record-mode=once"
--record-mode=once 옵션은 첫 실행에서 HTTP 상호작용을 기록하고 이후 실행에서는 재생합니다.
테스트에 vcr 마커를 붙이세요.
import pytest
from langchain.agents import create_agent
from langchain.messages import HumanMessage
@pytest.mark.vcr()
def test_agent_trajectory():
agent = create_agent("claude-sonnet-4-6", tools=[get_weather])
result = agent.invoke({
"messages": [HumanMessage(content="What's the weather in SF?")]
})
assert any(
tc["name"] == "get_weather"
for msg in result["messages"]
if hasattr(msg, "tool_calls")
for tc in (msg.tool_calls or [])
)
첫 실행은 실제 네트워크 호출을 하고 tests/cassettes/에 카세트 파일을 만듭니다. 이후 실행은 기록된 응답을 재생하죠. 프롬프트를 수정하거나, 도구를 새로 추가하거나, 기대 궤적을 바꾸면 저장된 카세트는 구식이 되어 기존 테스트가 실패합니다. 해당 카세트 파일을 삭제하고 테스트를 다시 실행해서 새로운 상호작용을 기록하세요.
다음 단계 (Next steps)
결정적 매칭이나 LLM-as-judge 이밸류에이터로 에이전트 궤적을 평가하는 방법을 Evals에서 배워보세요.