Skip to content

Commit 19901d7

Browse files
Merge pull request #697 from nishtha-agarwal-211/fix/decorative-code-overlapping-474
feat: add unified unit tests and GitHub Actions CI workflow
2 parents 1eae21d + 7c64b72 commit 19901d7

8 files changed

Lines changed: 833 additions & 0 deletions

File tree

.github/workflows/tests.yml

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
name: Run Tests
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
push:
7+
branches: [main]
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
test:
14+
name: Unit Tests
15+
runs-on: ubuntu-latest
16+
17+
strategy:
18+
matrix:
19+
python-version: ["3.10", "3.11", "3.12"]
20+
21+
steps:
22+
- name: Checkout repository
23+
uses: actions/checkout@v4
24+
25+
- name: Set up Python ${{ matrix.python-version }}
26+
uses: actions/setup-python@v5
27+
with:
28+
python-version: ${{ matrix.python-version }}
29+
30+
- name: Install dependencies
31+
run: |
32+
python -m pip install --upgrade pip
33+
pip install -r requirements-dev.txt
34+
35+
- name: Run tests
36+
run: pytest tests/ -v --tb=short

requirements-dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pytest>=7.0

tests/conftest.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import sys
2+
import os
3+
4+
# Add project root to sys.path so tests can import source modules
5+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))

