Skip to content

Commit 19fcaba

Browse files
Merge pull request #119 from Dog-Face-Development/copilot/add-github-actions-tests
Add comprehensive test suite with GitHub Actions integration
2 parents f80461d + 586e0b7 commit 19fcaba

File tree

5 files changed

+290
-10
lines changed

5 files changed

+290
-10
lines changed

.github/workflows/pytest.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,14 @@ jobs:
1313
uses: actions/setup-python@v6
1414
with:
1515
python-version: '3.x'
16+
- name: Install system dependencies
17+
run: |
18+
sudo apt-get update
19+
sudo apt-get install -y python3-tk
1620
- name: Install dependencies
1721
run: |
1822
python -m pip install --upgrade pip
1923
pip install -r requirements.txt
2024
- name: Run tests
21-
run: pytest
25+
run: pytest -v
2226

docs/TESTING.md

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Testing Documentation
2+
3+
This document describes the test suite for Mom's Canning Timer and how tests are integrated with GitHub Actions.
4+
5+
## Test Structure
6+
7+
The test suite is organized into three main test files:
8+
9+
### tests/test_main.py
10+
Tests for the main timer functionality in `main.py`.
11+
12+
**Test Cases:**
13+
- `test_timeStart1_callable()` - Verifies timeStart1 function is callable
14+
- `test_timeStart1_execution()` - Tests timeStart1 runs without errors (with mocking)
15+
- `test_timeStart2_callable()` - Verifies timeStart2 function is callable
16+
- `test_timeStart2_execution()` - Tests timeStart2 runs without errors (with mocking)
17+
- `test_timeStart3_callable()` - Verifies timeStart3 function is callable
18+
- `test_timeStart3_execution()` - Tests timeStart3 runs without errors (with mocking)
19+
- `test_timeStart4_callable()` - Verifies timeStart4 function is callable
20+
- `test_timeStart4_execution()` - Tests timeStart4 runs without errors (with mocking)
21+
- `test_timer_callable()` - Verifies timer function is callable
22+
- `test_timer_creates_window()` - Tests timer creates a Tk window properly
23+
24+
**Testing Approach:**
25+
- Uses `unittest.mock.patch` to mock `time.sleep()` and `print()` functions
26+
- Uses threading to prevent tests from blocking for 15 minutes
27+
- Verifies functions execute and call expected methods without full timer execution
28+
29+
### tests/test_init.py
30+
Tests for the package initialization in `__init__.py`.
31+
32+
**Test Cases:**
33+
- `test_all_attribute_exists()` - Verifies `__all__` attribute exists
34+
- `test_all_contains_main()` - Verifies `__all__` contains 'main'
35+
- `test_all_is_list()` - Verifies `__all__` is a list
36+
37+
### tests/test_entrypoint.py
38+
Tests for the entry point module in `__main__.py`.
39+
40+
**Test Cases:**
41+
- `test_main_module_exists()` - Verifies `__main__.py` file exists
42+
- `test_main_has_correct_imports()` - Verifies correct import structure
43+
- `test_main_has_docstring()` - Verifies proper documentation
44+
45+
## Running Tests
46+
47+
### Locally
48+
49+
1. Install dependencies:
50+
```bash
51+
pip install -r requirements.txt
52+
```
53+
54+
2. Install tkinter (if not already installed):
55+
```bash
56+
# On Ubuntu/Debian
57+
sudo apt-get install python3-tk
58+
59+
# On macOS (using Homebrew)
60+
brew install python-tk
61+
62+
# On Windows
63+
# tkinter is usually included with Python installation
64+
```
65+
66+
3. Run tests:
67+
```bash
68+
# Run all tests
69+
pytest
70+
71+
# Run with verbose output
72+
pytest -v
73+
74+
# Run specific test file
75+
pytest tests/test_main.py
76+
77+
# Run specific test
78+
pytest tests/test_main.py::TestTimer::test_timeStart1_callable
79+
```
80+
81+
### In GitHub Actions
82+
83+
Tests run automatically on:
84+
- Every push to any branch
85+
- Every pull request
86+
87+
The GitHub Actions workflow:
88+
1. Sets up Python environment
89+
2. Installs system dependencies (python3-tk)
90+
3. Installs Python dependencies from requirements.txt
91+
4. Runs pytest with verbose output
92+
93+
See `.github/workflows/pytest.yml` for the complete workflow configuration.
94+
95+
## Test Dependencies
96+
97+
- **pytest** - Testing framework
98+
- **unittest.mock** - Built-in Python mocking library
99+
- **threading** - Built-in Python threading library for non-blocking tests
100+
- **python3-tk** - System dependency for tkinter GUI framework
101+
102+
## Coverage
103+
104+
Current test coverage includes:
105+
- All timer start functions (timeStart1-4)
106+
- Main timer GUI function
107+
- Package initialization
108+
- Entry point module structure
109+
110+
## Best Practices
111+
112+
1. **Mocking Long-Running Operations**: Timer functions are designed to run for 15 minutes. Tests use mocking to verify logic without waiting.
113+
114+
2. **Threading for GUI Tests**: GUI operations can block test execution. Tests use threading with timeouts to prevent hanging.
115+
116+
3. **Testing Without Display**: Tests are designed to run in headless environments (like GitHub Actions) by mocking Tk window creation.
117+
118+
4. **Minimal Changes**: Tests verify behavior without modifying the original code, maintaining backward compatibility.
119+
120+
## Future Improvements
121+
122+
Potential areas for test expansion:
123+
- Integration tests for complete timer workflows
124+
- Performance tests for timer accuracy
125+
- UI interaction tests with GUI automation tools
126+
- Edge case testing (system sleep, interruptions, etc.)
127+
- Code coverage reporting

