Some checks failed
CI / test (pull_request) Has been cancelled
- Restructure docs into topic-specific files to minimize context - Create docs/workflow.md (Git + Agent workflow) - Create docs/commands.md (Common failures + build commands) - Create docs/architecture.md (System design + data flow) - Create docs/testing.md (Test structure + guidelines) - Rewrite CLAUDE.md as concise hub with links to detailed docs - Update .gitignore to exclude data/ directory Benefits: - Reduced context size for AI assistants - Faster reference lookups - Better maintainability - Topic-focused documentation Closes #13 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
214 lines
5.8 KiB
Markdown
214 lines
5.8 KiB
Markdown
# Testing Guidelines
|
|
|
|
## Test Structure
|
|
|
|
**54 tests** across four files. `asyncio_mode = "auto"` in pyproject.toml — async tests need no special decorator.
|
|
|
|
The `settings` fixture in `conftest.py` provides safe defaults with test credentials and in-memory DB.
|
|
|
|
### Test Files
|
|
|
|
#### `tests/test_risk.py` (11 tests)
|
|
- Circuit breaker boundaries
|
|
- Fat-finger edge cases
|
|
- P&L calculation edge cases
|
|
- Order validation logic
|
|
|
|
**Example:**
|
|
```python
|
|
def test_circuit_breaker_exact_threshold(risk_manager):
|
|
"""Circuit breaker should trip at exactly -3.0%."""
|
|
with pytest.raises(CircuitBreakerTripped):
|
|
risk_manager.validate_order(
|
|
current_pnl_pct=-3.0,
|
|
order_amount=1000,
|
|
total_cash=10000
|
|
)
|
|
```
|
|
|
|
#### `tests/test_broker.py` (6 tests)
|
|
- OAuth token lifecycle
|
|
- Rate limiting enforcement
|
|
- Hash key generation
|
|
- Network error handling
|
|
- SSL context configuration
|
|
|
|
**Example:**
|
|
```python
|
|
async def test_rate_limiter(broker):
|
|
"""Rate limiter should delay requests to stay under 10 RPS."""
|
|
start = time.monotonic()
|
|
for _ in range(15): # 15 requests
|
|
await broker._rate_limiter.acquire()
|
|
elapsed = time.monotonic() - start
|
|
assert elapsed >= 1.0 # Should take at least 1 second
|
|
```
|
|
|
|
#### `tests/test_brain.py` (18 tests)
|
|
- Valid JSON parsing
|
|
- Markdown-wrapped JSON handling
|
|
- Malformed JSON fallback
|
|
- Missing fields handling
|
|
- Invalid action validation
|
|
- Confidence threshold enforcement
|
|
- Empty response handling
|
|
- Prompt construction for different markets
|
|
|
|
**Example:**
|
|
```python
|
|
async def test_confidence_below_threshold_forces_hold(brain):
|
|
"""Decisions below confidence threshold should force HOLD."""
|
|
decision = brain.parse_response('{"action":"BUY","confidence":70,"rationale":"test"}')
|
|
assert decision.action == "HOLD"
|
|
assert decision.confidence == 70
|
|
```
|
|
|
|
#### `tests/test_market_schedule.py` (19 tests)
|
|
- Market open/close logic
|
|
- Timezone handling (UTC, Asia/Seoul, America/New_York, etc.)
|
|
- DST (Daylight Saving Time) transitions
|
|
- Weekend handling
|
|
- Lunch break logic
|
|
- Multiple market filtering
|
|
- Next market open calculation
|
|
|
|
**Example:**
|
|
```python
|
|
def test_is_market_open_during_trading_hours():
|
|
"""Market should be open during regular trading hours."""
|
|
# KRX: 9:00-15:30 KST, no lunch break
|
|
market = MARKETS["KR"]
|
|
trading_time = datetime(2026, 2, 3, 10, 0, tzinfo=ZoneInfo("Asia/Seoul")) # Monday 10:00
|
|
assert is_market_open(market, trading_time) is True
|
|
```
|
|
|
|
## Coverage Requirements
|
|
|
|
**Minimum coverage: 80%**
|
|
|
|
Check coverage:
|
|
```bash
|
|
pytest -v --cov=src --cov-report=term-missing
|
|
```
|
|
|
|
Expected output:
|
|
```
|
|
Name Stmts Miss Cover Missing
|
|
-----------------------------------------------------------
|
|
src/brain/gemini_client.py 85 5 94% 165-169
|
|
src/broker/kis_api.py 120 12 90% ...
|
|
src/core/risk_manager.py 35 2 94% ...
|
|
src/db.py 25 1 96% ...
|
|
src/main.py 150 80 47% (excluded from CI)
|
|
src/markets/schedule.py 95 3 97% ...
|
|
-----------------------------------------------------------
|
|
TOTAL 510 103 80%
|
|
```
|
|
|
|
**Note:** `main.py` has lower coverage as it contains the main loop which is tested via integration/manual testing.
|
|
|
|
## Test Configuration
|
|
|
|
### `pyproject.toml`
|
|
```toml
|
|
[tool.pytest.ini_options]
|
|
asyncio_mode = "auto"
|
|
testpaths = ["tests"]
|
|
python_files = ["test_*.py"]
|
|
```
|
|
|
|
### `tests/conftest.py`
|
|
```python
|
|
@pytest.fixture
|
|
def settings() -> Settings:
|
|
"""Provide test settings with safe defaults."""
|
|
return Settings(
|
|
KIS_APP_KEY="test_key",
|
|
KIS_APP_SECRET="test_secret",
|
|
KIS_ACCOUNT_NO="12345678-01",
|
|
GEMINI_API_KEY="test_gemini_key",
|
|
MODE="paper",
|
|
DB_PATH=":memory:", # In-memory SQLite
|
|
CONFIDENCE_THRESHOLD=80,
|
|
ENABLED_MARKETS="KR",
|
|
)
|
|
```
|
|
|
|
## Writing New Tests
|
|
|
|
### Naming Convention
|
|
- Test files: `test_<module>.py`
|
|
- Test functions: `test_<feature>_<scenario>()`
|
|
- Use descriptive names that explain what is being tested
|
|
|
|
### Good Test Example
|
|
```python
|
|
async def test_send_order_with_market_price(broker, settings):
|
|
"""Market orders should use price=0 and ORD_DVSN='01'."""
|
|
# Arrange
|
|
stock_code = "005930"
|
|
order_type = "BUY"
|
|
quantity = 10
|
|
|
|
# Act
|
|
with patch.object(broker._session, 'post') as mock_post:
|
|
mock_post.return_value.__aenter__.return_value.status = 200
|
|
mock_post.return_value.__aenter__.return_value.json = AsyncMock(
|
|
return_value={"rt_cd": "0", "msg1": "OK"}
|
|
)
|
|
|
|
await broker.send_order(stock_code, order_type, quantity, price=0)
|
|
|
|
# Assert
|
|
call_args = mock_post.call_args
|
|
body = call_args.kwargs['json']
|
|
assert body['ORD_DVSN'] == '01' # Market order
|
|
assert body['ORD_UNPR'] == '0' # Price 0
|
|
```
|
|
|
|
### Test Checklist
|
|
- [ ] Test passes in isolation (`pytest tests/test_foo.py::test_bar -v`)
|
|
- [ ] Test has clear docstring explaining what it tests
|
|
- [ ] Arrange-Act-Assert structure
|
|
- [ ] Uses appropriate fixtures from conftest.py
|
|
- [ ] Mocks external dependencies (API calls, network)
|
|
- [ ] Tests edge cases and error conditions
|
|
- [ ] Doesn't rely on test execution order
|
|
|
|
## Running Tests
|
|
|
|
```bash
|
|
# All tests
|
|
pytest -v
|
|
|
|
# Specific file
|
|
pytest tests/test_risk.py -v
|
|
|
|
# Specific test
|
|
pytest tests/test_brain.py::test_parse_valid_json -v
|
|
|
|
# With coverage
|
|
pytest -v --cov=src --cov-report=term-missing
|
|
|
|
# Stop on first failure
|
|
pytest -x
|
|
|
|
# Verbose output with print statements
|
|
pytest -v -s
|
|
```
|
|
|
|
## CI/CD Integration
|
|
|
|
Tests run automatically on:
|
|
- Every commit to feature branches
|
|
- Every PR to main
|
|
- Scheduled daily runs
|
|
|
|
**Blocking conditions:**
|
|
- Test failures → PR blocked
|
|
- Coverage < 80% → PR blocked (warning only for main.py)
|
|
|
|
**Non-blocking:**
|
|
- `mypy --strict` errors (type hints encouraged but not enforced)
|
|
- `ruff check` warnings (must be acknowledged)
|