Skip to content

Commit 663ef00

Browse files
wilsonfreitasclaude
andcommitted
Phase 7 (Part 2): Testing Overhaul - Comprehensive negative and async tests
- Add comprehensive negative test cases for bcb.currency module (12 tests): - API failures: 404, 429, 500 responses for ID list and rate fetching - Malformed data: wrong column count, invalid date format, invalid numeric conversion - Invalid input: empty symbol list, invalid dates, all unknown symbols - Edge cases: mixed valid/invalid symbols (returns only valid ones) - Add comprehensive negative test cases for bcb.sgs module (15 tests): - API failures: 404, 429, 500 responses - Malformed data: invalid JSON, missing expected fields - Invalid input: negative/zero codes, non-numeric codes, invalid frequency - Edge cases: empty lists, very large codes, text output validation - Add async API tests for all modules (9 tests using pytest-anyio): - SGS async: async_get_json(), async_get() with single/multiple codes, text output - Currency async: async_get_symbol(), async_get() with single symbol - OData async: async_text(), async_collect(), endpoint.async_get() shortcut - All 103 unit tests now pass (67 original + 12 currency negative + 15 SGS negative + 9 async) - Formatted all new test files with ruff Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
1 parent 3b05ba7 commit 663ef00

3 files changed

Lines changed: 681 additions & 0 deletions

File tree

