Manus에서 모든 스킬 실행
원클릭으로
원클릭으로
원클릭으로 Manus에서 모든 스킬 실행
시작하기$pwd:
discord-bot-test
// Discord Bot(TalkBot2)のテストコード作成をサポート。テスト駆動開発(TDD)を実践し、モックを使用した単体・統合テストのベストプラクティスを提供
$ git log --oneline --stat
stars:0
forks:0
updated:2026년 3월 25일 17:04
SKILL.md
// Discord Bot(TalkBot2)のテストコード作成をサポート。テスト駆動開発(TDD)を実践し、モックを使用した単体・統合テストのベストプラクティスを提供
[HINT] SKILL.md 및 모든 관련 파일을 포함한 전체 스킬 디렉토리를 다운로드합니다
| name | discord-bot-test |
| description | Discord Bot(TalkBot2)のテストコード作成をサポート。テスト駆動開発(TDD)を実践し、モックを使用した単体・統合テストのベストプラクティスを提供 |
| compatibility | Designed for GitHub Copilot CLI |
| metadata | {"author":"TalkBot2 Project","version":"1.0","original-source":"https://github.com/syuutaMC"} |
このスキルは、TalkBot2(Discord Bot)のテストコード作成をサポートし、**テスト駆動開発(TDD)**を実践するためのガイドラインを提供します。
高品質で保守性の高いDiscord Botを開発するため、以下を実現します:
新機能を実装する前に、まずテストを書く
# ❌ 悪い例: テストなしで機能を実装
async def new_feature():
# 実装...
pass
# ✅ 良い例: テストを先に書く
@pytest.mark.asyncio
async def test_new_feature_success():
# Arrange
bot = create_mock_bot()
# Act
result = await bot.new_feature()
# Assert
assert result is not None
適用場面:
単体テスト(Unit Tests):
tests/test_*.py統合テスト(Integration Tests):
tests/integration/test_*.py@pytest.mark.asyncio
async def test_create_audio_with_custom_speaker():
# Arrange (準備)
client = VoicevoxClient()
client.session = AsyncMock()
mock_response = create_mock_audio_response(b"audio_data")
client.session.post.return_value.__aenter__.return_value = mock_response
# Act (実行)
result = await client.create_audio("テスト", speaker_id=3, speed=1.2)
# Assert (検証)
assert result == b"audio_data"
client.session.post.assert_called_once()
import discord
from discord import app_commands
from unittest.mock import AsyncMock, MagicMock
import pytest
@pytest.mark.asyncio
async def test_slash_command_join_voice_channel():
"""ボイスチャンネル参加コマンドのテスト"""
# Arrange
bot = MagicMock(spec=VoiceBot)
interaction = MagicMock(spec=discord.Interaction)
# ユーザーがボイスチャンネルにいる状態をモック
voice_channel = MagicMock(spec=discord.VoiceChannel)
interaction.user.voice.channel = voice_channel
interaction.response.send_message = AsyncMock()
# Botがまだ接続していない状態
interaction.guild.voice_client = None
voice_channel.connect = AsyncMock()
# Act
await join_command(interaction, bot)
# Assert
voice_channel.connect.assert_called_once()
interaction.response.send_message.assert_called_once()
assert "参加しました" in str(interaction.response.send_message.call_args)
@pytest.mark.asyncio
async def test_slash_command_user_not_in_voice():
"""ユーザーがボイスチャンネルにいない場合のエラー処理"""
# Arrange
bot = MagicMock(spec=VoiceBot)
interaction = MagicMock(spec=discord.Interaction)
interaction.user.voice = None # ボイスチャンネルにいない
interaction.response.send_message = AsyncMock()
# Act
await join_command(interaction, bot)
# Assert
interaction.response.send_message.assert_called_once()
call_args = str(interaction.response.send_message.call_args)
assert "ボイスチャンネルに参加してください" in call_args or "エラー" in call_args
@pytest.mark.asyncio
async def test_voice_playback_with_queue():
"""音声キューを使用した再生のテスト"""
# Arrange
bot = VoiceBot()
guild_id = 12345
bot.voice_queues[guild_id] = asyncio.Queue()
voice_client = MagicMock(spec=discord.VoiceClient)
voice_client.is_playing.return_value = False
voice_client.play = MagicMock()
# キューに音声データを追加
audio_source = MagicMock(spec=discord.FFmpegPCMAudio)
await bot.voice_queues[guild_id].put(audio_source)
# Act
await bot.process_voice_queue(guild_id, voice_client)
# Assert
voice_client.play.assert_called_once()
assert bot.voice_queues[guild_id].empty()
@pytest.mark.asyncio
async def test_on_message_text_to_speech():
"""メッセージを受信して読み上げる処理のテスト"""
# Arrange
bot = VoiceBot()
bot.voicevox = AsyncMock(spec=VoicevoxClient)
bot.voicevox.create_audio = AsyncMock(return_value=b"audio_data")
message = MagicMock(spec=discord.Message)
message.author.bot = False
message.content = "こんにちは"
message.guild.id = 12345
message.author.id = 67890
# ボイスクライアントをモック
voice_client = MagicMock(spec=discord.VoiceClient)
message.guild.voice_client = voice_client
# Act
await bot.on_message(message)
# Assert
bot.voicevox.create_audio.assert_called_once_with(
"こんにちは",
speaker_id=bot.user_speakers.get(67890, 1),
speed=bot.user_speeds.get(67890, 1.0)
)
import aiohttp
from unittest.mock import AsyncMock, patch
import pytest
@pytest.fixture
async def voicevox_client():
"""VOICEVOX Clientのフィクスチャ"""
client = VoicevoxClient("http://localhost:50021")
await client.initialize()
yield client
await client.close()
@pytest.mark.asyncio
async def test_get_speakers_success(voicevox_client):
"""話者一覧取得の成功ケース"""
# Arrange
expected_speakers = [
{"name": "四国めたん", "speaker_uuid": "uuid1"},
{"name": "ずんだもん", "speaker_uuid": "uuid2"}
]
mock_response = AsyncMock()
mock_response.status = 200
mock_response.json = AsyncMock(return_value=expected_speakers)
voicevox_client.session.get = AsyncMock(
return_value=AsyncMockContext(mock_response)
)
# Act
speakers = await voicevox_client.get_speakers()
# Assert
assert len(speakers) == 2
assert speakers[0]["name"] == "四国めたん"
voicevox_client.session.get.assert_called_once_with(
"http://localhost:50021/speakers"
)
@pytest.mark.asyncio
async def test_create_audio_api_error(voicevox_client):
"""音声生成APIエラー時の処理"""
# Arrange
mock_response = AsyncMock()
mock_response.status = 500
voicevox_client.session.post = AsyncMock(
return_value=AsyncMockContext(mock_response)
)
# Act
result = await voicevox_client.create_audio("テスト", speaker_id=1)
# Assert
assert result is None # エラー時はNoneを返す
@pytest.mark.asyncio
async def test_create_audio_timeout():
"""タイムアウト時の処理"""
# Arrange
client = VoicevoxClient()
client.session = AsyncMock()
client.session.post.side_effect = asyncio.TimeoutError()
# Act
result = await client.create_audio("テスト", speaker_id=1)
# Assert
assert result is None
class AsyncMockContext:
"""aiohttpのレスポンスコンテキストマネージャーのモック"""
def __init__(self, response):
self.response = response
async def __aenter__(self):
return self.response
async def __aexit__(self, exc_type, exc_val, exc_tb):
pass
import json
import tempfile
from pathlib import Path
def test_load_config_file():
"""設定ファイル読み込みのテスト"""
# Arrange
with tempfile.TemporaryDirectory() as tmpdir:
config_path = Path(tmpdir) / "config.json"
config_data = {
"user_speakers": {"12345": 3, "67890": 5},
"user_speeds": {"12345": 1.2}
}
config_path.write_text(json.dumps(config_data), encoding="utf-8")
# Act
bot = VoiceBot()
bot.CONFIG_FILE = config_path
bot._load_config()
# Assert
assert bot.user_speakers[12345] == 3
assert bot.user_speeds[12345] == 1.2
def test_save_config_file():
"""設定ファイル保存のテスト"""
# Arrange
with tempfile.TemporaryDirectory() as tmpdir:
config_path = Path(tmpdir) / "config.json"
bot = VoiceBot()
bot.CONFIG_FILE = config_path
bot.user_speakers = {12345: 3}
bot.user_speeds = {12345: 1.5}
# Act
bot._save_config()
# Assert
assert config_path.exists()
saved_data = json.loads(config_path.read_text(encoding="utf-8"))
assert saved_data["user_speakers"]["12345"] == 3
assert saved_data["user_speeds"]["12345"] == 1.5
pytest.ini)[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
asyncio_mode = auto
markers =
asyncio: mark test as async
slow: mark test as slow running
integration: mark test as integration test
requirements-dev.txt)pytest>=7.4.0
pytest-asyncio>=0.21.0
pytest-cov>=4.1.0
pytest-mock>=3.11.0
# すべてのテストを実行
pytest
# カバレッジ付きで実行
pytest --cov=src --cov-report=html
# 特定のテストファイルのみ
pytest tests/test_bot.py
# 詳細出力
pytest -v
# 失敗したテストのみ再実行
pytest --lf
テストコードとプロダクションコードはセット
外部依存は必ずモック化
テストは独立して実行可能
命名規則を守る
def test_<機能名>_<条件>_<期待結果>():
pass
# 例:
def test_join_voice_channel_when_user_in_channel_succeeds():
pass
def test_create_audio_when_api_timeout_returns_none():
pass
適切なカバレッジを維持
単体テスト(Unit Tests):
統合テスト(Integration Tests):
Discord Bot関連のテスト:
# ✅ 良い例: 明確な状態設定
interaction = MagicMock(spec=discord.Interaction)
interaction.user.voice.channel = MagicMock(spec=discord.VoiceChannel)
# ❌ 悪い例: 曖昧なモック
interaction = MagicMock()
interaction.user = MagicMock()
VOICEVOX連携のテスト:
# ✅ 良い例: コンテキストマネージャーを適切にモック
mock_response = AsyncMock()
mock_response.status = 200
client.session.get.return_value = AsyncMockContext(mock_response)
# ❌ 悪い例: コンテキストマネージャーを無視
mock_response = AsyncMock()
client.session.get.return_value = mock_response
非同期処理のテスト:
# ✅ 良い例: @pytest.mark.asyncioを使用
@pytest.mark.asyncio
async def test_async_function():
result = await async_function()
assert result is not None
# ❌ 悪い例: 同期テストとして書く
def test_async_function():
result = async_function() # awaitなし
assert result is not None
requirements-dev.txt)# テストフレームワーク
pytest>=7.4.0
pytest-asyncio>=0.21.0
pytest-cov>=4.1.0
pytest-mock>=3.11.0
# カバレッジレポート
coverage>=7.3.0
pytest.ini)[pytest]
testpaths = tests
python_files = test_*.py
python_classes = Test*
python_functions = test_*
asyncio_mode = auto
markers =
asyncio: mark test as async
slow: mark test as slow running
integration: mark test as integration test
unit: mark test as unit test
# すべてのテストを実行
pytest
# カバレッジ付きで実行
pytest --cov=src --cov-report=html --cov-report=term
# 特定のテストファイルのみ
pytest tests/test_bot.py
# 詳細出力
pytest -v
# 失敗したテストのみ再実行
pytest --lf
# マーカーでフィルタリング
pytest -m "not slow"
必ず以下を同時に生成:
テンプレート:
# 1. テスト(先に書く)
@pytest.mark.asyncio
async def test_new_feature_success():
"""機能が正常に動作することを確認"""
# Arrange
...
# Act
...
# Assert
...
# 2. 実装(テストが通るように)
async def new_feature():
"""新機能の実装"""
...
| カテゴリ | 目標カバレッジ | 対象ファイル |
|---|---|---|
| コアロジック | 90%以上 | bot.py, voicevox_client.py |
| ユーティリティ | 80%以上 | その他のsrc/配下 |
| 全体 | 80%以上 | プロジェクト全体 |
| 新規追加コード | 100% | 新しく追加した機能 |
# 問題: interactionの属性が複雑でモックが難しい
# 解決策: specを使用して型を明示
interaction = MagicMock(spec=discord.Interaction)
interaction.guild = MagicMock(spec=discord.Guild)
interaction.guild.id = 12345
interaction.user = MagicMock(spec=discord.Member)
interaction.user.voice.channel = MagicMock(spec=discord.VoiceChannel)
interaction.response.send_message = AsyncMock()
# 問題: aiohttpはコンテキストマネージャーを使用
# 解決策: AsyncMockContext クラスを作成
class AsyncMockContext:
def __init__(self, response):
self.response = response
async def __aenter__(self):
return self.response
async def __aexit__(self, exc_type, exc_val, exc_tb):
pass
# 使用例
mock_response = AsyncMock()
mock_response.status = 200
mock_response.json = AsyncMock(return_value=[...])
client.session.get.return_value = AsyncMockContext(mock_response)
# 問題: 実際の音声ファイル生成は時間がかかる
# 解決策: tempfileとモックを組み合わせる
import tempfile
from pathlib import Path
def test_save_audio_file():
with tempfile.TemporaryDirectory() as tmpdir:
audio_path = Path(tmpdir) / "test.wav"
# テスト実行
save_audio(b"audio_data", audio_path)
# 検証
assert audio_path.exists()
assert audio_path.read_bytes() == b"audio_data"
# 詳細なエラー情報を表示
pytest -vv
# 最後に失敗したテストのみ再実行
pytest --lf
# ステップごとにデバッグ
pytest --pdb
# 特定のテストのみ実行
pytest tests/test_bot.py::test_specific_function
# モックの呼び出し履歴を確認
mock_object.method.assert_called()
print(mock_object.method.call_args)
print(mock_object.method.call_args_list)
# モックがどのように呼ばれたか詳細に確認
import pprint
pprint.pprint(mock_object.method.call_args_list)
このスキルを活用することで、TalkBot2プロジェクトは以下を実現します:
✅ 高品質なコードベース
✅ 安心してリファクタリング可能
✅ 新規開発者のオンボーディング容易化
✅ 継続的な品質向上
すべての変更は、テストとともに。