Files
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

4.9 KiB

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

make test        # Run all tests
make test-v      # Run with verbose output

Under the hood:

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:

@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:

# 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

# 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

# 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.