# Unit Tests

Pytest tests for a single layer in isolation. External dependencies (connectors, logic, models) are patched.

## Conventions

- Use pytest.
- Patch external dependencies (e.g., connectors, logic) with `@patch`.
- Use fixtures: `fixture_client`, `faker`, `mocker`.
- Assert:
  - HTTP status codes (e.g., `assert resp.status_code == 200`)
  - Response content (e.g., `assert resp.json.items() >= expected.items()`)
  - Schema validation/serialization called — spy with `mocker.spy`
  - Mock calls — see below.

## Mock-call assertions: use `call_args_list`

Prefer `call_args_list` over `assert_called_once_with`, `assert_called_with`, or `call_count`. It makes the full call history and arguments explicit in a single assertion, which catches both wrong-argument and wrong-call-count regressions:

```python
assert mock_logic.call_args_list == [call(**expected)]
```

## Example — handler test

```python
from unittest.mock import patch, call
from payment.blueprints.report_payment.schemas import ReportPaymentPutSchema, ReportPaymentDetailSchema
from payment.blueprints.report_payment.logic import create_or_update_report_payment
from payment.connectors.s3 import get_object_metadata_by_s3_url

@patch('payment.connectors.s3.get_object_metadata_by_s3_url')
@patch('payment.blueprints.report_payment.logic.create_or_update_report_payment')
def test_create_or_update_report_payment_success(
    mock_logic, mock_s3, fixture_client, faker, mocker
) -> None:
    mock_filename = faker.file_name(extension='pdf')
    mock_s3.return_value = {'Metadata': {'original_filename': mock_filename}}
    payload = {
        'report_type': REPORT_TYPE.APPROVAL,
        'report_export_url': faker.uri(),
    }
    expected = {
        'target_type': f'{faker.word()}_{faker.word()}',
        'target_id': faker.pyint(),
        **payload,
    }
    url = (
        f'/reports/{expected["target_type"].replace("_", "-")}/{expected["target_id"]}/'
    )
    mock_logic.return_value = ReportPaymentFactory.build(**expected)
    spy_validation = mocker.spy(ReportPaymentPutSchema, 'load')
    spy_serialization = mocker.spy(ReportPaymentDetailSchema, 'dump')
    resp = fixture_client.put(url, json=payload)
    assert resp.status_code == 200
    assert resp.json.items() >= expected.items()
    assert spy_validation.called
    assert spy_serialization.called
    assert mock_logic.call_args_list == [call(**expected)]
```

## Example — logic test

```python
@patch('payment.logic.report_payment.ReportPayment')
def test_create_or_update_report_payment_create(mock_model) -> None:
    """Test for creation."""
    mock_model.get_by_target_and_type.return_value = None
    mock_report = ReportPaymentFactory.build()
    mock_created_report = ReportPaymentFactory.create()
    mock_model.create.return_value = mock_created_report
    result = logic.create_or_update_report_payment(
        target_type=mock_report.target_type,
        target_id=mock_report.target_id,
        report_type=mock_report.report_type,
        report_export_url=mock_report.report_export_url,
    )
    assert mock_model.get_by_target_and_type.call_args_list == [
        call(
            target_type=mock_report.target_type,
            target_id=mock_report.target_id,
            report_type=mock_report.report_type,
        )
    ]
    assert mock_model.create.call_args_list == [
        call(
            target_type=mock_report.target_type,
            target_id=mock_report.target_id,
            report_type=mock_report.report_type,
            report_export_url=mock_report.report_export_url,
        )
    ]
    assert result == mock_created_report
```
