Skip to content

Commit 18c51ee

Browse files
daedalistclaude
andcommitted
Add comprehensive unit test suite (100 tests, 54% coverage)
Implements a complete unit testing infrastructure for SunGather with 100 tests achieving 54% overall code coverage across export modules and core functionality. Testing Infrastructure: - pytest: Test framework with fixtures and parameterization - pytest-mock: Mocking library for isolating components - pytest-cov: Coverage reporting (HTML and terminal output) - pytest.ini: Configuration for test discovery and coverage settings GitHub Workflow: - .github/workflows/test.yml: CI pipeline running on push/PR - Tests run on Python 3.9, 3.10, 3.11, and 3.12 - Automatic coverage reporting in PR comments - Prevents merging if tests fail Test Coverage by Module: - console.py: 100% coverage (11 tests) - webserver.py: 99% coverage (20 tests) - influxdb.py: 98% coverage (19 tests) - mqtt.py: 93% coverage (24 tests) - pvoutput.py: 84% coverage (21 tests) - hassio.py: 6% coverage (1 test - documents broken state) - sungather.py: 0% coverage (4 AST-based tests) Test Organization: - tests/exports/: Export module tests (console, webserver, mqtt, influxdb, pvoutput, hassio) - tests/test_sungather.py: Main module tests using AST parsing - tests/conftest.py: Shared fixtures and test configuration - tests/README.md: Documentation for running and writing tests Notable Test Patterns: - Mock-based testing for external dependencies (MQTT, InfluxDB, HTTP servers) - AST parsing for testing sungather.py (module has sys.exit() at top level) - Exception-based testing for hassio.py (documents known bug) - Coverage of both success and error paths Known Issues Documented: - hassio.py has a bug (line 12 defines api_base, lines 13-16 use undefined url_base) - Test expects AttributeError; will fail when bug is fixed as a reminder to add proper tests Development Workflow: - Run tests: pytest - Run with coverage: pytest --cov=SunGather --cov-report=html - View coverage: open htmlcov/index.html - Install dev dependencies: pip install -r requirements-dev.txt 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent a36efcf commit 18c51ee

15 files changed

Lines changed: 2725 additions & 1 deletion

