Skip to content

Commit 04f3503

Browse files
committed
test: add comprehensive unit tests for 80% coverage
Add unit tests for core modules: - analyzers: commits, issues, pull_requests, productivity - api: client, models - cli: output formatting - core: exceptions - exporters: csv_exporter Coverage improved from 54% to 80% with 236 passing tests.
1 parent 5e6cacd commit 04f3503

9 files changed

Lines changed: 2766 additions & 0 deletions

File tree

Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
"""Tests for commit analyzer."""
2+
3+
import pytest
4+
from datetime import datetime, timezone
5+
from unittest.mock import Mock, MagicMock
6+
7+
from src.github_analyzer.analyzers.commits import CommitAnalyzer
8+
from src.github_analyzer.api.models import Commit
9+
from src.github_analyzer.config.validation import Repository
10+
11+
12+
class TestCommitAnalyzerInit:
13+
"""Tests for CommitAnalyzer initialization."""
14+
15+
def test_initializes_with_client(self):
16+
"""Test analyzer initializes with client."""
17+
client = Mock()
18+
analyzer = CommitAnalyzer(client)
19+
assert analyzer._client is client
20+
21+
22+
class TestCommitAnalyzerFetchAndAnalyze:
23+
"""Tests for fetch_and_analyze method."""
24+
25+
def test_fetches_commits_from_api(self):
26+
"""Test fetches commits from GitHub API."""
27+
client = Mock()
28+
client.paginate.return_value = []
29+
client.get.return_value = None
30+
31+
analyzer = CommitAnalyzer(client)
32+
repo = Repository(owner="test", name="repo")
33+
since = datetime.now(timezone.utc)
34+
35+
result = analyzer.fetch_and_analyze(repo, since)
36+
37+
client.paginate.assert_called_once()
38+
assert result == []
39+
40+
def test_processes_commits_into_objects(self):
41+
"""Test processes raw commits into Commit objects."""
42+
raw_commit = {
43+
"sha": "abc123def456",
44+
"commit": {
45+
"author": {
46+
"name": "Test Author",
47+
"email": "test@example.com",
48+
"date": "2025-01-15T10:00:00Z",
49+
},
50+
"message": "Test commit message",
51+
},
52+
"author": {"login": "testuser"},
53+
"committer": {"login": "testuser"},
54+
"stats": {"additions": 10, "deletions": 5, "total": 15},
55+
"files": [{"filename": "test.py"}],
56+
"html_url": "https://github.com/test/repo/commit/abc123",
57+
}
58+
59+
client = Mock()
60+
client.paginate.return_value = [{"sha": "abc123def456"}]
61+
client.get.return_value = raw_commit
62+
63+
analyzer = CommitAnalyzer(client)
64+
repo = Repository(owner="test", name="repo")
65+
since = datetime.now(timezone.utc)
66+
67+
result = analyzer.fetch_and_analyze(repo, since)
68+
69+
assert len(result) == 1
70+
assert isinstance(result[0], Commit)
71+
assert result[0].sha == "abc123def456"
72+
assert result[0].author_login == "testuser"
73+
74+
def test_handles_missing_commit_details(self):
75+
"""Test handles when commit details fetch returns None."""
76+
client = Mock()
77+
# Return a commit with sha but no details
78+
client.paginate.return_value = [{"sha": "abc123def456"}]
79+
client.get.return_value = None
80+
81+
analyzer = CommitAnalyzer(client)
82+
repo = Repository(owner="test", name="repo")
83+
since = datetime.now(timezone.utc)
84+
85+
result = analyzer.fetch_and_analyze(repo, since)
86+
# Should still create commit from basic data
87+
assert len(result) == 1
88+
89+
def test_fetches_details_for_each_commit(self):
90+
"""Test fetches details for each commit."""
91+
raw_detail = {
92+
"sha": "valid123def456",
93+
"commit": {"author": {"date": "2025-01-15T10:00:00Z"}, "message": "test"},
94+
"author": {"login": "user"},
95+
"committer": {"login": "user"},
96+
"stats": {"additions": 10, "deletions": 5},
97+
"files": [],
98+
}
99+
100+
client = Mock()
101+
client.paginate.return_value = [{"sha": "valid123def456"}]
102+
client.get.return_value = raw_detail
103+
104+
analyzer = CommitAnalyzer(client)
105+
repo = Repository(owner="test", name="repo")
106+
since = datetime.now(timezone.utc)
107+
108+
result = analyzer.fetch_and_analyze(repo, since)
109+
110+
assert len(result) == 1
111+
assert client.get.called
112+
113+
114+
class TestCommitAnalyzerGetStats:
115+
"""Tests for get_stats method."""
116+
117+
def test_returns_empty_stats_for_no_commits(self):
118+
"""Test returns zeros for empty commit list."""
119+
client = Mock()
120+
analyzer = CommitAnalyzer(client)
121+
122+
stats = analyzer.get_stats([])
123+
124+
assert stats["total"] == 0
125+
assert stats["merge_commits"] == 0
126+
assert stats["revert_commits"] == 0
127+
assert stats["total_additions"] == 0
128+
assert stats["total_deletions"] == 0
129+
assert stats["unique_authors"] == 0
130+
131+
def test_calculates_correct_stats(self):
132+
"""Test calculates correct statistics."""
133+
client = Mock()
134+
analyzer = CommitAnalyzer(client)
135+
136+
commits = [
137+
Commit(
138+
repository="test/repo",
139+
sha="abc123def456",
140+
author_login="user1",
141+
author_email="user1@test.com",
142+
committer_login="user1",
143+
date=datetime.now(timezone.utc),
144+
message="feat: add feature",
145+
full_message="feat: add feature",
146+
additions=100,
147+
deletions=50,
148+
files_changed=5,
149+
),
150+
Commit(
151+
repository="test/repo",
152+
sha="def456ghi789",
153+
author_login="user2",
154+
author_email="user2@test.com",
155+
committer_login="user2",
156+
date=datetime.now(timezone.utc),
157+
message="Merge pull request #1",
158+
full_message="Merge pull request #1",
159+
additions=20,
160+
deletions=10,
161+
files_changed=2,
162+
),
163+
Commit(
164+
repository="test/repo",
165+
sha="ghi789jkl012",
166+
author_login="user1",
167+
author_email="user1@test.com",
168+
committer_login="user1",
169+
date=datetime.now(timezone.utc),
170+
message="Revert \"feat: add feature\"",
171+
full_message="Revert \"feat: add feature\"",
172+
additions=50,
173+
deletions=100,
174+
files_changed=5,
175+
),
176+
]
177+
178+
stats = analyzer.get_stats(commits)
179+
180+
assert stats["total"] == 3
181+
assert stats["merge_commits"] == 1
182+
assert stats["revert_commits"] == 1
183+
assert stats["total_additions"] == 170
184+
assert stats["total_deletions"] == 160
185+
assert stats["unique_authors"] == 2
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
"""Tests for issue analyzer."""
2+
3+
import pytest
4+
from datetime import datetime, timezone, timedelta
5+
from unittest.mock import Mock
6+
7+
from src.github_analyzer.analyzers.issues import IssueAnalyzer
8+
from src.github_analyzer.api.models import Issue
9+
from src.github_analyzer.config.validation import Repository
10+
11+
12+
class TestIssueAnalyzerInit:
13+
"""Tests for IssueAnalyzer initialization."""
14+
15+
def test_initializes_with_client(self):
16+
"""Test analyzer initializes with client."""
17+
client = Mock()
18+
analyzer = IssueAnalyzer(client)
19+
assert analyzer._client is client
20+
21+
22+
class TestIssueAnalyzerFetchAndAnalyze:
23+
"""Tests for fetch_and_analyze method."""
24+
25+
def test_fetches_issues_from_api(self):
26+
"""Test fetches issues from GitHub API."""
27+
client = Mock()
28+
client.paginate.return_value = []
29+
30+
analyzer = IssueAnalyzer(client)
31+
repo = Repository(owner="test", name="repo")
32+
since = datetime.now(timezone.utc)
33+
34+
result = analyzer.fetch_and_analyze(repo, since)
35+
36+
client.paginate.assert_called_once()
37+
assert result == []
38+
39+
def test_filters_out_pull_requests(self):
40+
"""Test filters out items that are pull requests."""
41+
now = datetime.now(timezone.utc)
42+
created = now.isoformat()
43+
44+
client = Mock()
45+
client.paginate.return_value = [
46+
{"number": 1, "title": "Issue", "state": "open", "created_at": created, "updated_at": created, "user": {"login": "user1"}},
47+
{"number": 2, "title": "PR", "state": "open", "created_at": created, "updated_at": created, "pull_request": {}, "user": {"login": "user1"}},
48+
]
49+
50+
analyzer = IssueAnalyzer(client)
51+
repo = Repository(owner="test", name="repo")
52+
since = now - timedelta(days=30)
53+
54+
result = analyzer.fetch_and_analyze(repo, since)
55+
56+
# Only issue should be included, not PR
57+
assert len(result) == 1
58+
assert result[0].number == 1
59+
60+
def test_processes_issues_into_objects(self):
61+
"""Test processes raw issues into Issue objects."""
62+
now = datetime.now(timezone.utc)
63+
created = now.isoformat()
64+
65+
raw_issue = {
66+
"number": 1,
67+
"title": "Test Issue",
68+
"state": "open",
69+
"user": {"login": "testuser"},
70+
"created_at": created,
71+
"updated_at": created,
72+
"closed_at": None,
73+
"labels": [{"name": "bug"}],
74+
"assignees": [{"login": "assignee1"}],
75+
"comments": 5,
76+
"html_url": "https://github.com/test/repo/issues/1",
77+
}
78+
79+
client = Mock()
80+
client.paginate.return_value = [raw_issue]
81+
82+
analyzer = IssueAnalyzer(client)
83+
repo = Repository(owner="test", name="repo")
84+
since = now - timedelta(days=30)
85+
86+
result = analyzer.fetch_and_analyze(repo, since)
87+
88+
assert len(result) == 1
89+
assert isinstance(result[0], Issue)
90+
assert result[0].number == 1
91+
assert result[0].title == "Test Issue"
92+
assert result[0].author_login == "testuser"
93+
94+
95+
class TestIssueAnalyzerGetStats:
96+
"""Tests for get_stats method."""
97+
98+
def test_returns_empty_stats_for_no_issues(self):
99+
"""Test returns zeros for empty issue list."""
100+
client = Mock()
101+
analyzer = IssueAnalyzer(client)
102+
103+
stats = analyzer.get_stats([])
104+
105+
assert stats["total"] == 0
106+
assert stats["open"] == 0
107+
assert stats["closed"] == 0
108+
assert stats["bugs"] == 0
109+
assert stats["enhancements"] == 0
110+
assert stats["avg_time_to_close_hours"] is None
111+
112+
def test_calculates_correct_stats(self):
113+
"""Test calculates correct statistics."""
114+
client = Mock()
115+
analyzer = IssueAnalyzer(client)
116+
117+
now = datetime.now(timezone.utc)
118+
issues = [
119+
Issue(
120+
repository="test/repo",
121+
number=1,
122+
title="Open Bug",
123+
state="open",
124+
author_login="user1",
125+
created_at=now - timedelta(days=5),
126+
updated_at=now,
127+
closed_at=None,
128+
labels=["bug"],
129+
assignees=["user1"],
130+
comments=2,
131+
),
132+
Issue(
133+
repository="test/repo",
134+
number=2,
135+
title="Closed Enhancement",
136+
state="closed",
137+
author_login="user2",
138+
created_at=now - timedelta(days=10),
139+
updated_at=now - timedelta(days=2),
140+
closed_at=now - timedelta(days=2),
141+
labels=["enhancement"],
142+
assignees=[],
143+
comments=5,
144+
),
145+
Issue(
146+
repository="test/repo",
147+
number=3,
148+
title="Closed Bug",
149+
state="closed",
150+
author_login="user3",
151+
created_at=now - timedelta(days=3),
152+
updated_at=now - timedelta(days=1),
153+
closed_at=now - timedelta(days=1),
154+
labels=["bug"],
155+
assignees=["user3"],
156+
comments=1,
157+
),
158+
]
159+
160+
stats = analyzer.get_stats(issues)
161+
162+
assert stats["total"] == 3
163+
assert stats["open"] == 1
164+
assert stats["closed"] == 2
165+
assert stats["bugs"] == 2
166+
assert stats["enhancements"] == 1
167+
assert stats["avg_time_to_close_hours"] is not None

0 commit comments

Comments
 (0)