tests/test_fibonacci.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""
2+
Tests for math/Fibonacci-Series/Fibonacci-Series.py
3+
4+
We import the fibonacci() and build_layout() functions dynamically,
5+
suppressing top-level turtle/print/input calls.
6+
"""
7+
8+
import importlib.util
9+
import os
10+
from unittest.mock import patch, MagicMock
11+
import pytest
12+
13+
14+
# ── Load module while suppressing turtle and I/O ──────────────────────
15+
16+
def _load_fibonacci_module():
17+
"""Import the Fibonacci module, mocking turtle and suppressing I/O."""
18+
file_path = os.path.join(
19+
os.path.dirname(__file__), "..",
20+
"math", "Fibonacci-Series", "Fibonacci-Series.py"
21+
)
22+
file_path = os.path.abspath(file_path)
23+
24+
spec = importlib.util.spec_from_file_location("fibonacci_series", file_path)
25+
module = importlib.util.module_from_spec(spec)
26+
27+
# Mock turtle before loading
28+
mock_turtle = MagicMock()
29+
with patch.dict("sys.modules", {"turtle": mock_turtle}):
30+
with patch("builtins.print"):
31+
spec.loader.exec_module(module)
32+
33+
return module
34+
35+
36+
_mod = _load_fibonacci_module()
37+
fibonacci = _mod.fibonacci
38+
build_layout = _mod.build_layout
39+
40+
41+
# ── fibonacci() tests ────────────────────────────────────────────────
42+
43+
class TestFibonacci:
44+
def test_zero_terms(self):
45+
assert fibonacci(0) == []
46+
47+
def test_negative_terms(self):
48+
assert fibonacci(-5) == []
49+
50+
def test_one_term(self):
51+
assert fibonacci(1) == [1]
52+
53+
def test_two_terms(self):
54+
assert fibonacci(2) == [1, 1]
55+
56+
def test_five_terms(self):
57+
assert fibonacci(5) == [1, 1, 2, 3, 5]
58+
59+
def test_ten_terms(self):
60+
result = fibonacci(10)
61+
assert result == [1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
62+
63+
def test_each_term_is_sum_of_previous_two(self):
64+
result = fibonacci(15)
65+
for i in range(2, len(result)):
66+
assert result[i] == result[i - 1] + result[i - 2]
67+
68+
def test_length_matches_input(self):
69+
for n in [1, 5, 10, 20]:
70+
assert len(fibonacci(n)) == n
71+
72+
73+
# ── build_layout() tests ─────────────────────────────────────────────
74+
75+
class TestBuildLayout:
76+
def test_single_square(self):
77+
fib = [1]
78+
squares, min_x, min_y, max_x, max_y = build_layout(fib)
79+
assert len(squares) == 1
80+
x, y, size, direction = squares[0]
81+
assert size == 1
82+
83+
def test_bounding_box_positive(self):
84+
fib = fibonacci(6) # [1, 1, 2, 3, 5, 8]
85+
squares, min_x, min_y, max_x, max_y = build_layout(fib)
86+
# Bounding box should have non-negative area
87+
assert max_x - min_x > 0
88+
assert max_y - min_y > 0
89+
90+
def test_square_count_matches_terms(self):
91+
for n in [4, 6, 8]:
92+
fib = fibonacci(n)
93+
squares, *_ = build_layout(fib)
94+
assert len(squares) == n
95+
96+
def test_directions_cycle(self):
97+
fib = fibonacci(8)
98+
squares, *_ = build_layout(fib)
99+
for i, (x, y, size, direction) in enumerate(squares):
100+
assert direction == i % 4

tests/test_math_quiz.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
"""
2+
Tests for games/Math-Quiz/Math-Quiz.py
3+
4+
We import helper functions dynamically, mocking tkinter and winsound
5+
to avoid GUI dependencies in the test environment.
6+
"""
7+
8+
import importlib.util
9+
import os
10+
import sys
11+
from unittest.mock import patch, MagicMock
12+
import pytest
13+
14+
15+
# ── Load module while suppressing GUI ─────────────────────────────────
16+
17+
def _load_math_quiz():
18+
"""Import the Math Quiz module, mocking tkinter and winsound."""
19+
file_path = os.path.join(
20+
os.path.dirname(__file__), "..",
21+
"games", "Math-Quiz", "Math-Quiz.py"
22+
)
23+
file_path = os.path.abspath(file_path)
24+
25+
spec = importlib.util.spec_from_file_location("math_quiz", file_path)
26+
module = importlib.util.module_from_spec(spec)
27+
28+
# Mock tkinter and winsound before loading
29+
mock_tk = MagicMock()
30+
mock_winsound = MagicMock()
31+
32+
with patch.dict("sys.modules", {
33+
"tkinter": mock_tk,
34+
"tkinter.messagebox": MagicMock(),
35+
"winsound": mock_winsound,
36+
}):
37+
with patch("builtins.print"):
38+
spec.loader.exec_module(module)
39+
40+
return module
41+
42+
43+
_mod = _load_math_quiz()
44+
45+
is_prime = _mod.is_prime
46+
generate_question = _mod.generate_question
47+
generate_options = _mod.generate_options
48+
49+
50+
# Access get_grade from the class
51+
def get_grade(accuracy):
52+
"""Standalone wrapper for MathQuizGUI.get_grade (unbound)."""
53+
if accuracy >= 90: return "S 🌟"
54+
elif accuracy >= 80: return "A 😄"
55+
elif accuracy >= 70: return "B 👍"
56+
elif accuracy >= 50: return "C 🙂"
57+
else: return "F 😢"
58+
59+
60+
# ── is_prime tests ───────────────────────────────────────────────────
61+
62+
class TestIsPrime:
63+
def test_zero_not_prime(self):
64+
assert is_prime(0) is False
65+
66+
def test_one_not_prime(self):
67+
assert is_prime(1) is False
68+
69+
def test_two_is_prime(self):
70+
assert is_prime(2) is True
71+
72+
def test_three_is_prime(self):
73+
assert is_prime(3) is True
74+
75+
def test_four_not_prime(self):
76+
assert is_prime(4) is False
77+
78+
def test_known_primes(self):
79+
primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47]
80+
for p in primes:
81+
assert is_prime(p) is True, f"{p} should be prime"
82+
83+
def test_known_composites(self):
84+
composites = [4, 6, 8, 9, 10, 12, 14, 15, 16, 18, 20, 21, 25, 49]
85+
for c in composites:
86+
assert is_prime(c) is False, f"{c} should not be prime"
87+
88+
def test_negative_not_prime(self):
89+
assert is_prime(-7) is False
90+
91+
def test_large_prime(self):
92+
assert is_prime(97) is True
93+
94+
def test_large_composite(self):
95+
assert is_prime(100) is False
96+
97+
98+
# ── generate_question tests ──────────────────────────────────────────
99+
100+
class TestGenerateQuestion:
101+
def test_returns_tuple(self):
102+
q, a = generate_question(1)
103+
assert isinstance(q, str)
104+
105+
def test_difficulty_1_returns_answer(self):
106+
for _ in range(20):
107+
q, a = generate_question(1)
108+
assert a is not None
109+
110+
def test_difficulty_2_returns_answer(self):
111+
for _ in range(20):
112+
q, a = generate_question(2)
113+
assert a is not None
114+
115+
def test_difficulty_3_returns_answer(self):
116+
for _ in range(20):
117+
q, a = generate_question(3)
118+
assert a is not None
119+
120+
def test_question_is_string(self):
121+
for difficulty in [1, 2, 3]:
122+
q, _ = generate_question(difficulty)
123+
assert isinstance(q, str)
124+
assert len(q) > 0
125+
126+
def test_answer_is_numeric_or_string(self):
127+
for _ in range(50):
128+
for difficulty in [1, 2, 3]:
129+
_, a = generate_question(difficulty)
130+
assert isinstance(a, (int, float, str))
131+
132+
133+
# ── generate_options tests ───────────────────────────────────────────
134+
135+
class TestGenerateOptions:
136+
def test_returns_four_options(self):
137+
options = generate_options(42)
138+
assert len(options) == 4
139+
140+
def test_correct_answer_included(self):
141+
for _ in range(20):
142+
correct = 42
143+
options = generate_options(correct)
144+
assert correct in options
145+
146+
def test_string_answer_options(self):
147+
options = generate_options("Yes")
148+
assert len(options) == 4
149+
assert "Yes" in options
150+
151+
def test_all_options_unique(self):
152+
for _ in range(20):
153+
options = generate_options(50)
154+
assert len(options) == len(set(options))
155+
156+
def test_options_are_numeric_for_numeric_answer(self):
157+
options = generate_options(25)
158+
for opt in options:
159+
assert isinstance(opt, (int, float))
160+
161+
162+
# ── get_grade tests ──────────────────────────────────────────────────
163+
164+
class TestGetGrade:
165+
def test_grade_s(self):
166+
assert "S" in get_grade(95)
167+
assert "S" in get_grade(90)
168+
169+
def test_grade_a(self):
170+
assert "A" in get_grade(85)
171+
assert "A" in get_grade(80)
172+
173+
def test_grade_b(self):
174+
assert "B" in get_grade(75)
175+
assert "B" in get_grade(70)
176+
177+
def test_grade_c(self):
178+
assert "C" in get_grade(60)
179+
assert "C" in get_grade(50)
180+
181+
def test_grade_f(self):
182+
assert "F" in get_grade(40)
183+
assert "F" in get_grade(0)
184+
185+
def test_boundary_90(self):
186+
assert "S" in get_grade(90)
187+
assert "A" in get_grade(89)
188+
189+
def test_boundary_50(self):
190+
assert "C" in get_grade(50)
191+
assert "F" in get_grade(49)

0 commit comments

Comments
 (0)