Add: Unit tests for habit_bot and stock_tracker

- tests/test_habit_bot.py: Habit tracking, food logging, keto guidance
- tests/test_stock_tracker.py: Portfolio management, P&L calculation
- pytest.ini: Pytest configuration
- Updated Jenkinsfile: Emphasized testing stages before build

Pipeline stages:
1. Code Quality Gates (lint + security)
2. Unit Tests (pytest with coverage)
3. Integration Tests (Oracle, Telegram, Gitea)
4. Build (only after tests pass)
5. Deploy to Staging
This commit is contained in:
Joungmin
2026-02-19 03:32:43 +09:00
parent 6d9bc5980f
commit ceb52b2146
5 changed files with 704 additions and 90 deletions

319
tests/test_stock_tracker.py Normal file
View File

@@ -0,0 +1,319 @@
#!/usr/bin/env python3
"""
Unit tests for Stock Tracker
Tests: Portfolio management, P&L calculation, price fetching
"""
import pytest
import sys
import os
import json
from datetime import datetime
from unittest.mock import Mock, patch, MagicMock
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
TEST_DATA_DIR = '/tmp/test_stock_tracker'
os.makedirs(TEST_DATA_DIR, exist_ok=True)
@pytest.fixture
def mock_prices():
"""Create mock price data"""
return {
'stock_AAPL': {
'symbol': 'AAPL',
'current_price': 180.0,
'change_percent': 2.5,
'high_52w': 200.0,
'low_52w': 150.0,
'volume': 50000000,
'updated_at': datetime.now().isoformat()
},
'stock_MSFT': {
'symbol': 'MSFT',
'current_price': 380.0,
'change_percent': -1.2,
'high_52w': 420.0,
'low_52w': 310.0,
'volume': 25000000,
'updated_at': datetime.now().isoformat()
},
'crypto_BTC': {
'symbol': 'BTC',
'current_price': 45000.0,
'change_percent': 3.8,
'high_52w': 69000.0,
'low_52w': 35000.0,
'volume': 30000000000,
'updated_at': datetime.now().isoformat()
}
}
@pytest.fixture
def mock_positions():
"""Create mock positions"""
return {
'stock_AAPL': {
'symbol': 'AAPL',
'asset_type': 'stock',
'quantity': 10,
'avg_cost': 150.0,
'entry_date': '2025-01-15',
'streak': 0,
'is_active': True
},
'stock_MSFT': {
'symbol': 'MSFT',
'asset_type': 'stock',
'quantity': 5,
'avg_cost': 350.0,
'entry_date': '2025-02-01',
'streak': 0,
'is_active': True
}
}
class TestPortfolioManagement:
"""Test portfolio management functionality"""
def test_add_position(self, mock_positions):
"""Test adding a new position"""
from stock_tracker import StockTracker, Position
# Mock the file operations
with patch('stock_tracker.load_json', return_value={}):
with patch('stock_tracker.save_json'):
tracker = StockTracker.__new__(StockTracker)
tracker.positions = {}
# Add a position
result = tracker.add_position(
symbol='NVDA',
asset_type='stock',
quantity=10,
avg_cost=800.0
)
assert result == True
key = 'stock_NVDA'
assert key in tracker.positions
assert tracker.positions[key].symbol == 'NVDA'
assert tracker.positions[key].quantity == 10
assert tracker.positions[key].avg_cost == 800.0
print(f"✅ Added position: NVDA 10 @ $800")
def test_remove_position(self, mock_positions):
"""Test removing a position"""
with patch('stock_tracker.load_json', return_value={}):
with patch('stock_tracker.save_json'):
tracker = StockTracker.__new__(StockTracker)
tracker.positions = mock_positions
# Remove a position
result = tracker.remove_position('AAPL', 'stock')
assert result == True
assert 'stock_AAPL' not in tracker.positions
print("✅ Removed position: AAPL")
def test_get_positions(self, mock_positions):
"""Test getting all positions"""
with patch('stock_tracker.load_json', return_value={}):
with patch('stock_tracker.save_json'):
tracker = StockTracker.__new__(StockTracker)
tracker.positions = mock_positions
positions = tracker.get_positions()
assert len(positions) == 2
print(f"✅ Retrieved {len(positions)} positions")
class TestPnLCalculation:
"""Test P&L calculation functionality"""
def test_calculate_profit(self, mock_positions, mock_prices):
"""Test profit calculation for winning position"""
with patch('stock_tracker.load_json', return_value={}):
with patch('stock_tracker.save_json'):
tracker = StockTracker.__new__(StockTracker)
tracker.positions = mock_positions
tracker.prices = mock_prices
summary = tracker.calculate_portfolio_summary()
# AAPL: Bought @ $150, Current @ $180 = +20% profit
assert summary.total_value > summary.total_cost
assert summary.total_pnl_percent > 0
print(f"✅ Profit calculated: {summary.total_pnl_percent:.1f}%")
def test_calculate_loss(self, mock_positions, mock_prices):
"""Test loss calculation for losing position"""
# Modify MSFT to have a loss
mock_positions['stock_MSFT']['avg_cost'] = 400.0 # Bought higher than current
with patch('stock_tracker.load_json', return_value={}):
with patch('stock_tracker.save_json'):
tracker = StockTracker.__new__(StockTracker)
tracker.positions = mock_positions
tracker.prices = mock_prices
summary = tracker.calculate_portfolio_summary()
# MSFT: Bought @ $400, Current @ $380 = -5% loss
msft_pos = next((p for p in summary.positions if p['symbol'] == 'MSFT'), None)
assert msft_pos['pnl_percent'] < 0
print(f"✅ Loss calculated: MSFT {msft_pos['pnl_percent']:.1f}%")
def test_pnl_percentage(self, mock_positions):
"""Test P&L percentage calculation"""
avg_cost = 100.0
current_price = 150.0
expected_pnl_percent = 50.0
pnl_percent = ((current_price - avg_cost) / avg_cost) * 100
assert pnl_percent == expected_pnl_percent
print(f"✅ P&L % calculated: {pnl_percent}%")
class TestInvestmentGuidelineChecks:
"""Test investment guideline compliance"""
def test_checklist_score_calculation(self):
"""Test 7-item checklist scoring"""
checklist = {
'story_clear': True,
'earnings_uptrend': True,
'balance_sheet_healthy': True,
'capital_return_plan': True,
'governance_clean': True,
'market_liquidity': True,
'relative_strength': False
}
score = sum(checklist.values())
max_score = len(checklist)
assert score == 6
assert f"{score}/{max_score}" == "6/7"
print(f"✅ Checklist score: {score}/{max_score}")
def test_pbr_evaluation(self):
"""Test PBR evaluation logic"""
# PBR < 1 is generally considered undervalued
pbr_values = {
'AAPL': 0.85, # Undervalued
'MSFT': 1.5, # Fair value
'GOOGL': 2.1, # Premium
'NVDA': 25.0 # Expensive (but justified by growth)
}
for symbol, pbr in pbr_values.items():
if pbr < 1:
status = "undervalued"
elif pbr < 3:
status = "fair value"
else:
status = "premium"
print(f"{symbol} PBR: {pbr}x ({status})")
def test_stop_loss_calculation(self):
"""Test -10% stop loss calculation"""
entry_price = 100000 # KRW
# Hard stop loss
stop_loss_price = entry_price * 0.9 # -10%
assert stop_loss_price == 90000
# Trailing stop (from high)
high_price = 120000
trailing_stop = high_price * 0.9 # -10% from high
assert trailing_stop == 108000
print(f"✅ Stop loss: {stop_loss_price} (entry: {entry_price})")
print(f"✅ Trailing stop: {trailing_stop} (high: {high_price})")
class TestReportGeneration:
"""Test report generation functionality"""
def test_daily_report_structure(self, mock_positions, mock_prices):
"""Test daily report has required sections"""
with patch('stock_tracker.load_json', return_value={}):
with patch('stock_tracker.save_json'):
tracker = StockTracker.__new__(StockTracker)
tracker.positions = mock_positions
tracker.prices = mock_prices
report = tracker.generate_daily_report()
# Check report contains key sections
assert '일일 포트폴리오 리포트' in report or 'Daily' in report
assert '총 가치' in report or 'Total Value' in report
assert '손익' in report or 'P&L' in report
print("✅ Daily report structure verified")
def test_weekly_report_structure(self, mock_positions, mock_prices):
"""Test weekly report has required sections"""
with patch('stock_tracker.load_json', return_value={}):
with patch('stock_tracker.save_json'):
tracker = StockTracker.__new__(StockTracker)
tracker.positions = mock_positions
tracker.prices = mock_prices
report = tracker.generate_weekly_report()
# Check report contains key sections
assert '주간 포트폴리오 리포트' in report or 'Weekly' in report
assert '목표' in report or 'Goal' in report
assert '체크리스트' in report or 'Checklist' in report
print("✅ Weekly report structure verified")
class TestDataTypes:
"""Test data type validation"""
def test_position_validation(self):
"""Test Position dataclass"""
from stock_tracker import Position
pos = Position(
symbol='TEST',
asset_type='stock',
quantity=100,
avg_cost=50.0,
entry_date='2025-01-01'
)
assert pos.symbol == 'TEST'
assert pos.quantity == 100
assert pos.avg_cost == 50.0
assert pos.is_active == True
print("✅ Position validation passed")
def test_price_data_validation(self):
"""Test PriceData dataclass"""
from stock_tracker import PriceData
price = PriceData(
symbol='TEST',
current_price=100.0,
change_percent=2.5,
high_52w=120.0,
low_52w=80.0,
volume=1000000.0
)
assert price.symbol == 'TEST'
assert price.current_price == 100.0
assert price.change_percent == 2.5
print("✅ PriceData validation passed")
# Pytest configuration
if __name__ == '__main__':
pytest.main([__file__, '-v'])