TradingAgents/docs/014_Reddit_Testing_Implemen...

9.4 KiB
Raw Blame History

チケット #014: Redditテスト実装

概要

Reddit praw実装の包括的なテストスイートの実装

目的

  • 各コンポーネントの単体テスト
  • 統合テストの実装
  • モックを使用したAPI呼び出しのテスト
  • 既存システムとの互換性テスト

実装要件

1. テスト優先開発TDDアプローチ

# テストを先に作成してから実装を行う
# 各モジュールの実装前にテストを定義

2. テスト構造

tests/
├── unit/
│   ├── test_reddit_praw_client.py
│   ├── test_reddit_data_fetcher.py
│   ├── test_reddit_cache_manager.py
│   └── test_reddit_utils_compatibility.py
├── integration/
│   ├── test_reddit_cli_commands.py
│   ├── test_reddit_full_flow.py
│   └── test_reddit_auto_update.py
├── fixtures/
│   ├── reddit_mock_data.py
│   └── reddit_test_config.py
└── conftest.py

3. RedditPrawClient テスト

# tests/unit/test_reddit_praw_client.py

class TestRedditPrawClient:
    @pytest.fixture
    def mock_reddit(self):
        """prawのRedditオブジェクトをモック"""
        with patch('praw.Reddit') as mock:
            yield mock
    
    def test_authentication_success(self, mock_reddit):
        """認証成功のテスト"""
        client = RedditPrawClient(test_config)
        assert client.authenticate() is True
    
    def test_authentication_failure(self, mock_reddit):
        """認証失敗のテスト"""
        mock_reddit.side_effect = Exception("Invalid credentials")
        client = RedditPrawClient(invalid_config)
        assert client.authenticate() is False
    
    def test_get_subreddit_posts(self, mock_reddit):
        """subreddit投稿取得のテスト"""
        # モックデータ設定
        mock_posts = create_mock_posts(count=10)
        mock_reddit.return_value.subreddit.return_value.hot.return_value = mock_posts
        
        client = RedditPrawClient(test_config)
        posts = client.get_subreddit_posts("worldnews", limit=10)
        
        assert len(posts) == 10
        assert all('title' in post for post in posts)

4. RedditDataFetcher テスト

# tests/unit/test_reddit_data_fetcher.py

class TestRedditDataFetcher:
    def test_fetch_global_news(self, mock_client):
        """グローバルニュース取得のテスト"""
        fetcher = RedditDataFetcher(mock_client, test_config)
        
        posts = fetcher.fetch_global_news("2024-01-01", limit_per_subreddit=5)
        
        # 5 subreddits × 5 posts = 25 posts expected
        assert len(posts) == 25
        assert all(post['posted_date'] == "2024-01-01" for post in posts)
    
    def test_company_news_filtering(self):
        """企業関連投稿のフィルタリングテスト"""
        posts = [
            {"title": "Apple announces new iPhone", "selftext": "..."},
            {"title": "Random tech news", "selftext": "..."},
            {"title": "AAPL stock rises", "selftext": "..."},
        ]
        
        filtered = filter_company_relevant_posts(posts, "AAPL", "Apple")
        assert len(filtered) == 2
    
    def test_duplicate_removal(self):
        """重複排除のテスト"""
        fetcher = RedditDataFetcher(mock_client, test_config)
        
        posts_with_duplicates = [
            {"id": "abc123", "title": "Post 1"},
            {"id": "def456", "title": "Post 2"},
            {"id": "abc123", "title": "Post 1"},  # 重複
        ]
        
        unique_posts = fetcher.remove_duplicates(posts_with_duplicates)
        assert len(unique_posts) == 2

5. RedditCacheManager テスト

# tests/unit/test_reddit_cache_manager.py

class TestRedditCacheManager:
    @pytest.fixture
    def temp_cache_dir(self, tmp_path):
        """テスト用の一時ディレクトリ"""
        return tmp_path / "reddit_data"
    
    def test_save_and_load_posts(self, temp_cache_dir):
        """投稿の保存と読み込みテスト"""
        manager = RedditCacheManager(str(temp_cache_dir))
        
        test_posts = [
            {"id": "1", "title": "Test Post 1"},
            {"id": "2", "title": "Test Post 2"},
        ]
        
        # 保存
        file_path = manager.save_posts(
            test_posts, "global_news", "2024-01-01", "worldnews"
        )
        assert Path(file_path).exists()
        
        # 読み込み
        loaded_posts = manager.load_posts(
            "global_news", "2024-01-01", "worldnews"
        )
        assert len(loaded_posts) == 2
        assert loaded_posts[0]["title"] == "Test Post 1"
    
    def test_fetch_history_tracking(self, temp_cache_dir):
        """取得履歴の記録テスト"""
        manager = RedditCacheManager(str(temp_cache_dir))
        
        manager.update_fetch_history(
            "global_news",
            "2024-01-01",
            ["worldnews", "news"],
            "2024-01-02T10:00:00Z",
            100
        )
        
        history = manager.get_fetch_history()
        assert "global_news" in history
        assert history["global_news"]["2024-01-01"]["post_count"] == 100

