금융 애플리케이션을 개발하다 보면 사용자의 자연어 쿼리에서 주식 티커 심볼을 정확히 추출하는 기능이 필수적입니다. 특히 요즘 같은 AI 시대에는 LLM(Large Language Model)을 활용해 회사 이름을 티커로 변환하는 고급 기능도 구현하게 됩니다. 이런 복잡한 기능을 어떻게 효과적으로 테스트할 수 있을까요? 이 글에서는 금융 애플리케이션의 핵심 기능인 티커 추출에 대한 포괄적인 테스트 전략을 소개합니다.
효과적인 테스트를 위해서는 다양한 시나리오를 고려해야 합니다. 티커 추출 기능의 경우 다음과 같은 시나리오를 테스트해야 합니다:
사용자가 쿼리에 직접 티커 심볼을 포함시키는 경우입니다. 이는 가장 기본적인 시나리오로, 정규 표현식만으로도 구현 가능합니다.
def test_explicit_ticker_extraction(self):
"""Test extraction of explicitly mentioned tickers"""
testcases = [
("Analyze financials for AAPL", "AAPL"),
("What's the financial status of ticker MSFT?", "MSFT"),
("Do a financial analysis of TSLA", "TSLA"),
("Ticker: AMZN financial analysis", "AMZN"),
("Google GOOG financial statements", "GOOG"),
("Apple (AAPL) financial status", "AAPL")
]
for query, expected in testcases:
result = self.tool._extract_ticker(query)
self.assertEqual(result, expected, f"Query: {query}")
이 테스트는 다음과 같은 패턴을 검증합니다:
사용자가 티커 대신 회사 이름만 언급하는 경우입니다. 이 경우 미리 정의된 매핑 사전을 활용합니다.
def test_company_name_to_ticker_mapping(self):
"""Test extraction of tickers from company names using mapping dictionary"""
testcases = [
("Analyze Apple's financials", "AAPL"),
("How is Microsoft doing?", "MSFT"),
("Google's financial status", "GOOGL"),
("Should I invest in Tesla?", "TSLA"),
("Amazon financial analysis", "AMZN"),
("Facebook (Meta) performance?", "META")
]
for query, expected in testcases:
result = self.tool._extract_ticker(query)
self.assertEqual(result, expected, f"Query: {query}")
이 테스트는 아래와 같은 시나리오를 검증합니다:
티커나 회사 이름이 언급되지 않은 일반적인 쿼리의 경우입니다.
def test_no_ticker_extraction(self):
"""Test cases where no ticker can be extracted"""
testcases = [
"Analyze financial statements",
"How's the stock market?",
"Recent corporate earnings trends",
"Recommend good investments"
]
for query in testcases:
result = self.tool._extract_ticker(query)
self.assertIsNone(result, f"Query: {query}")
이러한 케이스들은 티커 추출에 실패해야 하며, None
을 반환하는 것이 적절합니다. 이를 통해 후속 로직이 적절한 오류 메시지를 제공할 수 있습니다.
정규 표현식이나 매핑 사전으로 해결할 수 없는 경우, LLM을 활용한 추론을 사용합니다.
@patch('langchain_openai.ChatOpenAI')
def test_llm_ticker_extraction(self, mock_llm):
"""Test ticker extraction using LLM"""
# Add LLM attribute to tool if not present
if not hasattr(self.tool, 'llm'):
self.tool.llm = None
# Setup LLM mock
mock_llm_instance = MagicMock()
self.tool.llm = mock_llm_instance
# Test various LLM response scenarios
test_scenarios = [
{
"query": "How is Nvidia's financial health?",
"llm_response": "NVDA",
"expected": "NVDA"
},
{
"query": "Netflix stock analysis",
"llm_response": "NFLX",
"expected": "NFLX"
},
{
"query": "Analyze an unknown company",
"llm_response": "UNKNOWN",
"expected": None
}
]
for scenario in test_scenarios:
# Set LLM response for each scenario
mock_llm_instance.predict.return_value = scenario["llm_response"]
# Run test
result = self.tool._extract_ticker(scenario["query"])
# Verify result
self.assertEqual(result, scenario["expected"],
f"Query: {scenario['query']}, LLM response: {scenario['llm_response']}")
이 테스트에서는 LLM의 응답을 모킹(mocking)하여 다양한 시나리오를 테스트합니다:
사용자 쿼리에서 티커를 추출하는 것 외에도, 분석 결과 텍스트에서 티커를 다시 추출하는 로직도 테스트해야 합니다. 이는 노드 간 데이터 전달 시 중요합니다.
def test_node_ticker_extraction_from_result(self):
"""Test ticker extraction from analysis result text"""
testcases = [
("Financial Statement Analysis for Apple Inc (Ticker: AAPL)", {"ticker": "AAPL"}),
("Analysis shows that ticker MSFT has strong fundamentals", {"ticker": "MSFT"}),
("The company Amazon (AMZN) shows improving metrics", {"ticker": "AMZN", "company": "The company Amazon"}),
("Financial analysis for TSLA indicates positive growth", {"ticker": "TSLA"}),
("Financial analysis completed but no ticker found", {"ticker": "unknown"})
]
for text, expected in testcases:
result = self.node._extract_ticker_from_result(text)
self.assertEqual(result, expected, f"Text: {text}")
이 테스트에서는 다양한 형식의 결과 텍스트에서 티커와 회사 정보를 올바르게 추출하는지 확인합니다. 추출에 실패하면 "unknown"을 기본값으로 반환해야 합니다.
티커 추출 기능이 전체 애플리케이션 흐름에서 올바르게 작동하는지 확인하는 통합 테스트도 중요합니다.
@patch('src.graph.nodes.us_financial.create_react_agent')
@patch('src.tools.us_stock.tool.USFinancialStatementTool._extract_ticker')
def test_run_with_explicit_ticker(self, mock_extract_ticker, mock_create_agent):
"""Test node execution with explicit ticker in query"""
# Set up mocked ticker extraction
mock_extract_ticker.return_value = "AAPL"
# Set up mocked result
result_content = "Financial Statement Analysis for Apple Inc (Ticker: AAPL)\n\nThe company shows strong financial health..."
# Set up mocked agent
mock_agent = MagicMock()
mock_agent.invoke.return_value = {
"messages": [
AIMessage(content=result_content)
]
}
mock_create_agent.return_value = mock_agent
# Test state object with English message containing a ticker
state = {
"llm": MagicMock(),
"messages": [HumanMessage(content="Analyze AAPL financials")]
}
# Execute node
self.node.agent = mock_agent
result = self.node._run(state)
# Verify
self.assertEqual(result.goto, "supervisor")
self.assertEqual(result.update["financial_analysis"]["ticker"], "AAPL")
self.assertEqual(result.update["financial_analysis"]["market"], "US")
self.assertEqual(result.update["financial_analysis"]["analysis_text"], result_content)
@patch('src.graph.nodes.us_financial.create_react_agent')
@patch('src.tools.us_stock.tool.USFinancialStatementTool._extract_ticker')
def test_run_with_company_name(self, mock_extract_ticker, mock_create_agent):
"""Test node execution with company name instead of ticker"""
# Set up mocked ticker extraction (simulating LLM-based inference)
mock_extract_ticker.return_value = "AAPL"
# Set up mocked result
result_content = "Financial Statement Analysis for Apple Inc (Ticker: AAPL)\n\nThe company shows strong financial health..."
# Set up mocked agent
mock_agent = MagicMock()
mock_agent.invoke.return_value = {
"messages": [
AIMessage(content=result_content)
]
}
mock_create_agent.return_value = mock_agent
# Test state object with English message containing only company name
state = {
"llm": MagicMock(),
"messages": [HumanMessage(content="Analyze Apple's financial status")]
}
# Execute node
self.node.agent = mock_agent
result = self.node._run(state)
# Verify
self.assertEqual(result.goto, "supervisor")
self.assertEqual(result.update["financial_analysis"]["ticker"], "AAPL")
self.assertEqual(result.update["financial_analysis"]["market"], "US")
self.assertEqual(result.update["financial_analysis"]["analysis_text"], result_content)
# Verify that extract_ticker was called with the company name query
mock_extract_ticker.assert_called_with("Analyze Apple's financial status")
@patch('src.graph.nodes.us_financial.create_react_agent')
@patch('src.tools.us_stock.tool.USFinancialStatementTool._extract_ticker')
def test_no_company_identified(self, mock_extract_ticker, mock_create_agent):
"""Test when neither ticker nor company can be identified"""
# Set up extraction to return None (no ticker identified)
mock_extract_ticker.return_value = None
# Set up mocked result
result_content = "I need more information. Please provide a specific company name or ticker symbol for financial analysis."
# Set up mocked agent
mock_agent = MagicMock()
mock_agent.invoke.return_value = {
"messages": [
AIMessage(content=result_content)
]
}
mock_create_agent.return_value = mock_agent
# Test state object with vague query
state = {
"llm": MagicMock(),
"messages": [HumanMessage(content="Analyze a technology company")]
}
# Execute node
self.node.agent = mock_agent
result = self.node._run(state)
# Verify - should return "unknown" ticker as specified in the current implementation
self.assertEqual(result.goto, "supervisor")
self.assertEqual(result.update["financial_analysis"]["ticker"], "unknown")
self.assertEqual(result.update["financial_analysis"]["market"], "US")
@patch('src.graph.nodes.us_financial.create_react_agent')
def test_non_english_query(self, mock_create_agent):
"""Test handling of non-English queries"""
# Set up mocked result with English response, even though query is non-English
result_content = "Financial Statement Analysis for Apple Inc (Ticker: AAPL)\n\nThe company shows strong financial health..."
# Set up mocked agent
mock_agent = MagicMock()
mock_agent.invoke.return_value = {
"messages": [
AIMessage(content=result_content)
]
}
mock_create_agent.return_value = mock_agent
# Test state object with non-English message
state = {
"llm": MagicMock(),
"messages": [HumanMessage(content="애플 회사의 재무 상태를 분석해 주세요")]
}
# We need to mock the tool's _extract_ticker to handle Korean text
with patch.object(USFinancialStatementTool, '_extract_ticker', return_value="AAPL"):
# Execute node
self.node.agent = mock_agent
result = self.node._run(state)
# Verify
self.assertEqual(result.goto, "supervisor")
self.assertEqual(result.update["financial_analysis"]["ticker"], "AAPL")
self.assertEqual(result.update["financial_analysis"]["market"], "US")
마지막으로, 다양한 쿼리 유형에 대한 종합적인 테스트를 통해 전체 시스템의 강건성을 검증합니다.
@patch('src.graph.nodes.us_financial.create_react_agent')
@patch('src.tools.us_stock.tool.USFinancialStatementTool._extract_ticker')
def test_node_with_various_queries(self, mock_extract_ticker, mock_create_agent):
"""Test node behavior with various query types"""
# Setup test cases
test_cases = [
{
"query": "Analyze AAPL financials",
"extracted_ticker": "AAPL",
"result_ticker": "AAPL",
"success": True
},
{
"query": "What's Apple's financial status?",
"extracted_ticker": "AAPL", # Extracted from mapping dictionary
"result_ticker": "AAPL",
"success": True
},
{
"query": "Analyze an AI company",
"extracted_ticker": "NVDA", # Inferred by LLM
"result_ticker": "NVDA",
"success": True
},
{
"query": "Analyze the stock market",
"extracted_ticker": None, # Extraction failed
"result_ticker": "unknown",
"success": False
}
]
for case in test_cases:
# Mock ticker extraction
mock_extract_ticker.return_value = case["extracted_ticker"]
# Mock agent response
mock_agent = MagicMock()
if case["success"]:
result_content = f"Financial Statement Analysis for Company (Ticker: {case['result_ticker']})\n\nThe company shows..."
else:
result_content = "I need more information. Please provide a specific ticker symbol for analysis."
mock_agent.invoke.return_value = {
"messages": [
MagicMock(content=result_content)
]
}
mock_create_agent.return_value = mock_agent
# State object for node execution
state = {
"llm": MagicMock(),
"messages": [MagicMock(content=case["query"])]
}
# Execute node
self.node.agent = mock_agent
result = self.node._run(state)
# Verify results
if case["success"]:
self.assertEqual(result.update["financial_analysis"]["ticker"], case["result_ticker"])
else:
self.assertEqual(result.update["financial_analysis"]["ticker"], "unknown")
위 테스트 코드들에서 사용된 주요 테스트 패턴과 구성 요소를 정리해보겠습니다:
@patch
데코레이터를 사용해 LLM, API 호출 등 외부 의존성을 모킹이러한 포괄적인 테스트는 단순히 버그를 찾는 것을 넘어 다양한 인사이트를 제공합니다:
LLM을 활용한 티커 추출 기능과 같은 복잡한 시스템을 테스트하려면 잘 설계된 테스트 전략이 필수적입니다. 단위 테스트부터 통합 테스트까지, 다양한 시나리오를 포괄하는 테스트 코드를 작성함으로써 애플리케이션의 품질과 신뢰성을 크게 향상시킬 수 있습니다.
특히 LLM과 같은 확률적 모델을 활용하는 기능은 일관된 결과를 보장하기 어렵기 때문에, 모킹을 통한 체계적인 테스트가 더욱 중요합니다. 이 글에서 소개한 테스트 패턴들은 금융 애플리케이션뿐만 아니라 다양한 AI 기반 애플리케이션의 테스트에도 적용할 수 있을 것입니다.