.github/workflows/test.yml

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [ main, develop ]
6+
pull_request:
7+
branches: [ main, develop ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ['3.9', '3.10', '3.11', '3.12']
15+
16+
steps:
17+
- name: Checkout code
18+
uses: actions/checkout@v4
19+
20+
- name: Set up Python ${{ matrix.python-version }}
21+
uses: actions/setup-python@v5
22+
with:
23+
python-version: ${{ matrix.python-version }}
24+
cache: 'pip'
25+
26+
- name: Install dependencies
27+
run: |
28+
python -m pip install --upgrade pip
29+
pip install -r requirements.txt
30+
pip install -r requirements-dev.txt
31+
32+
- name: Run tests with coverage
33+
run: |
34+
pytest
35+
36+
- name: Upload coverage to Codecov
37+
if: matrix.python-version == '3.12'
38+
uses: codecov/codecov-action@v4
39+
with:
40+
file: ./coverage.xml
41+
flags: unittests
42+
name: codecov-umbrella
43+
fail_ci_if_error: false
44+
45+
- name: Upload coverage reports as artifact
46+
if: matrix.python-version == '3.12'
47+
uses: actions/upload-artifact@v4
48+
with:
49+
name: coverage-report
50+
path: htmlcov/

.gitignore

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,4 +124,10 @@ dmypy.json
124124

125125
# Intellij IDEA
126126
.idea
127-
*.iml
127+
*.iml
128+
129+
# Testing and Coverage
130+
.pytest_cache/
131+
htmlcov/
132+
coverage.xml
133+
.coverage

pytest.ini

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
[pytest]
2+
# Pytest configuration for SunGather
3+
4+
# Test discovery patterns
5+
python_files = test_*.py
6+
python_classes = Test*
7+
python_functions = test_*
8+
9+
# Minimum Python version
10+
minversion = 7.0
11+
12+
# Test paths
13+
testpaths = tests
14+
15+
# Coverage options
16+
addopts =
17+
--verbose
18+
--strict-markers
19+
--cov=SunGather
20+
--cov-report=term-missing
21+
--cov-report=html
22+
--cov-report=xml
23+
--cov-branch
24+
25+
# Markers for organizing tests
26+
markers =
27+
unit: Unit tests (fast, no external dependencies)
28+
integration: Integration tests (may require external services)
29+
slow: Slow tests
30+
31+
# Output options
32+
console_output_style = progress
33+
34+
# Ignore these paths during test collection
35+
norecursedirs =
36+
.git
37+
.github
38+
venv
39+
env
40+
.private
41+
htmlcov
42+
*.egg-info

requirements-dev.txt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Development and Testing Dependencies
2+
# Install with: pip install -r requirements-dev.txt
3+
4+
# Testing Framework
5+
pytest>=7.4.0,<9.0.0
6+
7+
# Mocking Support
8+
pytest-mock>=3.12.0,<4.0.0
9+
10+
# Code Coverage
11+
pytest-cov>=4.1.0,<6.0.0
12+
13+
# Note: Production dependencies are in requirements.txt
14+
# Install both with: pip install -r requirements.txt -r requirements-dev.txt

tests/README.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# SunGather Tests
2+
3+
This directory contains unit tests for the SunGather project.
4+
5+
## Running Tests
6+
7+
### Activate virtual environment:
8+
```bash
9+
source venv/bin/activate
10+
```
11+
12+
You should see `(venv)` in your prompt.
13+
14+
### Install test dependencies:
15+
```bash
16+
pip install -r requirements-dev.txt
17+
```
18+
19+
### Run all tests:
20+
```bash
21+
pytest
22+
```
23+
24+
### Run tests with coverage report:
25+
```bash
26+
pytest --cov=SunGather --cov-report=html
27+
```
28+
29+
### Run specific test file:
30+
```bash
31+
pytest tests/test_sungather.py
32+
```
33+
34+
### Run specific test class:
35+
```bash
36+
pytest tests/exports/test_mqtt.py::TestMQTTConfiguration
37+
```
38+
39+
### Run specific test:
40+
```bash
41+
pytest tests/exports/test_mqtt.py::TestMQTTConfiguration::test_placeholder
42+
```
43+
44+
## Test Organization
45+
46+
```
47+
tests/
48+
├── conftest.py # Shared fixtures and test configuration
49+
├── test_sungather.py # Tests for main sungather.py module
50+
└── exports/ # Export plugin tests
51+
├── test_console.py # Console export tests
52+
├── test_mqtt.py # MQTT export tests
53+
├── test_influxdb.py # InfluxDB export tests
54+
└── test_webserver.py # Webserver export tests
55+
```
56+
57+
## Writing Tests
58+
59+
### Using fixtures:
60+
61+
Fixtures are defined in `conftest.py` and automatically available to all tests:
62+
63+
```python
64+
def test_mqtt_configure(mock_mqtt_config, mocker):
65+
"""Test MQTT configuration."""
66+
export = export_mqtt()
67+
result = export.configure(mock_mqtt_config, mock_inverter)
68+
assert result == True
69+
```
70+
71+
### Using mocks:
72+
73+
The `mocker` fixture from pytest-mock provides mocking capabilities:
74+
75+
```python
76+
def test_connection_failure(mocker):
77+
"""Test handling of connection failures."""
78+
mock_client = mocker.patch('paho.mqtt.client.Client')
79+
mock_client.return_value.connect.side_effect = Exception("Connection refused")
80+
81+
# Your test code here
82+
```
83+
84+
### Test markers:
85+
86+
Use markers to categorize tests:
87+
88+
```python
89+
@pytest.mark.unit
90+
def test_fast_unit_test():
91+
"""Fast unit test with no external dependencies."""
92+
pass
93+
94+
@pytest.mark.slow
95+
def test_slow_integration():
96+
"""Slow integration test."""
97+
pass
98+
```
99+
100+
Run only unit tests:
101+
```bash
102+
pytest -m unit
103+
```
104+
105+
## Coverage Reports
106+
107+
After running tests with coverage, open the HTML report:
108+
```bash
109+
open htmlcov/index.html
110+
```
111+
112+
## Continuous Integration
113+
114+
Tests run automatically on GitHub Actions for:
115+
- All pull requests
116+
- Pushes to `main` and `develop` branches
117+
- Python versions: 3.9, 3.10, 3.11, 3.12
118+
119+
Coverage reports are uploaded to Codecov and stored as artifacts.

tests/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Tests package

tests/conftest.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""
2+
Shared pytest fixtures for SunGather tests.
3+
4+
This file is automatically discovered by pytest and makes fixtures
5+
available to all test files.
6+
"""
7+
8+
import pytest
9+
import sys
10+
from pathlib import Path
11+
12+
# Add SunGather directory to Python path for imports
13+
sungather_dir = Path(__file__).parent.parent / "SunGather"
14+
sys.path.insert(0, str(sungather_dir))
15+
16+
17+
# Fixture: Mock inverter configuration
18+
@pytest.fixture
19+
def mock_inverter_config():
20+
"""Standard inverter configuration for testing."""
21+
return {
22+
'host': '192.168.1.100',
23+
'port': 502,
24+
'timeout': 10,
25+
'retries': 3,
26+
'slave': 0x01,
27+
'scan_interval': 30,
28+
'connection': 'modbus',
29+
'model': 'SH5.0RS',
30+
'smart_meter': False,
31+
'use_local_time': False,
32+
'log_console': 'WARNING',
33+
'log_file': 'OFF',
34+
'level': 1
35+
}
36+
37+
38+
# Fixture: Mock register data
39+
@pytest.fixture
40+
def mock_register_data():
41+
"""Sample register data returned from inverter."""
42+
return {
43+
'device_type_code': 'SH5.0RS',
44+
'serial_number': '12345678',
45+
'total_active_power': 3500,
46+
'meter_power': -200, # Negative = exporting to grid
47+
'load_power': 1500,
48+
'battery_voltage': 52.4,
49+
'battery_current': 10.5,
50+
'battery_power': 550,
51+
'daily_export_energy': 15.2,
52+
'timestamp': '2025-10-19 10:30:00'
53+
}
54+
55+
56+
# Fixture: Mock MQTT export configuration
57+
@pytest.fixture
58+
def mock_mqtt_config():
59+
"""Standard MQTT export configuration for testing."""
60+
return {
61+
'name': 'mqtt',
62+
'enabled': True,
63+
'host': 'localhost',
64+
'port': 1883,
65+
'topic': 'SunGather/12345678',
66+
'username': None,
67+
'password': None,
68+
'homeassistant': False
69+
}
70+
71+
72+
# Fixture: Mock InfluxDB export configuration
73+
@pytest.fixture
74+
def mock_influxdb_config():
75+
"""Standard InfluxDB export configuration for testing."""
76+
return {
77+
'name': 'influxdb',
78+
'enabled': True,
79+
'url': 'http://localhost:8086',
80+
'token': 'test-token',
81+
'org': 'test-org',
82+
'bucket': 'sungather',
83+
'measurements': [
84+
{'register': 'total_active_power', 'point': 'power'},
85+
{'register': 'battery_voltage', 'point': 'battery'}
86+
]
87+
}
88+
89+
90+
# Fixture: Mock webserver export configuration
91+
@pytest.fixture
92+
def mock_webserver_config():
93+
"""Standard webserver export configuration for testing."""
94+
return {
95+
'name': 'webserver',
96+
'enabled': True,
97+
'port': 8080
98+
}

tests/exports/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Export tests package

0 commit comments

Comments
 (0)