Skip to content

Commit e2b7b7d

Browse files
Migrate Wikipedia-API from unittest to pytest (#514)
* Switch to pytest * Fix tox config * Fix failing test for MacOS and Python 3.10 * Convert few more files * Migrate test files from unittest to pytest - Migrated 11 test files from unittest to pytest - Converted unittest.TestCase classes to plain classes with pytest fixtures - Replaced all self.assert* methods with standard assert statements - Fixed pytest.raises usage and exception access patterns - All tests passing successfully Files migrated: - query_errors_test.py - query_submodules_test.py - async_http_client_test.py - async_wikipedia_page_test.py - http_client_test.py - wikipedia_test.py - wikipedia_page_test.py - wikipedia_page_section_test.py - extract_wiki_format_test.py - extract_html_format_test.py - search_enums_test.py * convert few more files * fix another batch
1 parent 605aceb commit e2b7b7d

33 files changed

Lines changed: 1665 additions & 1718 deletions

AGENTS.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -219,9 +219,9 @@ make html
219219
make run-tests
220220
```
221221

222-
This command runs both the unit tests and the CLI verification tests.
222+
This command runs both the unit tests and CLI verification tests.
223223

224-
- The unit tests are executed via `python3 -m unittest discover tests/ '*test.py'`. All test files are in the `tests/` directory and follow the `*test.py` naming pattern.
224+
- The unit tests are executed via `uv run pytest tests/`. All test files are in the `tests/` directory and follow the `*_test.py` naming pattern.
225225
- The CLI verification tests are run using `./tests/cli/test_cli.sh verify`.
226226

227227
### CLI Tests
@@ -240,7 +240,7 @@ You can run the CLI tests independently.
240240
make run-coverage
241241
```
242242

243-
Produces a coverage report and `coverage.xml` for the `wikipediaapi` package.
243+
Produces a coverage report and `coverage.xml` for the `wikipediaapi` package using pytest and pytest-cov.
244244

245245
### Code Coverage Requirements
246246

CONTRIBUTING.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ The main test command runs both unit tests and CLI verification tests::
6262

6363
make run-tests
6464

65-
* The unit tests are executed via ``python3 -m unittest discover tests/ '*test.py'``. All test files are in the ``tests/`` directory and follow the ``*test.py`` naming pattern.
65+
* The unit tests are executed via ``uv run pytest tests/``. All test files are in the ``tests/`` directory and follow the ``*_test.py`` naming pattern.
6666
* The CLI verification tests are run using ``./tests/cli/test_cli.sh verify``.
6767

6868
CLI Tests
@@ -83,6 +83,8 @@ Run tests with coverage to generate a coverage report and ``coverage.xml`` for t
8383

8484
make run-coverage
8585

86+
This uses pytest with pytest-cov to generate coverage reports.
87+
8688
Multi-Python Testing
8789
~~~~~~~~~~~~~~~~~~~~~
8890

DEVELOPMENT.rst

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,21 @@
11
Development
22
===========
33

4-
Prerequisities
4+
Prerequisites
55
--------------
6+
67
* Make
7-
* Python3.10+
8+
* Python 3.10+
89
* Pip
910

1011
Makefile targets
11-
----------------
12+
-----------------
1213
* ``make run-pre-commit`` - lints source code
1314
* ``make requirements-all`` - install all requirements
1415
* ``make requirements`` - install package requirements
1516
* ``make requirements-dev`` - install development requirements
16-
* ``make run-tests`` - run unit tests
17-
* ``make run-coverage`` - run code coverage
17+
* ``make run-tests`` - run unit tests (pytest)
18+
* ``make run-coverage`` - run code coverage (pytest-cov)
1819
* ``make pypi-html`` - generates single HTML documentation into ``pypi-doc.html``
1920
* ``make html`` - generates HTML documentation similar to RTFD into folder ``_build/html/``
2021
* ``make release`` - creates new release as well as git tag

Makefile

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ run-pre-commit:
2727
run-tests: run-tests-unit run-test-cli-verify run-tests-cli-unit
2828

2929
run-tests-unit:
30-
uv run python -m unittest discover tests/ '*test.py'
30+
uv run pytest tests/
3131

3232
run-test-cli-verify:
3333
uv run ./tests/cli/test_cli.sh verify
@@ -36,7 +36,7 @@ run-test-cli-record:
3636
uv run ./tests/cli/test_cli.sh record
3737

3838
run-tests-cli-unit:
39-
uv run python -m unittest tests.cli_test -v
39+
uv run pytest tests/cli_test.py -v
4040

4141
run-type-check:
4242
uv run mypy ./wikipediaapi
@@ -53,9 +53,7 @@ run-tox-ci:
5353
uv run tox -e py
5454

5555
run-coverage:
56-
uv run coverage run --source=wikipediaapi -m unittest discover tests/ '*test.py'
57-
uv run coverage report -m
58-
uv run coverage xml
56+
uv run pytest --cov=wikipediaapi --cov-report=term-missing --cov-report=xml tests/
5957

6058
run-validate-attributes-mappping:
6159
uv run python ./validate_attributes_mapping.py

pyproject.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ dev = [
5050
"pre-commit==4.5.1",
5151
"pygments==2.19.2",
5252
"pyupgrade==3.21.2",
53+
"pytest==8.3.5",
54+
"pytest-asyncio==0.26.0",
55+
"pytest-cov==6.1.0",
5356
"tox==4.50.3",
5457
"anyio[trio]==4.13.0",
5558
"respx==0.22.0",
@@ -151,3 +154,23 @@ source = ["wikipediaapi"]
151154

152155
[tool.coverage.report]
153156
show_missing = true
157+
158+
[tool.pytest.ini_options]
159+
testpaths = ["tests"]
160+
python_files = ["*_test.py", "test_*.py"]
161+
python_classes = ["Test*"]
162+
python_functions = ["test_*"]
163+
filterwarnings = [
164+
"ignore::DeprecationWarning:pytest_asyncio.plugin:",
165+
"ignore::pytest.PytestDeprecationWarning:asyncio_default_fixture_loop_scope",
166+
]
167+
addopts = [
168+
"--strict-markers",
169+
"--strict-config",
170+
"--verbose",
171+
]
172+
asyncio_mode = "auto"
173+
markers = [
174+
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
175+
"integration: marks tests as integration tests",
176+
]

tests/async_http_client_test.py

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
1-
import unittest
2-
31
import httpx
2+
import pytest
43
import respx
54

65
from tests.mock_data import user_agent
@@ -10,97 +9,98 @@
109
API_URL = "https://en.wikipedia.org/w/api.php"
1110

1211

13-
class TestAsyncHTTPClientInit(unittest.TestCase):
12+
class TestAsyncHTTPClientInit:
1413
"""Tests for AsyncHTTPClient initialisation."""
1514

1615
def test_user_agent_set_in_client_headers(self):
1716
client = AsyncHTTPClient(user_agent, "en")
18-
self.assertIn(user_agent, client._client.headers.get("User-Agent"))
17+
assert user_agent in client._client.headers.get("User-Agent")
1918

2019
def test_user_agent_appends_library_agent(self):
2120
client = AsyncHTTPClient(user_agent, "en")
2221
header = client._client.headers.get("User-Agent")
23-
self.assertIn(wikipediaapi.USER_AGENT, header)
22+
assert wikipediaapi.USER_AGENT in header
2423

2524
def test_language_set(self):
2625
client = AsyncHTTPClient(user_agent, "de")
27-
self.assertEqual(client.language, "de")
26+
assert client.language == "de"
2827

2928
def test_variant_set(self):
3029
client = AsyncHTTPClient(user_agent, "zh", variant="zh-tw")
31-
self.assertEqual(client.variant, "zh-tw")
30+
assert client.variant == "zh-tw"
3231

3332
def test_max_retries_stored(self):
3433
client = AsyncHTTPClient(user_agent, "en", max_retries=5)
35-
self.assertEqual(client._max_retries, 5)
34+
assert client._max_retries == 5
3635

3736
def test_retry_wait_stored(self):
3837
client = AsyncHTTPClient(user_agent, "en", retry_wait=2.5)
39-
self.assertEqual(client._retry_wait, 2.5)
38+
assert client._retry_wait == 2.5
4039

4140
def test_missing_user_agent_raises(self):
42-
with self.assertRaises(AssertionError):
41+
with pytest.raises(AssertionError):
4342
AsyncHTTPClient("en")
4443

4544

46-
class TestAsyncHTTPClientGet(unittest.IsolatedAsyncioTestCase):
45+
class TestAsyncHTTPClientGet:
4746
"""Tests for AsyncHTTPClient._get."""
4847

49-
def setUp(self):
48+
def setup_method(self):
5049
self.client = AsyncHTTPClient(user_agent, "en", max_retries=0, retry_wait=0.0)
5150

5251
@respx.mock
52+
@pytest.mark.asyncio
5353
async def test_successful_get_returns_json(self):
5454
data = {"query": {"pages": {"1": {"title": "Test"}}}}
5555
respx.get(API_URL).mock(return_value=httpx.Response(200, json=data))
5656
result = await self.client._get("en", {"action": "query"})
57-
self.assertEqual(result, data)
57+
assert result == data
5858

5959
@respx.mock
6060
async def test_get_de_uses_de_url(self):
6161
de_url = "https://de.wikipedia.org/w/api.php"
6262
data = {"ok": True}
6363
route = respx.get(de_url).mock(return_value=httpx.Response(200, json=data))
6464
await self.client._get("de", {"action": "query"})
65-
self.assertTrue(route.called)
65+
assert route.called is True
6666

6767
@respx.mock
6868
async def test_404_raises_http_error(self):
6969
respx.get(API_URL).mock(return_value=httpx.Response(404))
70-
with self.assertRaises(wikipediaapi.WikiHttpError) as ctx:
70+
with pytest.raises(wikipediaapi.WikiHttpError) as ctx:
7171
await self.client._get("en", {"action": "query"})
72-
self.assertEqual(ctx.exception.status_code, 404)
72+
assert ctx.value.status_code == 404
7373

7474
@respx.mock
7575
async def test_429_raises_rate_limit_error(self):
7676
respx.get(API_URL).mock(return_value=httpx.Response(429, headers={"Retry-After": "10"}))
77-
with self.assertRaises(wikipediaapi.WikiRateLimitError) as ctx:
77+
with pytest.raises(wikipediaapi.WikiRateLimitError) as ctx:
7878
await self.client._get("en", {"action": "query"})
79-
self.assertEqual(ctx.exception.retry_after, 10)
79+
assert ctx.value.retry_after == 10
8080

8181
@respx.mock
8282
async def test_timeout_raises_timeout_error(self):
8383
respx.get(API_URL).mock(side_effect=httpx.TimeoutException("timeout"))
84-
with self.assertRaises(wikipediaapi.WikiHttpTimeoutError):
84+
with pytest.raises(wikipediaapi.WikiHttpTimeoutError):
8585
await self.client._get("en", {"action": "query"})
8686

8787
@respx.mock
8888
async def test_connect_error_raises_connection_error(self):
8989
respx.get(API_URL).mock(side_effect=httpx.ConnectError("err"))
90-
with self.assertRaises(wikipediaapi.WikiConnectionError):
90+
with pytest.raises(wikipediaapi.WikiConnectionError):
9191
await self.client._get("en", {"action": "query"})
9292

9393
@respx.mock
9494
async def test_invalid_json_raises_invalid_json_error(self):
9595
respx.get(API_URL).mock(return_value=httpx.Response(200, content=b"not-json"))
96-
with self.assertRaises(wikipediaapi.WikiInvalidJsonError):
96+
with pytest.raises(wikipediaapi.WikiInvalidJsonError):
9797
await self.client._get("en", {"action": "query"})
9898

9999

100-
class TestAsyncHTTPClientRetry(unittest.IsolatedAsyncioTestCase):
100+
class TestAsyncHTTPClientRetry:
101101
"""Tests for retry behaviour in AsyncHTTPClient."""
102102

103-
def setUp(self):
103+
def setup_method(self):
104104
self.client = AsyncHTTPClient(user_agent, "en", max_retries=2, retry_wait=0.0)
105105

106106
@respx.mock
@@ -109,21 +109,21 @@ async def test_retries_on_500(self):
109109
responses = iter([httpx.Response(500), httpx.Response(200, json=success)])
110110
route = respx.get(API_URL).mock(side_effect=lambda req: next(responses))
111111
result = await self.client._get("en", {"action": "query"})
112-
self.assertEqual(result, success)
113-
self.assertEqual(route.call_count, 2)
112+
assert result == success
113+
assert route.call_count == 2
114114

115115
@respx.mock
116116
async def test_exhausts_retries_on_500(self):
117117
route = respx.get(API_URL).mock(return_value=httpx.Response(500))
118-
with self.assertRaises(wikipediaapi.WikiHttpError):
118+
with pytest.raises(wikipediaapi.WikiHttpError):
119119
await self.client._get("en", {"action": "query"})
120-
self.assertEqual(route.call_count, 3)
120+
assert route.call_count == 3
121121

122122
@respx.mock
123123
async def test_retries_on_429(self):
124124
success = {"ok": True}
125125
responses = iter([httpx.Response(429), httpx.Response(200, json=success)])
126126
route = respx.get(API_URL).mock(side_effect=lambda req: next(responses))
127127
result = await self.client._get("en", {"action": "query"})
128-
self.assertEqual(result, success)
129-
self.assertEqual(route.call_count, 2)
128+
assert result == success
129+
assert route.call_count == 2

0 commit comments

Comments
 (0)