tests/test_entrypoint.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
"""Tests for __main__.py."""
2+
3+
# pylint: disable=invalid-name
4+
5+
import sys
6+
import os
7+
import unittest
8+
9+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
10+
11+
12+
class TestEntrypoint(unittest.TestCase):
13+
"""Tests for __main__.py."""
14+
15+
def test_main_module_exists(self):
16+
"""Test that __main__.py file exists."""
17+
main_path = os.path.join(
18+
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "__main__.py"
19+
)
20+
self.assertTrue(os.path.exists(main_path))
21+
22+
def test_main_has_correct_imports(self):
23+
"""Test that __main__.py has the correct structure."""
24+
main_path = os.path.join(
25+
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "__main__.py"
26+
)
27+
with open(main_path, "r", encoding="utf-8") as f:
28+
content = f.read()
29+
self.assertIn("from main import timer", content)
30+
self.assertIn('if __name__ == "__main__":', content)
31+
32+
def test_main_has_docstring(self):
33+
"""Test that __main__.py has a docstring."""
34+
main_path = os.path.join(
35+
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "__main__.py"
36+
)
37+
with open(main_path, "r", encoding="utf-8") as f:
38+
content = f.read()
39+
# Check for docstring at the beginning
40+
self.assertIn('"""', content)
41+
42+
43+
if __name__ == "__main__":
44+
unittest.main()