tests/test_async.py

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
"""Async API tests using pytest-anyio.
2+
3+
Tests for async_get() functions in sgs, currency, and odata modules.
4+
"""
5+
6+
import re
7+
from datetime import datetime
8+
9+
import pytest
10+
11+
from bcb import currency, sgs
12+
from bcb.odata.api import Expectativas
13+
from tests.conftest import (
14+
CURRENCY_ID_LIST_HTML,
15+
CURRENCY_LIST_CSV,
16+
CURRENCY_RATE_CSV,
17+
SGS_JSON_5,
18+
ODATA_METADATA_XML,
19+
ODATA_SERVICE_ROOT_JSON,
20+
ODATA_QUERY_RESPONSE_JSON,
21+
)
22+
23+
pytestmark = pytest.mark.anyio
24+
25+
START = datetime(2020, 12, 1)
26+
END = datetime(2020, 12, 7)
27+
28+
PTAX_ID_LIST_URL = re.compile(r".*exibeFormularioConsultaBoletim.*")
29+
PTAX_CSV_DOWNLOAD_URL = re.compile(r".*www4\.bcb\.gov\.br.*\.csv")
30+
PTAX_RATE_URL = re.compile(r".*gerarCSVFechamento.*")
31+
SGS_CODE_URL = re.compile(r".*bcdata\.sgs\..*")
32+
33+
34+
# ---------------------------------------------------------------------------
35+
# SGS async tests
36+
# ---------------------------------------------------------------------------
37+
38+
39+
async def test_async_get_json_single_code(httpx_mock):
40+
"""Test async_get_json() with a single code."""
41+
httpx_mock.add_response(
42+
url=SGS_CODE_URL,
43+
text=SGS_JSON_5,
44+
status_code=200,
45+
)
46+
result = await sgs.async_get_json(1)
47+
assert isinstance(result, str)
48+
assert "data" in result
49+
assert "valor" in result
50+
51+
52+
async def test_async_get_single_code_returns_dataframe(httpx_mock):
53+
"""Test async_get() with a single code returns DataFrame."""
54+
httpx_mock.add_response(
55+
url=SGS_CODE_URL,
56+
text=SGS_JSON_5,
57+
status_code=200,
58+
)
59+
df = await sgs.async_get(1)
60+
assert df is not None
61+
assert len(df) == 5
62+
63+
64+
async def test_async_get_multiple_codes_concurrent(httpx_mock):
65+
"""Test async_get() with multiple codes uses concurrent requests."""
66+
# Add response for each code - should be called concurrently
67+
httpx_mock.add_response(
68+
url=SGS_CODE_URL,
69+
text=SGS_JSON_5,
70+
status_code=200,
71+
)
72+
httpx_mock.add_response(
73+
url=SGS_CODE_URL,
74+
text=SGS_JSON_5,
75+
status_code=200,
76+
)
77+
df = await sgs.async_get([1, 11], multi=True)
78+
assert df is not None
79+
# Should have two columns (one for each code)
80+
assert df.shape[1] == 2
81+
82+
83+
async def test_async_get_text_output(httpx_mock):
84+
"""Test async_get() with output='text' returns JSON string."""
85+
httpx_mock.add_response(
86+
url=SGS_CODE_URL,
87+
text=SGS_JSON_5,
88+
status_code=200,
89+
)
90+
result = await sgs.async_get(1, output="text")
91+
assert isinstance(result, str)
92+
assert "data" in result
93+
94+
95+
# ---------------------------------------------------------------------------
96+
# Currency async tests
97+
# ---------------------------------------------------------------------------
98+
99+
100+
async def test_async_get_symbol_returns_dataframe(httpx_mock):
101+
"""Test async_get_symbol() returns DataFrame."""
102+
httpx_mock.add_response(
103+
url=PTAX_ID_LIST_URL,
104+
content=CURRENCY_ID_LIST_HTML,
105+
status_code=200,
106+
)
107+
httpx_mock.add_response(
108+
url=PTAX_CSV_DOWNLOAD_URL,
109+
text=CURRENCY_LIST_CSV,
110+
status_code=200,
111+
)
112+
httpx_mock.add_response(
113+
url=PTAX_RATE_URL,
114+
text=CURRENCY_RATE_CSV,
115+
status_code=200,
116+
headers={"Content-Type": "text/csv"},
117+
)
118+
df = await currency._async_get_symbol("USD", START, END)
119+
assert df is not None
120+
assert ("USD", "bid") in df.columns
121+
assert ("USD", "ask") in df.columns
122+
123+
124+
async def test_async_get_single_symbol_returns_dataframe(httpx_mock):
125+
"""Test async_get() with single symbol returns DataFrame."""
126+
httpx_mock.add_response(
127+
url=PTAX_ID_LIST_URL,
128+
content=CURRENCY_ID_LIST_HTML,
129+
status_code=200,
130+
)
131+
httpx_mock.add_response(
132+
url=PTAX_CSV_DOWNLOAD_URL,
133+
text=CURRENCY_LIST_CSV,
134+
status_code=200,
135+
)
136+
httpx_mock.add_response(
137+
url=PTAX_RATE_URL,
138+
text=CURRENCY_RATE_CSV,
139+
status_code=200,
140+
headers={"Content-Type": "text/csv"},
141+
)
142+
df = await currency.async_get("USD", START, END)
143+
assert df is not None
144+
145+
146+
# ---------------------------------------------------------------------------
147+
# OData async tests
148+
# ---------------------------------------------------------------------------
149+
150+
151+
async def test_odata_query_async_text(httpx_mock):
152+
"""Test ODataQuery.async_text() returns JSON string."""
153+
httpx_mock.add_response(
154+
url="https://olinda.bcb.gov.br/olinda/servico/Expectativas/versao/v1/odata/",
155+
text=ODATA_SERVICE_ROOT_JSON,
156+
status_code=200,
157+
)
158+
httpx_mock.add_response(
159+
url="https://olinda.bcb.gov.br/olinda/servico/Expectativas/versao/v1/odata/$metadata",
160+
content=ODATA_METADATA_XML,
161+
status_code=200,
162+
)
163+
httpx_mock.add_response(
164+
url=re.compile(r".*ExpectativasMercadoAnuais.*"),
165+
text=ODATA_QUERY_RESPONSE_JSON,
166+
status_code=200,
167+
)
168+
api = Expectativas()
169+
ep = api.get_endpoint("ExpectativasMercadoAnuais")
170+
result = await ep.query().limit(1).async_text()
171+
assert isinstance(result, str)
172+
assert "value" in result
173+
174+
175+
async def test_odata_query_async_collect(httpx_mock):
176+
"""Test ODataQuery.async_collect() returns DataFrame."""
177+
httpx_mock.add_response(
178+
url="https://olinda.bcb.gov.br/olinda/servico/Expectativas/versao/v1/odata/",
179+
text=ODATA_SERVICE_ROOT_JSON,
180+
status_code=200,
181+
)
182+
httpx_mock.add_response(
183+
url="https://olinda.bcb.gov.br/olinda/servico/Expectativas/versao/v1/odata/$metadata",
184+
content=ODATA_METADATA_XML,
185+
status_code=200,
186+
)
187+
httpx_mock.add_response(
188+
url=re.compile(r".*ExpectativasMercadoAnuais.*"),
189+
text=ODATA_QUERY_RESPONSE_JSON,
190+
status_code=200,
191+
)
192+
api = Expectativas()
193+
ep = api.get_endpoint("ExpectativasMercadoAnuais")
194+
df = await ep.query().limit(1).async_collect()
195+
assert df is not None
196+
assert len(df) == 1
197+
198+
199+
async def test_endpoint_async_get(httpx_mock):
200+
"""Test Endpoint.async_get() shortcut."""
201+
httpx_mock.add_response(
202+
url="https://olinda.bcb.gov.br/olinda/servico/Expectativas/versao/v1/odata/",
203+
text=ODATA_SERVICE_ROOT_JSON,
204+
status_code=200,
205+
)
206+
httpx_mock.add_response(
207+
url="https://olinda.bcb.gov.br/olinda/servico/Expectativas/versao/v1/odata/$metadata",
208+
content=ODATA_METADATA_XML,
209+
status_code=200,
210+
)
211+
httpx_mock.add_response(
212+
url=re.compile(r".*ExpectativasMercadoAnuais.*"),
213+
text=ODATA_QUERY_RESPONSE_JSON,
214+
status_code=200,
215+
)
216+
api = Expectativas()
217+
ep = api.get_endpoint("ExpectativasMercadoAnuais")
218+
df = await ep.async_get(limit=1)
219+
assert df is not None
220+
assert len(df) == 1

0 commit comments

Comments
 (0)