Files
fuj-management/docs/by-claude-opus/testing.md
Jan Novak 9b99f6d33b
All checks were successful
Deploy to K8s / deploy (push) Successful in 8s
docs: experiment with generated documentation, let's keep it in git for
now
2026-03-11 11:57:30 +01:00

146 lines
4.9 KiB
Markdown

# Testing Guide
## Overview
The project uses Python's built-in `unittest` framework with `unittest.mock` for mocking external dependencies (Google Sheets API, attendance data). Tests live in the `tests/` directory.
## Running Tests
```bash
make test # Run all tests
make test-v # Run with verbose output
```
Under the hood:
```bash
PYTHONPATH=scripts:. python3 -m unittest discover tests
```
The `PYTHONPATH` includes both `scripts/` and the project root so that test files can import from both `app.py` and `scripts/*.py`.
## Test Files
### `test_app.py` — Flask Route Tests
Tests the Flask web application routes using Flask's built-in test client. All external data fetching is mocked.
| Test | What it verifies |
|------|-----------------|
| `test_index_page` | `GET /` returns 200 and contains a redirect to `/fees` |
| `test_fees_route` | `GET /fees` renders the fees dashboard with correct member names |
| `test_reconcile_route` | `GET /reconcile` renders the reconciliation page with payment matching |
| `test_payments_route` | `GET /payments` renders the ledger with grouped transactions |
**Mocking strategy**:
```python
@patch('app.get_members_with_fees')
def test_fees_route(self, mock_get_members):
mock_get_members.return_value = (
[('Test Member', 'A', {'2026-01': (750, 4)})],
['2026-01']
)
response = self.client.get('/fees')
self.assertEqual(response.status_code, 200)
self.assertIn(b'Test Member', response.data)
```
Each test patches the data-fetching functions (`get_members_with_fees`, `fetch_sheet_data`) to return controlled test data, avoiding any network calls.
**Notable**: The reconcile route test also mocks `fetch_sheet_data` and verifies that the reconciliation engine correctly matches a payment against an expected fee (checking for "OK" in the response).
### `test_reconcile_exceptions.py` — Reconciliation Logic Tests
Tests the `reconcile()` function directly (unit tests for the core business logic):
| Test | What it verifies |
|------|-----------------|
| `test_reconcile_applies_exceptions` | When a fee exception exists (400 CZK instead of 750), the expected amount is overridden and balance is calculated correctly |
| `test_reconcile_fallback_to_attendance` | When no exception exists, the attendance-based fee is used |
**Why these tests matter**: The exception system is critical for correctness — an incorrect override could cause members to be shown incorrect amounts owed. These tests verify that:
- Exceptions properly override the attendance-based fee
- The absence of an exception correctly falls back to the standard calculation
- Balances are computed correctly against overridden amounts
## Test Data Patterns
The tests use minimal but representative data:
```python
# A member with attendance-based fee
members = [('Alice', 'A', {'2026-01': (750, 4)})]
# An exception reducing the fee
exceptions = {('alice', '2026-01'): {'amount': 400, 'note': 'Test exception'}}
# A matching payment
transactions = [{
'date': '2026-01-05',
'amount': 400,
'person': 'Alice',
'purpose': '2026-01',
'inferred_amount': 400,
'sender': 'Alice Sender',
'message': 'fee'
}]
```
## What's Not Tested
| Area | Status | Notes |
|------|--------|-------|
| Name matching logic | ❌ Not tested | `match_members()`, `_build_name_variants()` |
| Czech month parsing | ❌ Not tested | `parse_month_references()` |
| Fio bank data fetching | ❌ Not tested | Both API and HTML scraping |
| Sync deduplication | ❌ Not tested | `generate_sync_id()` |
| QR code generation | ❌ Not tested | `/qr` route |
| Payment inference | ❌ Not tested | `infer_payments.py` logic |
| Multi-person payment splitting | ❌ Not tested | Even split across members/months |
| Edge cases | ❌ Not tested | Empty sheets, malformed dates, etc. |
## Writing New Tests
### Adding a Flask route test
```python
# In tests/test_app.py
@patch('app.some_function')
def test_new_route(self, mock_fn):
mock_fn.return_value = expected_data
response = self.client.get('/new-route')
self.assertEqual(response.status_code, 200)
self.assertIn(b'expected content', response.data)
```
### Adding a reconciliation logic test
```python
# In tests/test_reconcile_exceptions.py (or a new test file)
def test_multi_month_payment(self):
members = [('Bob', 'A', {
'2026-01': (750, 3),
'2026-02': (750, 4)
})]
transactions = [{
'date': '2026-02-01',
'amount': 1500,
'person': 'Bob',
'purpose': '2026-01, 2026-02',
'inferred_amount': 1500,
'sender': 'Bob',
'message': 'leden+unor'
}]
result = reconcile(members, ['2026-01', '2026-02'], transactions)
bob = result['members']['Bob']
self.assertEqual(bob['months']['2026-01']['paid'], 750)
self.assertEqual(bob['months']['2026-02']['paid'], 750)
self.assertEqual(bob['total_balance'], 0)
```
---
*Testing documentation generated from comprehensive code analysis on 2026-03-03.*