Skip to content

Commit caf04a5

Browse files
wilsonfreitasclaude
andcommitted
Overhaul test suite with mocked HTTP and integration separation (Phase 5)
- Add tests/conftest.py: autouse cache-clear fixture + shared mock data constants - Rewrite tests/test_currency.py: fully mocked with pytest-httpx (13 tests) - Add tests/test_exceptions.py: exception hierarchy and attribute tests (7 tests) - Add tests/test_odata.py: mocked OData service, metadata, query, operator tests (9 tests) - Rewrite tests/sgs/test_series.py: keep pure unit tests, add mocked HTTP tests (14 tests) - Mark TestGetNonPerformingLoans as @pytest.mark.integration in test_regional_economy.py - Add tests/integration/: live-network tests for currency, SGS, Expectativas - Move test_expectativas.py content to tests/integration/test_expectativas.py - Register "integration" marker in pyproject.toml Run unit tests only: poetry run pytest -m "not integration" Run all tests: poetry run pytest Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b14073b commit caf04a5

12 files changed

Lines changed: 667 additions & 107 deletions

pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ build-backend = "poetry.core.masonry.api"
4040

4141
[tool.pytest.ini_options]
4242
testpaths = ["tests"]
43+
markers = [
44+
"integration: marks tests as requiring live BCB network access (deselect with '-m \"not integration\"')",
45+
]
4346

4447
[tool.coverage.run]
4548
source = ["bcb"]

tests/conftest.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import pytest
2+
from bcb import currency
3+
4+
5+
# ---------------------------------------------------------------------------
6+
# Mock data constants
7+
# ---------------------------------------------------------------------------
8+
9+
CURRENCY_ID_LIST_HTML = b"""
10+
<html><body><form>
11+
<select name="ChkMoeda">
12+
<option value="61">DOLLAR DOS EUA</option>
13+
</select>
14+
</form></body></html>
15+
"""
16+
17+
# First row is treated as header by pd.read_csv, then overwritten by df.columns = [...]
18+
CURRENCY_LIST_CSV = (
19+
"Codigo;Nome;Simbolo;CodPais;NomePais;Tipo;DataExclusao\n"
20+
"61;DOLLAR DOS EUA;USD;249;EUA;A;\n"
21+
)
22+
23+
# 8 columns, no header, date format DDMMYYYY, comma as decimal separator
24+
CURRENCY_RATE_CSV = (
25+
"01122020;0;0;0;5,0000;5,1000;0;0\n"
26+
"02122020;0;0;0;5,0100;5,1100;0;0\n"
27+
"03122020;0;0;0;5,0200;5,1200;0;0\n"
28+
"04122020;0;0;0;5,0300;5,1300;0;0\n"
29+
"07122020;0;0;0;5,0400;5,1400;0;0\n"
30+
)
31+
32+
SGS_JSON_5 = (
33+
'[{"data":"18/01/2021","valor":"5.1234"},'
34+
'{"data":"19/01/2021","valor":"5.2345"},'
35+
'{"data":"20/01/2021","valor":"5.3456"},'
36+
'{"data":"21/01/2021","valor":"5.4567"},'
37+
'{"data":"22/01/2021","valor":"5.5678"}]'
38+
)
39+
40+
ODATA_METADATA_XML = b"""<?xml version="1.0" encoding="utf-8"?>
41+
<edmx:Edmx Version="4.0" xmlns:edmx="http://docs.oasis-open.org/odata/ns/edmx">
42+
<edmx:DataServices>
43+
<Schema Namespace="IFBCB_DadosSeries_v2" xmlns="http://docs.oasis-open.org/odata/ns/edm">
44+
<EntityType Name="Expectativa">
45+
<Property Name="Indicador" Type="Edm.String"/>
46+
<Property Name="Data" Type="Edm.Date"/>
47+
<Property Name="Mediana" Type="Edm.Decimal"/>
48+
</EntityType>
49+
<EntityContainer Name="IFBCB_DadosSeries_v2">
50+
<EntitySet Name="ExpectativasMercadoAnuais"
51+
EntityType="IFBCB_DadosSeries_v2.Expectativa"/>
52+
</EntityContainer>
53+
</Schema>
54+
</edmx:DataServices>
55+
</edmx:Edmx>"""
56+
57+
ODATA_SERVICE_ROOT_JSON = """{
58+
"@odata.context": "https://olinda.bcb.gov.br/olinda/servico/Expectativas/versao/v1/odata/$metadata",
59+
"value": [
60+
{"name": "ExpectativasMercadoAnuais", "kind": "EntitySet", "url": "ExpectativasMercadoAnuais"}
61+
]
62+
}"""
63+
64+
ODATA_QUERY_RESPONSE_JSON = """{
65+
"value": [
66+
{"Indicador": "IPCA", "Data": "2021-01-04", "Mediana": 4.5}
67+
]
68+
}"""
69+
70+
71+
# ---------------------------------------------------------------------------
72+
# Fixtures
73+
# ---------------------------------------------------------------------------
74+
75+
@pytest.fixture(autouse=True)
76+
def clear_currency_cache():
77+
"""Clear module-level currency cache before and after each test."""
78+
currency.CACHE.clear()
79+
yield
80+
currency.CACHE.clear()

