| name | test-guide |
| description | Testing patterns for claude-code-discord-bridge — pytest, async testing, mocking Discord objects, TDD workflow |
Test Guide — Testing Patterns for claude-code-discord-bridge
When to Activate
- Writing tests for new features
- Fixing test failures
- Adding test coverage for existing code
- When asked to "add tests", "test this", or "improve coverage"
TDD Workflow
Follow the Red-Green-Refactor cycle:
- RED: Write a failing test that describes the desired behavior
- GREEN: Write the minimum code to make the test pass
- REFACTOR: Clean up while keeping tests green
Project Test Setup
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]
- pytest-asyncio: Auto mode — all async test functions run automatically
- pytest-cov: Coverage reporting
- No test markers needed: asyncio_mode = "auto" handles async tests
Test Patterns by Module
Pure Logic (parser, chunker, types) — Full Unit Tests
These have no external dependencies. Test exhaustively:
def test_parse_system_message():
line = '{"type":"system","session_id":"abc-123"}'
event = parse_line(line)
assert event.message_type == MessageType.SYSTEM
assert event.session_id == "abc-123"
def test_parse_invalid_json():
event = parse_line("not json")
assert event is None
Repository (database) — Integration Tests with Real SQLite
@pytest.fixture
async def repo(tmp_path):
db_path = str(tmp_path / "test.db")
await init_db(db_path)
return SessionRepository(db_path)
async def test_save_and_get(repo):
await repo.save(thread_id=123, session_id="abc-def")
result = await repo.get_session_id(thread_id=123)
assert result == "abc-def"
Runner — Unit Test Args/Env Building
def test_build_args_basic():
runner = ClaudeRunner(command="claude", model="sonnet")
args = runner._build_args("hello", session_id=None)
assert args[0] == "claude"
assert "-p" in args
assert "--" in args
assert args[-1] == "hello"
def test_build_args_rejects_invalid_session():
runner = ClaudeRunner()
with pytest.raises(ValueError):
runner._build_args("hello", session_id="'; DROP TABLE --")
def test_env_strips_secrets():
runner = ClaudeRunner()
import os
os.environ["DISCORD_BOT_TOKEN"] = "test-token"
env = runner._build_env()
assert "DISCORD_BOT_TOKEN" not in env
Cogs — Mock Discord Objects
from unittest.mock import AsyncMock, MagicMock
def make_mock_interaction(user_id: int = 12345) -> MagicMock:
interaction = MagicMock()
interaction.user = MagicMock()
interaction.user.id = user_id
interaction.response = AsyncMock()
interaction.followup = AsyncMock()
return interaction
Chunker — Edge Cases Are Critical
def test_chunk_empty_string():
assert chunk_message("") == []
def test_chunk_preserves_code_blocks():
text = "```python\nprint('hello')\n```"
chunks = chunk_message(text)
assert len(chunks) == 1
def test_chunk_splits_at_boundary():
long_text = "a" * 3000
chunks = chunk_message(long_text)
assert all(len(c) <= 2000 for c in chunks)
Running Tests
uv run pytest tests/ -v --cov=claude_discord --cov-report=term-missing
uv run pytest tests/test_parser.py -v
uv run pytest tests/test_parser.py::test_parse_system_message -v
uv run pytest tests/ -x
Coverage Goals
- Parser, chunker, types: 90%+ (pure logic, easy to test)
- Repository: 90%+ (real SQLite, no mocks needed)
- Runner: 60%+ (test arg building and env, mock subprocess)
- Cogs, StatusManager: 30%+ (heavy Discord mocking, test validation logic)
- Overall: 50%+ and improving
What NOT to Test
- Discord.py library internals
- SQLite behavior
- asyncio internals
- Simple property access / getters