From 4756c20ec0947a37a7cf02ac84a994f60150d534 Mon Sep 17 00:00:00 2001 From: P Date: Mon, 9 Mar 2026 16:43:04 -0400 Subject: [PATCH] refactor: match upstream unittest convention Convert all new tests from bare pytest style to unittest.TestCase with sys.path.insert, matching the convention used by all existing tests. Remove pyproject.toml and conftest.py. Co-Authored-By: Claude Opus 4.6 --- pyproject.toml | 3 -- tests/conftest.py | 33 -------------- tests/test_instagram_sc.py | 60 ++++++++++++++++---------- tests/test_reddit_enrich.py | 79 +++++++++++++++++++++------------- tests/test_reddit_sc.py | 75 +++++++++++++++++++------------- tests/test_schema_roundtrip.py | 77 +++++++++++++++++++-------------- 6 files changed, 175 insertions(+), 152 deletions(-) delete mode 100644 pyproject.toml delete mode 100644 tests/conftest.py diff --git a/pyproject.toml b/pyproject.toml deleted file mode 100644 index 5364782..0000000 --- a/pyproject.toml +++ /dev/null @@ -1,3 +0,0 @@ -[tool.pytest.ini_options] -testpaths = ["tests"] -pythonpath = ["scripts"] diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 2a75c30..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Shared pytest fixtures for last30days tests.""" - -import json -from pathlib import Path - -import pytest - - -@pytest.fixture -def project_root(): - """Path to the repository root.""" - return Path(__file__).parent.parent - - -@pytest.fixture -def fixtures_dir(project_root): - """Path to the fixtures directory.""" - return project_root / "fixtures" - - -@pytest.fixture -def tmp_config_dir(tmp_path): - """Temporary directory for config file tests. Auto-cleaned by pytest.""" - return tmp_path - - -@pytest.fixture -def load_fixture(fixtures_dir): - """Load a JSON fixture file by name.""" - def _load(name): - with open(fixtures_dir / name) as f: - return json.load(f) - return _load diff --git a/tests/test_instagram_sc.py b/tests/test_instagram_sc.py index 1b455f9..0076c80 100644 --- a/tests/test_instagram_sc.py +++ b/tests/test_instagram_sc.py @@ -1,74 +1,88 @@ """Tests for instagram.py — ScrapeCreators Instagram search module.""" +import sys +import unittest +from pathlib import Path + +# Add lib to path +sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) + from lib import instagram -class TestTokenize: +class TestTokenize(unittest.TestCase): """Tests for _tokenize().""" def test_strips_stopwords(self): tokens = instagram._tokenize("how to use the AI tools") - assert "how" not in tokens - assert "the" not in tokens - assert "to" not in tokens + self.assertNotIn("how", tokens) + self.assertNotIn("the", tokens) + self.assertNotIn("to", tokens) def test_expands_synonyms(self): tokens = instagram._tokenize("ai tools") - assert "artificial" in tokens or "intelligence" in tokens + self.assertTrue("artificial" in tokens or "intelligence" in tokens) def test_removes_single_char(self): tokens = instagram._tokenize("a b c python") - assert "a" not in tokens - assert "b" not in tokens - assert "python" in tokens + self.assertNotIn("a", tokens) + self.assertNotIn("b", tokens) + self.assertIn("python", tokens) def test_lowercases(self): tokens = instagram._tokenize("Python REACT") - assert "python" in tokens - assert "react" in tokens + self.assertIn("python", tokens) + self.assertIn("react", tokens) def test_strips_punctuation(self): tokens = instagram._tokenize("hello, world!") - assert "hello" in tokens - assert "world" in tokens + self.assertIn("hello", tokens) + self.assertIn("world", tokens) -class TestComputeRelevance: +class TestComputeRelevance(unittest.TestCase): """Tests for _compute_relevance().""" def test_exact_match_high(self): rel = instagram._compute_relevance("claude code", "Claude Code tricks and tips") - assert rel >= 0.8 + self.assertGreaterEqual(rel, 0.8) def test_partial_match_lower(self): rel = instagram._compute_relevance("claude code tips", "Best AI tools for coding") - assert rel < 0.5 + self.assertLess(rel, 0.5) def test_hashtag_boost(self): base = instagram._compute_relevance("claude code", "random video about stuff") boosted = instagram._compute_relevance("claude code", "random video about stuff", ["claudecode", "ai"]) - assert boosted > base + self.assertGreater(boosted, base) def test_floor_at_01(self): rel = instagram._compute_relevance("quantum physics", "cat dancing video") - assert rel >= 0.1 + self.assertGreaterEqual(rel, 0.1) def test_empty_query_returns_default(self): rel = instagram._compute_relevance("", "Some video title") - assert rel == 0.5 + self.assertEqual(rel, 0.5) -class TestInstagramDepthConfig: +class TestInstagramDepthConfig(unittest.TestCase): """Tests for DEPTH_CONFIG.""" def test_all_depths_exist(self): for depth in ("quick", "default", "deep"): - assert depth in instagram.DEPTH_CONFIG + self.assertIn(depth, instagram.DEPTH_CONFIG) def test_required_keys(self): for depth, config in instagram.DEPTH_CONFIG.items(): - assert "results_per_page" in config - assert "max_captions" in config + self.assertIn("results_per_page", config) + self.assertIn("max_captions", config) def test_deep_has_more_results(self): - assert instagram.DEPTH_CONFIG["deep"]["results_per_page"] > instagram.DEPTH_CONFIG["quick"]["results_per_page"] + self.assertGreater( + instagram.DEPTH_CONFIG["deep"]["results_per_page"], + instagram.DEPTH_CONFIG["quick"]["results_per_page"], + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_reddit_enrich.py b/tests/test_reddit_enrich.py index 72c1887..c093f82 100644 --- a/tests/test_reddit_enrich.py +++ b/tests/test_reddit_enrich.py @@ -1,58 +1,73 @@ """Tests for reddit_enrich.py — comment enrichment and parsing.""" +import json +import sys +import unittest +from pathlib import Path + +# Add lib to path +sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) + from lib import reddit_enrich +FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" -class TestExtractRedditPath: + +def _load_fixture(name): + with open(FIXTURES_DIR / name) as f: + return json.load(f) + + +class TestExtractRedditPath(unittest.TestCase): """Tests for extract_reddit_path().""" def test_valid_url(self): url = "https://www.reddit.com/r/ClaudeAI/comments/abc123/post_title/" path = reddit_enrich.extract_reddit_path(url) - assert path == "/r/ClaudeAI/comments/abc123/post_title/" + self.assertEqual(path, "/r/ClaudeAI/comments/abc123/post_title/") def test_non_reddit_url(self): - assert reddit_enrich.extract_reddit_path("https://example.com/foo") is None + self.assertIsNone(reddit_enrich.extract_reddit_path("https://example.com/foo")) def test_empty_string(self): - assert reddit_enrich.extract_reddit_path("") is None + self.assertIsNone(reddit_enrich.extract_reddit_path("")) def test_old_reddit(self): url = "https://old.reddit.com/r/test/comments/xyz/" - assert reddit_enrich.extract_reddit_path(url) is not None + self.assertIsNotNone(reddit_enrich.extract_reddit_path(url)) -class TestParseThreadData: +class TestParseThreadData(unittest.TestCase): """Tests for parse_thread_data() using fixture.""" - def test_parses_submission(self, load_fixture): - data = load_fixture("reddit_thread_sample.json") + def test_parses_submission(self): + data = _load_fixture("reddit_thread_sample.json") result = reddit_enrich.parse_thread_data(data) - assert result["submission"] is not None - assert result["submission"]["score"] == 847 - assert result["submission"]["num_comments"] == 156 + self.assertIsNotNone(result["submission"]) + self.assertEqual(result["submission"]["score"], 847) + self.assertEqual(result["submission"]["num_comments"], 156) - def test_parses_comments(self, load_fixture): - data = load_fixture("reddit_thread_sample.json") + def test_parses_comments(self): + data = _load_fixture("reddit_thread_sample.json") result = reddit_enrich.parse_thread_data(data) - assert len(result["comments"]) == 8 - assert result["comments"][0]["author"] == "skill_expert" + self.assertEqual(len(result["comments"]), 8) + self.assertEqual(result["comments"][0]["author"], "skill_expert") def test_empty_input(self): result = reddit_enrich.parse_thread_data([]) - assert result["submission"] is None - assert result["comments"] == [] + self.assertIsNone(result["submission"]) + self.assertEqual(result["comments"], []) def test_malformed_input(self): result = reddit_enrich.parse_thread_data("not a list") - assert result["submission"] is None + self.assertIsNone(result["submission"]) def test_none_input(self): result = reddit_enrich.parse_thread_data(None) - assert result["submission"] is None + self.assertIsNone(result["submission"]) -class TestGetTopComments: +class TestGetTopComments(unittest.TestCase): """Tests for get_top_comments().""" def test_sorted_by_score(self): @@ -62,8 +77,8 @@ class TestGetTopComments: {"score": 50, "author": "c"}, ] top = reddit_enrich.get_top_comments(comments, limit=3) - assert top[0]["score"] == 100 - assert top[1]["score"] == 50 + self.assertEqual(top[0]["score"], 100) + self.assertEqual(top[1]["score"], 50) def test_filters_deleted(self): comments = [ @@ -72,19 +87,19 @@ class TestGetTopComments: {"score": 10, "author": "real_user"}, ] top = reddit_enrich.get_top_comments(comments) - assert len(top) == 1 - assert top[0]["author"] == "real_user" + self.assertEqual(len(top), 1) + self.assertEqual(top[0]["author"], "real_user") def test_respects_limit(self): comments = [{"score": i, "author": f"u{i}"} for i in range(20)] top = reddit_enrich.get_top_comments(comments, limit=5) - assert len(top) == 5 + self.assertEqual(len(top), 5) def test_empty_list(self): - assert reddit_enrich.get_top_comments([]) == [] + self.assertEqual(reddit_enrich.get_top_comments([]), []) -class TestExtractCommentInsights: +class TestExtractCommentInsights(unittest.TestCase): """Tests for extract_comment_insights().""" def test_filters_short_comments(self): @@ -93,7 +108,7 @@ class TestExtractCommentInsights: {"body": "A" * 50 + " this is a substantive comment about the topic."}, ] insights = reddit_enrich.extract_comment_insights(comments) - assert len(insights) == 1 + self.assertEqual(len(insights), 1) def test_filters_low_value_patterns(self): comments = [ @@ -102,9 +117,13 @@ class TestExtractCommentInsights: {"body": "A" * 50 + " Here's a real insight about how to approach this problem."}, ] insights = reddit_enrich.extract_comment_insights(comments) - assert len(insights) == 1 + self.assertEqual(len(insights), 1) def test_respects_limit(self): comments = [{"body": f"Comment number {i} " + "x" * 50} for i in range(20)] insights = reddit_enrich.extract_comment_insights(comments, limit=3) - assert len(insights) <= 3 + self.assertLessEqual(len(insights), 3) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_reddit_sc.py b/tests/test_reddit_sc.py index 52b2129..d621de8 100644 --- a/tests/test_reddit_sc.py +++ b/tests/test_reddit_sc.py @@ -1,59 +1,66 @@ """Tests for reddit.py — ScrapeCreators Reddit search module.""" +import sys +import unittest +from pathlib import Path + +# Add lib to path +sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) + from lib import reddit -class TestExtractCoreSubject: +class TestExtractCoreSubject(unittest.TestCase): """Tests for _extract_core_subject().""" def test_strips_what_are_prefix(self): - assert reddit._extract_core_subject("what are the best AI tools") == "ai tools" + self.assertEqual(reddit._extract_core_subject("what are the best AI tools"), "ai tools") def test_strips_how_to_prefix(self): - assert reddit._extract_core_subject("how to use cursor IDE") == "cursor ide" + self.assertEqual(reddit._extract_core_subject("how to use cursor IDE"), "cursor ide") def test_strips_noise_words(self): result = reddit._extract_core_subject("latest trending updates") - assert result == "latest trending updates" + self.assertEqual(result, "latest trending updates") def test_preserves_product_name(self): - assert reddit._extract_core_subject("cursor IDE") == "cursor ide" + self.assertEqual(reddit._extract_core_subject("cursor IDE"), "cursor ide") def test_strips_trailing_punctuation(self): result = reddit._extract_core_subject("what is Claude?") - assert not result.endswith("?") + self.assertFalse(result.endswith("?")) def test_empty_string(self): result = reddit._extract_core_subject("") - assert result == "" + self.assertEqual(result, "") def test_strips_what_do_people_think(self): result = reddit._extract_core_subject("what do people think about React Server Components") - assert result == "react server components" + self.assertEqual(result, "react server components") -class TestExpandRedditQueries: +class TestExpandRedditQueries(unittest.TestCase): """Tests for expand_reddit_queries().""" def test_quick_returns_one_query(self): queries = reddit.expand_reddit_queries("cursor IDE", "quick") - assert len(queries) >= 1 + self.assertGreaterEqual(len(queries), 1) def test_default_includes_review_variant(self): queries = reddit.expand_reddit_queries("cursor IDE", "default") - assert any("worth it" in q or "review" in q for q in queries) + self.assertTrue(any("worth it" in q or "review" in q for q in queries)) def test_deep_includes_issues_variant(self): queries = reddit.expand_reddit_queries("cursor IDE", "deep") - assert any("issues" in q or "problems" in q for q in queries) + self.assertTrue(any("issues" in q or "problems" in q for q in queries)) def test_deep_has_more_queries_than_quick(self): quick = reddit.expand_reddit_queries("cursor IDE", "quick") deep = reddit.expand_reddit_queries("cursor IDE", "deep") - assert len(deep) > len(quick) + self.assertGreater(len(deep), len(quick)) -class TestDiscoverSubreddits: +class TestDiscoverSubreddits(unittest.TestCase): """Tests for discover_subreddits().""" def test_ranks_by_frequency(self): @@ -63,7 +70,7 @@ class TestDiscoverSubreddits: {"subreddit": "python", "score": 5}, ] subs = reddit.discover_subreddits(results, max_subs=5) - assert subs[0] == "programming" + self.assertEqual(subs[0], "programming") def test_utility_sub_penalty(self): results = [ @@ -72,7 +79,7 @@ class TestDiscoverSubreddits: {"subreddit": "python", "score": 10}, ] subs = reddit.discover_subreddits(results, topic="python", max_subs=5) - assert subs[0] == "python" + self.assertEqual(subs[0], "python") def test_topic_name_bonus(self): results = [ @@ -80,7 +87,7 @@ class TestDiscoverSubreddits: {"subreddit": "webdev", "score": 10}, ] subs = reddit.discover_subreddits(results, topic="react hooks", max_subs=5) - assert subs[0] == "reactjs" + self.assertEqual(subs[0], "reactjs") def test_engagement_bonus(self): results = [ @@ -88,48 +95,56 @@ class TestDiscoverSubreddits: {"subreddit": "OtherSub", "ups": 5}, ] subs = reddit.discover_subreddits(results, max_subs=5) - assert subs[0] == "AIsub" + self.assertEqual(subs[0], "AIsub") def test_max_subs_limit(self): results = [{"subreddit": f"sub{i}"} for i in range(20)] subs = reddit.discover_subreddits(results, max_subs=3) - assert len(subs) <= 3 + self.assertLessEqual(len(subs), 3) def test_empty_results(self): - assert reddit.discover_subreddits([]) == [] + self.assertEqual(reddit.discover_subreddits([]), []) def test_missing_subreddit_field(self): results = [{"title": "no sub field"}] - assert reddit.discover_subreddits(results) == [] + self.assertEqual(reddit.discover_subreddits(results), []) -class TestParseDate: +class TestParseDate(unittest.TestCase): """Tests for _parse_date().""" def test_valid_timestamp(self): - assert reddit._parse_date(1705363200) == "2024-01-16" + self.assertEqual(reddit._parse_date(1705363200), "2024-01-16") def test_string_timestamp(self): - assert reddit._parse_date("1705363200") == "2024-01-16" + self.assertEqual(reddit._parse_date("1705363200"), "2024-01-16") def test_none_returns_none(self): - assert reddit._parse_date(None) is None + self.assertIsNone(reddit._parse_date(None)) def test_zero_returns_none(self): - assert reddit._parse_date(0) is None + self.assertIsNone(reddit._parse_date(0)) -class TestDepthConfig: +class TestDepthConfig(unittest.TestCase): """Tests for DEPTH_CONFIG structure.""" def test_all_depths_exist(self): for depth in ("quick", "default", "deep"): - assert depth in reddit.DEPTH_CONFIG + self.assertIn(depth, reddit.DEPTH_CONFIG) def test_required_keys(self): required = {"global_searches", "subreddit_searches", "comment_enrichments", "timeframe"} for depth, config in reddit.DEPTH_CONFIG.items(): - assert required.issubset(config.keys()), f"Missing keys in {depth}: {required - config.keys()}" + self.assertTrue(required.issubset(config.keys()), + f"Missing keys in {depth}: {required - config.keys()}") def test_deep_has_more_searches(self): - assert reddit.DEPTH_CONFIG["deep"]["global_searches"] > reddit.DEPTH_CONFIG["quick"]["global_searches"] + self.assertGreater( + reddit.DEPTH_CONFIG["deep"]["global_searches"], + reddit.DEPTH_CONFIG["quick"]["global_searches"], + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_schema_roundtrip.py b/tests/test_schema_roundtrip.py index 4362899..25f0b50 100644 --- a/tests/test_schema_roundtrip.py +++ b/tests/test_schema_roundtrip.py @@ -1,20 +1,27 @@ """Tests for schema.py — data class serialization roundtrips.""" +import sys +import unittest +from pathlib import Path + +# Add lib to path +sys.path.insert(0, str(Path(__file__).parent.parent / "scripts")) + from lib import schema -class TestEngagement: +class TestEngagement(unittest.TestCase): """Tests for Engagement.to_dict().""" def test_sparse_fields(self): eng = schema.Engagement(score=100, num_comments=50) d = eng.to_dict() - assert d == {"score": 100, "num_comments": 50} - assert "likes" not in d + self.assertEqual(d, {"score": 100, "num_comments": 50}) + self.assertNotIn("likes", d) def test_all_none_returns_none(self): eng = schema.Engagement() - assert eng.to_dict() is None + self.assertIsNone(eng.to_dict()) def test_all_fields(self): eng = schema.Engagement( @@ -23,19 +30,19 @@ class TestEngagement: views=7, shares=8, volume=9.0, liquidity=10.0, ) d = eng.to_dict() - assert len(d) == 11 + self.assertEqual(len(d), 11) -class TestComment: +class TestComment(unittest.TestCase): def test_basic(self): c = schema.Comment(score=50, date="2026-03-01", author="user", excerpt="text", url="http://x") d = c.to_dict() - assert d["score"] == 50 - assert d["author"] == "user" - assert len(d) == 5 + self.assertEqual(d["score"], 50) + self.assertEqual(d["author"], "user") + self.assertEqual(len(d), 5) -class TestRedditItem: +class TestRedditItem(unittest.TestCase): def test_roundtrip(self): item = schema.RedditItem( id="R1", title="Test", url="http://reddit.com/r/test", @@ -43,10 +50,10 @@ class TestRedditItem: engagement=schema.Engagement(score=100), ) d = item.to_dict() - assert d["id"] == "R1" - assert d["subreddit"] == "test" - assert d["engagement"] == {"score": 100} - assert "cross_refs" not in d + self.assertEqual(d["id"], "R1") + self.assertEqual(d["subreddit"], "test") + self.assertEqual(d["engagement"], {"score": 100}) + self.assertNotIn("cross_refs", d) def test_cross_refs_included_when_present(self): item = schema.RedditItem( @@ -54,78 +61,82 @@ class TestRedditItem: cross_refs=["X1", "HN2"], ) d = item.to_dict() - assert d["cross_refs"] == ["X1", "HN2"] + self.assertEqual(d["cross_refs"], ["X1", "HN2"]) -class TestXItem: +class TestXItem(unittest.TestCase): def test_roundtrip(self): item = schema.XItem( id="X1", text="tweet", url="http://x.com/1", author_handle="user", date="2026-03-01", ) d = item.to_dict() - assert d["id"] == "X1" - assert d["author_handle"] == "user" - assert "cross_refs" not in d + self.assertEqual(d["id"], "X1") + self.assertEqual(d["author_handle"], "user") + self.assertNotIn("cross_refs", d) -class TestYouTubeItem: +class TestYouTubeItem(unittest.TestCase): def test_roundtrip(self): item = schema.YouTubeItem( id="YT1", title="Video", url="http://youtube.com/1", channel_name="chan", ) d = item.to_dict() - assert d["channel_name"] == "chan" - assert d["date_confidence"] == "high" + self.assertEqual(d["channel_name"], "chan") + self.assertEqual(d["date_confidence"], "high") -class TestTikTokItem: +class TestTikTokItem(unittest.TestCase): def test_roundtrip(self): item = schema.TikTokItem( id="TK1", text="caption", url="http://tiktok.com/1", author_name="creator", hashtags=["ai", "code"], ) d = item.to_dict() - assert d["hashtags"] == ["ai", "code"] - assert d["author_name"] == "creator" + self.assertEqual(d["hashtags"], ["ai", "code"]) + self.assertEqual(d["author_name"], "creator") -class TestInstagramItem: +class TestInstagramItem(unittest.TestCase): def test_roundtrip(self): item = schema.InstagramItem( id="IG1", text="caption", url="http://instagram.com/reel/1", author_name="creator", ) d = item.to_dict() - assert d["id"] == "IG1" + self.assertEqual(d["id"], "IG1") -class TestWebSearchItem: +class TestWebSearchItem(unittest.TestCase): def test_roundtrip(self): item = schema.WebSearchItem( id="W1", title="Article", url="http://example.com", source_domain="example.com", snippet="text", ) d = item.to_dict() - assert d["source_domain"] == "example.com" + self.assertEqual(d["source_domain"], "example.com") -class TestHackerNewsItem: +class TestHackerNewsItem(unittest.TestCase): def test_roundtrip(self): item = schema.HackerNewsItem( id="HN1", title="Show HN", url="http://example.com", hn_url="http://news.ycombinator.com/item?id=1", author="pg", ) d = item.to_dict() - assert d["hn_url"].startswith("http://news.ycombinator.com") + self.assertTrue(d["hn_url"].startswith("http://news.ycombinator.com")) -class TestPolymarketItem: +class TestPolymarketItem(unittest.TestCase): def test_roundtrip(self): item = schema.PolymarketItem( id="PM1", title="Election", question="Who wins?", url="http://polymarket.com/1", ) d = item.to_dict() - assert d["question"] == "Who wins?" + self.assertEqual(d["question"], "Who wins?") + + +if __name__ == "__main__": + unittest.main()