tests/integration/__init__.py

Whitespace-only changes.

tests/integration/test_currency.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
"""Integration tests for bcb.currency — require live BCB network access."""
2+
from datetime import datetime, date
3+
4+
import pandas as pd
5+
import pytest
6+
from pytest import mark
7+
8+
from bcb import currency
9+
from bcb.exceptions import CurrencyNotFoundError
10+
11+
12+
@pytest.mark.integration
13+
@mark.flaky(max_runs=20, min_passes=1)
14+
def test_currency_id():
15+
assert currency._get_currency_id("USD") == 61
16+
17+
18+
@pytest.mark.integration
19+
@mark.flaky(max_runs=20, min_passes=1)
20+
def test_currency_get_symbol():
21+
start_date = datetime.strptime("2020-12-01", "%Y-%m-%d")
22+
end_date = datetime.strptime("2020-12-05", "%Y-%m-%d")
23+
x = currency._get_symbol("USD", start_date, end_date)
24+
assert isinstance(x, pd.DataFrame)
25+
x = currency._get_symbol("ZAR", start_date, end_date)
26+
assert x is None
27+
x = currency.get("USD", start_date, end_date)
28+
assert isinstance(x, pd.DataFrame)
29+
with pytest.raises(CurrencyNotFoundError):
30+
currency.get("ZAR", start_date, end_date)
31+
with pytest.raises(CurrencyNotFoundError):
32+
currency.get(["ZAR", "ZZ1"], start_date, end_date)
33+
34+
35+
@pytest.mark.integration
36+
@mark.flaky(max_runs=20, min_passes=1)
37+
def test_get_valid_currency_list():
38+
x = currency._get_valid_currency_list(date.today())
39+
assert x is not None
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
"""Integration tests for bcb.odata Expectativas — require live BCB network access."""
2+
import pandas as pd
3+
import pytest
4+
from datetime import datetime
5+
from pytest import fixture
6+
7+
from bcb import Expectativas
8+
9+
10+
@fixture
11+
def endpoints():
12+
ep = Expectativas()
13+
return [ep.get_endpoint(e.data["name"]) for e in ep.service.endpoints]
14+
15+
16+
@pytest.mark.integration
17+
def test_expectativas_date_format(endpoints):
18+
date_columns = {"Data", "dataHoraCotacao", "InicioPeriodo", "FimPeriodo", "DataVigencia"}
19+
for endpoint in endpoints:
20+
query = endpoint.query().limit(1)
21+
data = query.collect()
22+
assert isinstance(data, pd.DataFrame)
23+
assert data.shape[0] == 1
24+
for col in date_columns:
25+
if col in data.columns:
26+
assert isinstance(data[col].iloc[0], datetime)

tests/integration/test_sgs.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
"""Integration tests for bcb.sgs — require live BCB network access."""
2+
from datetime import datetime
3+
4+
import pandas as pd
5+
import pytest
6+
7+
from bcb import sgs
8+
9+
10+
@pytest.mark.integration
11+
def test_get_series():
12+
x = sgs.get(1, last=10)
13+
assert isinstance(x, pd.DataFrame)
14+
assert x.columns.tolist() == ["1"]
15+
assert len(x) == 10
16+
17+
x = sgs.get({"USDBRL": 1}, last=5)
18+
assert isinstance(x, pd.DataFrame)
19+
assert x.columns.tolist() == ["USDBRL"]
20+
assert len(x) == 5
21+
22+
x = sgs.get({"USDBRL": 1}, start="2021-01-18", end="2021-01-22")
23+
assert isinstance(x, pd.DataFrame)
24+
assert x.columns.tolist() == ["USDBRL"]
25+
assert len(x) == 5
26+
assert x.index[0] == datetime.strptime("2021-01-18", "%Y-%m-%d")
27+
assert x.index[-1] == datetime.strptime("2021-01-22", "%Y-%m-%d")
28+
29+
30+
@pytest.mark.integration
31+
def test_json_return():
32+
x = sgs.get_json(1, last=10)
33+
assert isinstance(x, str)
34+
assert len(x) > 0
35+
assert x.startswith("[")
36+
assert x.endswith("]")
37+
38+
39+
@pytest.mark.integration
40+
def test_json_return_long_series_error():
41+
try:
42+
sgs.get_json(1, start="2000-01-01", end="2023-01-01")
43+
except Exception as e:
44+
assert "no máximo" in str(e)
45+
else:
46+
assert False, "Expected an exception but none was raised."
47+
48+
try:
49+
sgs.get_json(1, last=50)
50+
except Exception as e:
51+
assert "máxima" in str(e)
52+
else:
53+
assert False, "Expected an exception but none was raised."

tests/sgs/test_regional_economy.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ def test_get_non_performing_loans_codes_by_state_total(self, states, expected_co
1515
assert get_non_performing_loans_codes(states) == expected_codes
1616

1717

18+
@pytest.mark.integration
1819
class TestGetNonPerformingLoans:
1920
@pytest.mark.parametrize("states,expected_columns", [
2021
(["BA"], ["BA"]),

0 commit comments

Comments
 (0)