Files
The-Ouroboros/docs/testing.md
agentson 05e8986ff5
Some checks failed
CI / test (pull_request) Has been cancelled
refactor: split CLAUDE.md into focused documentation structure
- 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>
2026-02-04 10:13:48 +09:00

5.8 KiB

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:

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:

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:

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:

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:

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

[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
python_files = ["test_*.py"]

tests/conftest.py

@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

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

# 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)