6. 統合テスト

# tests/integration/test_reddit_full_flow.py

@pytest.mark.integration
class TestRedditFullFlow:
    def test_end_to_end_data_fetch(self):
        """完全なデータ取得フローのテスト"""
        # 実際のAPIは使わず、モックで代替
        with patch('praw.Reddit') as mock_reddit:
            setup_mock_reddit_responses(mock_reddit)
            
            # CLI実行
            result = runner.invoke(cli, [
                'reddit', 'fetch-historical',
                '--no-interactive',
                '--start', '2024-01-01',
                '--end', '2024-01-01',
                '--category', 'global_news'
            ])
            
            assert result.exit_code == 0
            
            # データが保存されたか確認
            cache_manager = RedditCacheManager(test_data_dir)
            posts = cache_manager.load_posts("global_news", "2024-01-01")
            assert len(posts) > 0

7. モックデータ生成

# tests/fixtures/reddit_mock_data.py

def create_mock_post(post_id: str = None, **kwargs):
    """モック投稿データの生成"""
    post = {
        "id": post_id or str(uuid.uuid4()),
        "title": kwargs.get("title", "Test Post Title"),
        "selftext": kwargs.get("selftext", "Test post content"),
        "url": kwargs.get("url", "https://reddit.com/test"),
        "ups": kwargs.get("ups", random.randint(1, 1000)),
        "created_utc": kwargs.get("created_utc", int(time.time())),
        "subreddit": kwargs.get("subreddit", "test"),
        "author": kwargs.get("author", "test_user"),
        "num_comments": kwargs.get("num_comments", random.randint(0, 100))
    }
    return post

def create_mock_posts(count: int = 10, **kwargs):
    """複数のモック投稿を生成"""
    return [create_mock_post(f"post_{i}", **kwargs) for i in range(count)]

8. 互換性テスト

# tests/unit/test_reddit_utils_compatibility.py

class TestRedditUtilsCompatibility:
    def test_legacy_interface_maintained(self):
        """既存インターフェースが維持されているか"""
        # 既存の関数シグネチャでの呼び出し
        result = fetch_top_from_category(
            "global_news",
            "2024-01-01",
            100,
            data_path="test_data"
        )
        
        assert isinstance(result, list)
        assert all(isinstance(post, dict) for post in result)
    
    def test_data_format_conversion(self):
        """データ形式の変換テスト"""
        praw_posts = create_mock_posts(5)
        legacy_posts = convert_praw_to_legacy_format(praw_posts)
        
        # 既存形式のフィールドが存在するか
        for post in legacy_posts:
            assert "content" in post  # selftextから変換
            assert "upvotes" in post  # upsから変換
            assert "posted_date" in post  # created_utcから変換

9. パフォーマンステスト

@pytest.mark.performance
def test_large_data_handling():
    """大量データ処理のパフォーマンステスト"""
    start_time = time.time()
    
    # 1000件のモックデータで処理
    large_dataset = create_mock_posts(1000)
    manager = RedditCacheManager(test_dir)
    manager.save_posts(large_dataset, "test", "2024-01-01")
    
    duration = time.time() - start_time
    assert duration < 5.0  # 5秒以内に完了

受け入れ条件

  • テスト優先開発TDDの実践
  • 全モジュールの単体テスト実装
  • 統合テストの成功
  • コードカバレッジ80%以上
  • モックを使用したAPI非依存テスト
  • 既存システムとの互換性確認
  • パフォーマンステストの合格
  • USE_PRAW_APIフラグのテスト

依存関係

  • pytest
  • pytest-mock
  • pytest-cov
  • 全Reddit実装モジュール

タスク

  • テストディレクトリ構造の作成
  • モックデータ生成ユーティリティの作成
  • RedditPrawClientのテスト作成
  • RedditDataFetcherのテスト作成
  • RedditCacheManagerのテスト作成
  • CLIコマンドのテスト作成
  • 統合テストの実装
  • 互換性テスト
  • 段階的実装フラグのテスト
  • パフォーマンステスト
  • CI/CD設定GitHub Actions