tests/test_init.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""Tests for __init__.py."""
2+
3+
import sys
4+
import os
5+
import unittest
6+
7+
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
8+
9+
import __init__
10+
11+
12+
class TestInit(unittest.TestCase):
13+
"""Tests for __init__.py."""
14+
15+
def test_all_attribute_exists(self):
16+
"""Test that __all__ attribute exists."""
17+
self.assertTrue(hasattr(__init__, "__all__"))
18+
19+
def test_all_contains_main(self):
20+
"""Test that __all__ contains 'main'."""
21+
self.assertIn("main", __init__.__all__)
22+
23+
def test_all_is_list(self):
24+
"""Test that __all__ is a list."""
25+
self.assertIsInstance(__init__.__all__, list)
26+
27+
28+
if __name__ == "__main__":
29+
unittest.main()

tests/test_main.py

Lines changed: 85 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,110 @@
11
"""Tests for main.py."""
2+
23
# pylint: disable=invalid-name, wrong-import-position
34

45
import sys
56
import os
67
import unittest
8+
from unittest.mock import patch, MagicMock
9+
import threading
710

811
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
912

10-
from main import timeStart1, timeStart2, timeStart3, timeStart4
13+
from main import timeStart1, timeStart2, timeStart3, timeStart4, timer
1114

1215

1316
class TestTimer(unittest.TestCase):
1417
"""Tests for timer.py."""
1518

16-
def test_timeStart1(self):
17-
"""Test timeStart1 function."""
19+
def _run_function_in_thread(self, func, timeout=2.0):
20+
"""Helper method to run a function in a thread with timeout."""
21+
thread = threading.Thread(target=func)
22+
thread.daemon = True
23+
thread.start()
24+
thread.join(timeout=timeout)
25+
26+
def test_timeStart1_callable(self):
27+
"""Test timeStart1 function is callable."""
1828
self.assertTrue(callable(timeStart1))
1929

20-
def test_timeStart2(self):
21-
"""Test timeStart2 function."""
30+
def test_timeStart2_callable(self):
31+
"""Test timeStart2 function is callable."""
2232
self.assertTrue(callable(timeStart2))
2333

24-
def test_timeStart3(self):
25-
"""Test timeStart3 function."""
34+
def test_timeStart3_callable(self):
35+
"""Test timeStart3 function is callable."""
2636
self.assertTrue(callable(timeStart3))
2737

28-
def test_timeStart4(self):
29-
"""Test timeStart4 function."""
38+
def test_timeStart4_callable(self):
39+
"""Test timeStart4 function is callable."""
3040
self.assertTrue(callable(timeStart4))
3141

42+
@patch("time.sleep")
43+
@patch("builtins.print")
44+
def test_timeStart1_execution(self, mock_print, mock_sleep):
45+
"""Test timeStart1 runs without errors when mocked."""
46+
self._run_function_in_thread(timeStart1)
47+
48+
# Verify that print was called (timer is running)
49+
self.assertTrue(mock_print.called)
50+
# Verify that sleep was called (timer is working)
51+
self.assertTrue(mock_sleep.called)
52+
53+
@patch("time.sleep")
54+
@patch("builtins.print")
55+
def test_timeStart2_execution(self, mock_print, mock_sleep):
56+
"""Test timeStart2 runs without errors when mocked."""
57+
self._run_function_in_thread(timeStart2)
58+
59+
self.assertTrue(mock_print.called)
60+
self.assertTrue(mock_sleep.called)
61+
62+
@patch("time.sleep")
63+
@patch("builtins.print")
64+
def test_timeStart3_execution(self, mock_print, mock_sleep):
65+
"""Test timeStart3 runs without errors when mocked."""
66+
self._run_function_in_thread(timeStart3)
67+
68+
self.assertTrue(mock_print.called)
69+
self.assertTrue(mock_sleep.called)
70+
71+
@patch("time.sleep")
72+
@patch("builtins.print")
73+
def test_timeStart4_execution(self, mock_print, mock_sleep):
74+
"""Test timeStart4 runs without errors when mocked."""
75+
self._run_function_in_thread(timeStart4)
76+
77+
self.assertTrue(mock_print.called)
78+
self.assertTrue(mock_sleep.called)
3279

80+
def test_timer_callable(self):
81+
"""Test timer function is callable."""
82+
self.assertTrue(callable(timer))
83+
84+
@patch("main.Tk")
85+
def test_timer_creates_window(self, mock_tk):
86+
"""Test timer function creates a Tk window."""
87+
# Mock the Tk instance and its methods
88+
mock_window = MagicMock()
89+
mock_tk.return_value = mock_window
90+
91+
# Run timer in a thread to avoid blocking
92+
def run_timer():
93+
try:
94+
timer()
95+
except Exception:
96+
# Expected to fail when mainloop is not properly mocked
97+
pass
98+
99+
self._run_function_in_thread(run_timer, timeout=1.0)
100+
101+
# Verify Tk was instantiated
102+
mock_tk.assert_called_once()
103+
# Verify window title was set
104+
mock_window.title.assert_called_once_with("Canning Timer")
105+
106+
107+
if __name__ == "__main__":
108+
unittest.main()
33109
if __name__ == "__main__":
34110
unittest.main()

0 commit comments

Comments
 (0)